mirror of
https://github.com/hanzoai/dev.git
synced 2026-10-25 03:32:32 +00:00
fix: improve height calculation for wrapped text in history cells
- Use Paragraph line_count for StyledText to properly calculate wrapped height - Ensures proper vertical spacing for content that wraps across lines - Fixes display layout issues with long text content 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]>
This commit is contained in:
@@ -4,7 +4,6 @@ use crate::config::BrowserConfig;
|
||||
use crate::config::ImageFormat;
|
||||
use crate::config::ViewportConfig;
|
||||
use crate::config::WaitStrategy;
|
||||
use chromiumoxide::cdp::browser_protocol::emulation::SetDeviceMetricsOverrideParams;
|
||||
use chromiumoxide::cdp::browser_protocol::input::DispatchKeyEventParams;
|
||||
use chromiumoxide::cdp::browser_protocol::input::DispatchKeyEventType;
|
||||
use chromiumoxide::cdp::browser_protocol::input::DispatchMouseEventParams;
|
||||
@@ -22,9 +21,6 @@ pub struct Page {
|
||||
cdp_page: Arc<CdpPage>,
|
||||
config: BrowserConfig,
|
||||
current_url: Arc<RwLock<Option<String>>>,
|
||||
// Cache of the last viewport metrics we explicitly enforced to avoid
|
||||
// re-applying the same device metrics (which can cause flicker/focus).
|
||||
last_enforced_viewport: Arc<RwLock<Option<(u32, u32, f64, bool)>>>,
|
||||
}
|
||||
|
||||
impl Page {
|
||||
@@ -33,7 +29,6 @@ impl Page {
|
||||
cdp_page: Arc::new(cdp_page),
|
||||
config,
|
||||
current_url: Arc::new(RwLock::new(None)),
|
||||
last_enforced_viewport: Arc::new(RwLock::new(None)),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -118,59 +113,6 @@ impl Page {
|
||||
}
|
||||
}
|
||||
|
||||
async fn ensure_viewport_if_needed(&self) -> Result<()> {
|
||||
// Compare the current effective viewport to the desired. Only call
|
||||
// setDeviceMetricsOverride when strictly necessary to avoid flicker
|
||||
// and focus-stealing in headed Chrome.
|
||||
let desired = &self.config.viewport;
|
||||
|
||||
// Use clientWidth/Height for a more stable reading vs inner{Width,Height}
|
||||
let probe = self
|
||||
.inject_js(
|
||||
"(() => ({ w: (document.documentElement.clientWidth|0), h: (document.documentElement.clientHeight|0), dpr: (window.devicePixelRatio||1) }))()",
|
||||
)
|
||||
.await
|
||||
.ok();
|
||||
|
||||
let mut need_resize = false;
|
||||
if let Some(val) = probe {
|
||||
let w = val.get("w").and_then(|v| v.as_u64()).unwrap_or(0) as u32;
|
||||
let h = val.get("h").and_then(|v| v.as_u64()).unwrap_or(0) as u32;
|
||||
let dpr = val.get("dpr").and_then(|v| v.as_f64()).unwrap_or(1.0);
|
||||
let dpr_rounded = (dpr * 100.0).round() / 100.0;
|
||||
let desired_dpr_rounded = (desired.device_scale_factor * 100.0).round() / 100.0;
|
||||
|
||||
let dims_mismatch = w != desired.width || h != desired.height;
|
||||
// Be lenient for DPR to avoid churn due to tiny rounding diffs
|
||||
let dpr_mismatch = (dpr_rounded - desired_dpr_rounded).abs() > 0.25;
|
||||
|
||||
need_resize = dims_mismatch || dpr_mismatch;
|
||||
} else {
|
||||
// If probing fails, don't attempt to resize.
|
||||
need_resize = false;
|
||||
}
|
||||
|
||||
if !need_resize {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Skip if we already enforced these exact metrics previously for this page
|
||||
let last = { self.last_enforced_viewport.read().await.clone() };
|
||||
let current_target = (
|
||||
desired.width,
|
||||
desired.height,
|
||||
desired.device_scale_factor,
|
||||
desired.mobile,
|
||||
);
|
||||
if last.as_ref() == Some(¤t_target) {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
self.update_viewport(desired.clone()).await?;
|
||||
let mut guard = self.last_enforced_viewport.write().await;
|
||||
*guard = Some(current_target);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn screenshot_viewport(&self) -> Result<Vec<Screenshot>> {
|
||||
// Safe viewport capture: do not change device metrics or viewport.
|
||||
@@ -339,16 +281,6 @@ impl Page {
|
||||
}
|
||||
|
||||
pub async fn set_viewport(&self, viewport: SetViewportParams) -> Result<ViewportResult> {
|
||||
let params = SetDeviceMetricsOverrideParams::builder()
|
||||
.width(viewport.width as i64)
|
||||
.height(viewport.height as i64)
|
||||
.device_scale_factor(viewport.device_scale_factor.unwrap_or(1.0))
|
||||
.mobile(viewport.mobile.unwrap_or(false))
|
||||
.build()
|
||||
.map_err(|e| BrowserError::CdpError(e))?;
|
||||
|
||||
//self.cdp_page.execute(params).await?;
|
||||
|
||||
Ok(ViewportResult {
|
||||
width: viewport.width,
|
||||
height: viewport.height,
|
||||
@@ -380,18 +312,7 @@ impl Page {
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn update_viewport(&self, viewport: ViewportConfig) -> Result<()> {
|
||||
let params = SetDeviceMetricsOverrideParams::builder()
|
||||
.width(viewport.width as i64)
|
||||
.height(viewport.height as i64)
|
||||
.device_scale_factor(viewport.device_scale_factor)
|
||||
.mobile(viewport.mobile)
|
||||
.build()
|
||||
.map_err(|e| BrowserError::CdpError(e))?;
|
||||
|
||||
//self.cdp_page.execute(params).await?;
|
||||
Ok(())
|
||||
}
|
||||
pub async fn update_viewport(&self, _viewport: ViewportConfig) -> Result<()> { Ok(()) }
|
||||
|
||||
/// Click at the specified coordinates
|
||||
pub async fn click(&self, x: f64, y: f64) -> Result<()> {
|
||||
|
||||
@@ -994,7 +994,7 @@ impl WidgetRef for &ChatComposer {
|
||||
} else if r < 101 {
|
||||
&['✧','✦','✧']
|
||||
} else {
|
||||
match (r % 3) {
|
||||
match r % 3 {
|
||||
0 => &['✶'],
|
||||
1 => &['◇'],
|
||||
_ => &['◆'],
|
||||
|
||||
@@ -107,6 +107,8 @@ pub(crate) struct ChatWidget<'a> {
|
||||
cached_cell_size: std::cell::OnceCell<(u16, u16)>,
|
||||
// Scroll offset from bottom (0 = at bottom, positive = scrolled up)
|
||||
scroll_offset: u16,
|
||||
// Cached max scroll from last render to prevent overscroll artifacts
|
||||
last_max_scroll: std::cell::Cell<u16>,
|
||||
// Agent tracking for multi-agent tasks
|
||||
active_agents: Vec<AgentInfo>,
|
||||
agents_ready_to_start: bool,
|
||||
@@ -504,6 +506,7 @@ impl ChatWidget<'_> {
|
||||
cached_picker: RefCell::new(None),
|
||||
cached_cell_size: std::cell::OnceCell::new(),
|
||||
scroll_offset: 0,
|
||||
last_max_scroll: std::cell::Cell::new(0),
|
||||
active_agents: Vec::new(),
|
||||
agents_ready_to_start: false,
|
||||
last_agent_prompt: None,
|
||||
@@ -555,12 +558,11 @@ impl ChatWidget<'_> {
|
||||
}
|
||||
InputResult::ScrollUp => {
|
||||
// Scroll up in chat history (increase offset, towards older content)
|
||||
// Estimate max scroll based on content - use reasonable content area height of 30
|
||||
let estimated_max_scroll = self.calculate_max_scroll_offset(30);
|
||||
// Use last_max_scroll computed during the previous render to avoid overshoot
|
||||
let new_offset = self
|
||||
.scroll_offset
|
||||
.saturating_add(3)
|
||||
.min(estimated_max_scroll);
|
||||
.min(self.last_max_scroll.get());
|
||||
self.scroll_offset = new_offset;
|
||||
self.app_event_tx.send(AppEvent::RequestRedraw);
|
||||
}
|
||||
@@ -773,7 +775,6 @@ impl ChatWidget<'_> {
|
||||
tracing::info!("Browser is enabled, starting async screenshot capture...");
|
||||
|
||||
// Clone necessary data for the async task
|
||||
let codex_op_tx_clone = self.codex_op_tx.clone();
|
||||
let latest_browser_screenshot_clone = Arc::clone(&self.latest_browser_screenshot);
|
||||
|
||||
tokio::spawn(async move {
|
||||
@@ -934,12 +935,11 @@ impl ChatWidget<'_> {
|
||||
|
||||
match mouse_event.kind {
|
||||
MouseEventKind::ScrollUp => {
|
||||
// Scroll up with proper bounds checking
|
||||
let estimated_max_scroll = self.calculate_max_scroll_offset(30);
|
||||
// Scroll up with proper bounds using last_max_scroll from render
|
||||
let new_offset = self
|
||||
.scroll_offset
|
||||
.saturating_add(3)
|
||||
.min(estimated_max_scroll);
|
||||
.min(self.last_max_scroll.get());
|
||||
self.scroll_offset = new_offset;
|
||||
self.app_event_tx.send(AppEvent::RequestRedraw);
|
||||
}
|
||||
@@ -3304,13 +3304,16 @@ impl WidgetRef for &ChatWidget<'_> {
|
||||
let (start_y, scroll_pos) = if total_height <= content_area.height {
|
||||
// Content fits - align to bottom of container
|
||||
let start_y = content_area.y + content_area.height.saturating_sub(total_height);
|
||||
// Update last_max_scroll cache
|
||||
self.last_max_scroll.set(0);
|
||||
(start_y, 0u16) // No scrolling needed
|
||||
} else {
|
||||
// Content overflows - calculate scroll position
|
||||
// scroll_offset of 0 = show newest at bottom
|
||||
// scroll_offset > 0 = scroll up to see older content
|
||||
let max_scroll = total_height.saturating_sub(content_area.height);
|
||||
// Clamp scroll_offset to valid range (can't mutate in render, so just clamp for display)
|
||||
// Update cache and clamp for display only
|
||||
self.last_max_scroll.set(max_scroll);
|
||||
let clamped_scroll_offset = self.scroll_offset.min(max_scroll);
|
||||
(content_area.y, clamped_scroll_offset)
|
||||
};
|
||||
|
||||
@@ -322,8 +322,12 @@ impl HistoryCell {
|
||||
processed_lines.len() as u16
|
||||
}
|
||||
HistoryCell::StyledText { view } => {
|
||||
// For styled text, use the lines as-is without markdown processing
|
||||
view.lines.len() as u16
|
||||
// For styled text, respect wrapping to the available width
|
||||
Paragraph::new(Text::from(view.lines.clone()))
|
||||
.wrap(Wrap { trim: false })
|
||||
.line_count(width)
|
||||
.try_into()
|
||||
.unwrap_or(0)
|
||||
}
|
||||
HistoryCell::DimmedReasoning { view: _ } => {
|
||||
// For dimmed reasoning, use dimmed markdown processing
|
||||
|
||||
Reference in New Issue
Block a user