fix: Windows sub-agent completion halves TUI render width
Root cause: AgentComplete unconditionally calls resume_terminal() even when the terminal was never paused, causing a secondary EnterAlternateScreen on Windows that creates a new buffer whose width may differ from the window width. Additionally, ColorCompatBackend had no terminal_size cache, so size() fell through to crossterm::terminal::size() which on Windows returns the WinAPI buffer width rather than the window width. Changes: - AgentComplete: add event_broker.is_paused() guard - resume_terminal(): cache real terminal size before reset_viewport - Resize handler: also set terminal_size alongside forced_size - subagent_routing: 3x mark_history_updated -> bump_history_cell(idx) - color_compat: add terminal_size field, set_terminal_size(), fix size() fallback priority (forced_size > terminal_size) - tests: 3 unit tests for size() fallback chain Review feedback addressed: - forced_size now takes priority over terminal_size (gemini-code-assist) - Redundant map lookups removed in subagent_routing (both bots) - set_terminal_size moved before reset_terminal_viewport (greptile-apps) (cherry picked from commit 4463c46644a6e485e7e20dc2b19c29c2e8eb3c5c)
This commit is contained in:
@@ -43,6 +43,11 @@ pub(crate) struct ColorCompatBackend<W: Write> {
|
||||
/// Forcing the expected size prevents ratatui's internal `autoresize` from
|
||||
/// shrinking the viewport back to the stale dimension inside `draw()`.
|
||||
forced_size: Option<Size>,
|
||||
/// Cached terminal size from `crossterm::terminal::size()`, set after
|
||||
/// re-entering alt-screen to avoid stale buffer dimensions on Windows.
|
||||
/// Used as the primary fallback in `size()` before falling through to
|
||||
/// the live crossterm query.
|
||||
terminal_size: Option<Size>,
|
||||
render_debug: Option<RenderDebugLog>,
|
||||
}
|
||||
|
||||
@@ -59,6 +64,7 @@ impl<W: Write> ColorCompatBackend<W> {
|
||||
// to a community preset.
|
||||
active_ui_theme: UiTheme::detect(),
|
||||
forced_size: None,
|
||||
terminal_size: None,
|
||||
render_debug: RenderDebugLog::from_env(),
|
||||
}
|
||||
}
|
||||
@@ -71,6 +77,10 @@ impl<W: Write> ColorCompatBackend<W> {
|
||||
self.forced_size = None;
|
||||
}
|
||||
|
||||
pub(crate) fn set_terminal_size(&mut self, size: Size) {
|
||||
self.terminal_size = Some(size);
|
||||
}
|
||||
|
||||
pub(crate) fn set_palette_mode(&mut self, palette_mode: PaletteMode) {
|
||||
self.palette_mode = palette_mode;
|
||||
}
|
||||
@@ -152,10 +162,14 @@ impl<W: Write> Backend for ColorCompatBackend<W> {
|
||||
}
|
||||
|
||||
fn size(&self) -> io::Result<Size> {
|
||||
match self.forced_size {
|
||||
Some(size) => Ok(size),
|
||||
None => self.inner.size(),
|
||||
// forced_size takes priority: it is set during resize events to prevent
|
||||
// ratatui's autoresize from shrinking the viewport back to a stale
|
||||
// dimension. terminal_size is the cached real terminal size used as a
|
||||
// fallback after alt-screen re-entry (Windows buffer width workaround).
|
||||
if let Some(size) = self.forced_size.or(self.terminal_size) {
|
||||
return Ok(size);
|
||||
}
|
||||
self.inner.size()
|
||||
}
|
||||
|
||||
fn window_size(&mut self) -> io::Result<WindowSize> {
|
||||
@@ -496,4 +510,34 @@ mod tests {
|
||||
assert!(body.contains("diff_cells=1"), "{body}");
|
||||
assert!(body.contains("sample=3:4"), "{body}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn size_returns_terminal_size_when_set() {
|
||||
let writer = SharedWriter::default();
|
||||
let mut backend = ColorCompatBackend::new(writer, ColorDepth::TrueColor, PaletteMode::Dark);
|
||||
|
||||
backend.set_terminal_size(Size::new(120, 40));
|
||||
assert_eq!(backend.size().unwrap(), Size::new(120, 40));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn forced_size_takes_priority_over_terminal_size() {
|
||||
let writer = SharedWriter::default();
|
||||
let mut backend = ColorCompatBackend::new(writer, ColorDepth::TrueColor, PaletteMode::Dark);
|
||||
|
||||
// forced_size is set during resize events to temporarily override the
|
||||
// cached terminal_size — it must win to prevent viewport shrinking.
|
||||
backend.set_terminal_size(Size::new(120, 40));
|
||||
backend.force_size(Size::new(80, 25));
|
||||
assert_eq!(backend.size().unwrap(), Size::new(80, 25));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn size_falls_back_to_forced_size_when_terminal_size_unset() {
|
||||
let writer = SharedWriter::default();
|
||||
let mut backend = ColorCompatBackend::new(writer, ColorDepth::TrueColor, PaletteMode::Dark);
|
||||
|
||||
backend.force_size(Size::new(80, 25));
|
||||
assert_eq!(backend.size().unwrap(), Size::new(80, 25));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -113,7 +113,7 @@ pub(super) fn handle_subagent_mailbox(app: &mut App, seq: u64, message: &Mailbox
|
||||
{
|
||||
apply_to_fanout(card, message);
|
||||
app.subagent_card_index.insert(agent_id, idx);
|
||||
app.mark_history_updated();
|
||||
app.bump_history_cell(idx);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -129,7 +129,9 @@ pub(super) fn handle_subagent_mailbox(app: &mut App, seq: u64, message: &Mailbox
|
||||
_ => false,
|
||||
};
|
||||
if updated {
|
||||
app.mark_history_updated();
|
||||
// idx is already in scope from the outer
|
||||
// `if let Some(&idx) = app.subagent_card_index.get(&agent_id)`.
|
||||
app.bump_history_cell(idx);
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -168,13 +170,13 @@ pub(super) fn handle_subagent_mailbox(app: &mut App, seq: u64, message: &Mailbox
|
||||
let card = DelegateCard::new(agent_id.clone(), agent_type.clone());
|
||||
app.add_message(HistoryCell::SubAgent(SubAgentCell::Delegate(card)));
|
||||
let idx = app.history.len().saturating_sub(1);
|
||||
app.subagent_card_index.insert(agent_id, idx);
|
||||
app.subagent_card_index.insert(agent_id.clone(), idx);
|
||||
// Single delegate consumes the pending dispatch label so a follow-on
|
||||
// tool call doesn't accidentally inherit it.
|
||||
app.pending_subagent_dispatch = None;
|
||||
// idx was just inserted on the line above — no need to re-query.
|
||||
app.bump_history_cell(idx);
|
||||
}
|
||||
|
||||
app.mark_history_updated();
|
||||
}
|
||||
|
||||
pub(super) fn task_mode_label(mode: AppMode) -> &'static str {
|
||||
|
||||
@@ -2161,7 +2161,7 @@ async fn run_event_loop(
|
||||
subagent_elapsed,
|
||||
);
|
||||
}
|
||||
if should_recapture_terminal {
|
||||
if should_recapture_terminal && event_broker.is_paused() {
|
||||
resume_terminal(
|
||||
terminal,
|
||||
app.use_alt_screen,
|
||||
@@ -2694,7 +2694,9 @@ async fn run_event_loop(
|
||||
// this single draw so the buffer matches the real viewport.
|
||||
{
|
||||
let backend = terminal.backend_mut();
|
||||
backend.force_size(Size::new(final_w, final_h));
|
||||
let new_size = Size::new(final_w, final_h);
|
||||
backend.force_size(new_size);
|
||||
backend.set_terminal_size(new_size);
|
||||
}
|
||||
draw_app_frame_inner(terminal, app, true)?;
|
||||
draws_since_last_full_repaint = 0;
|
||||
@@ -7935,6 +7937,16 @@ fn resume_terminal(
|
||||
use_mouse_capture,
|
||||
use_bracketed_paste,
|
||||
);
|
||||
// Cache the real terminal size *before* resetting the viewport, so that
|
||||
// reset_terminal_viewport → terminal.clear() → autoresize() → backend.size()
|
||||
// picks up the cached size instead of falling through to
|
||||
// crossterm::terminal::size() which may return stale buffer metadata
|
||||
// (especially on Windows after a secondary EnterAlternateScreen).
|
||||
if let Ok((cols, rows)) = crossterm::terminal::size() {
|
||||
terminal
|
||||
.backend_mut()
|
||||
.set_terminal_size(Size::new(cols, rows));
|
||||
}
|
||||
reset_terminal_viewport(terminal, sync_output_enabled)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user