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:
James Peter
2025-08-13 21:08:18 +10:00
co-authored by Claude
parent dc31517bc6
commit 98b26df421
4 changed files with 19 additions and 91 deletions
+1 -80
View File
@@ -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(&current_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 => &['◇'],
_ => &['◆'],
+11 -8
View File
@@ -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)
};
+6 -2
View File
@@ -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