Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 31 additions & 1 deletion app/src/terminal/view.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7331,6 +7331,11 @@ impl TerminalView {
input.set_input_mode_agent(true, ctx);
input.clear_buffer_and_reset_undo_stack(ctx);
});
// Tagging the agent in while an alt-screen TUI is streaming makes the
// input prompt visible, which shrinks the rendered alt-screen viewport.
// Force a synchronous size refresh so the PTY winsize and the alt-screen
// grid converge before the next paint (see issue #9365).
self.refresh_size_if_alt_screen_active(ctx);
ctx.notify();
}

Expand Down Expand Up @@ -7362,6 +7367,9 @@ impl TerminalView {
});
self.redetermine_terminal_focus(ctx);

// Tagging the agent out hides the input prompt, restoring the
// alt-screen viewport's full height. See `tag_agent_in` and #9365.
self.refresh_size_if_alt_screen_active(ctx);
ctx.notify();
}

Expand Down Expand Up @@ -15076,13 +15084,35 @@ impl TerminalView {
Appearance::as_ref(ctx)
}

fn refresh_size(&mut self, ctx: &mut ViewContext<Self>) {
pub(in crate::terminal) fn refresh_size(&mut self, ctx: &mut ViewContext<Self>) {
self.resize_internal(
SizeUpdateBuilder::for_refresh(*self.size_info).build(self, ctx),
ctx,
)
}

/// When the input prompt's visibility flips while an alt-screen TUI is
/// active, the rendered output area changes size and the embedded
/// `TerminalSizeElement` will naturally fire a resize through
/// `resize_tx`. However, that resize happens asynchronously after the
/// next layout pass, leaving a window in which the alt-screen grid still
/// holds rows from the previous (larger) layout. Heavy-streaming alt-
/// screen apps such as Claude Code can paint into those stale rows
/// before they process SIGWINCH, producing the visual symptom of the
/// input prompt rendering "mid-output" with lines clipped above it
/// (see issue #9365).
///
/// This helper issues a synchronous refresh of the model size so the PTY
/// winsize and alt-screen grid converge before the next paint.
pub(in crate::terminal) fn refresh_size_if_alt_screen_active(
&mut self,
ctx: &mut ViewContext<Self>,
) {
if self.model.lock().is_alt_screen_active() {
self.refresh_size(ctx);
}
}

fn resize_internal(&mut self, size_update: SizeUpdate, ctx: &mut ViewContext<Self>) {
// Viewer-driven sizing: report the viewer's natural size to the sharer.
// This runs before the early-return so the initial report on viewer join
Expand Down
10 changes: 10 additions & 0 deletions app/src/terminal/view/use_agent_footer/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -611,6 +611,11 @@ impl TerminalView {
}

self.redetermine_terminal_focus(ctx);
// Closing the rich input restores the alt-screen viewport's full
// height when an alt-screen TUI is streaming (e.g. Claude Code).
// Force a synchronous size refresh so the PTY winsize and alt-screen
// grid converge before the next paint (see issue #9365).
self.refresh_size_if_alt_screen_active(ctx);
ctx.notify();
}

Expand Down Expand Up @@ -1048,6 +1053,11 @@ impl TerminalView {
// Input mode switch, buffer clear, draft restoration, and hint text
// are handled reactively by Input's subscription to InputSessionChanged.
self.redetermine_terminal_focus(ctx);
// Opening the rich input shrinks the rendered alt-screen viewport
// when an alt-screen TUI is streaming (e.g. Claude Code). Force a
// synchronous size refresh so the PTY winsize and the alt-screen
// grid converge before the next paint (see issue #9365).
self.refresh_size_if_alt_screen_active(ctx);
ctx.notify();
}
}
Expand Down
56 changes: 56 additions & 0 deletions app/src/terminal/view_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5645,3 +5645,59 @@ fn linear_deeplink_via_default_entrypoint_does_not_auto_submit_in_fullscreen() {
});
})
}

/// Regression test for #9365.
///
/// When the input prompt's visibility flips while an alt-screen TUI is
/// streaming (e.g., the agent is tagged in mid-stream), the rendered
/// alt-screen viewport shrinks. The PTY winsize must be refreshed so the
/// alt-screen grid and the TUI agree on the visible row count, preventing
/// the input prompt from appearing to render "mid-output" and stale rows
/// from being clipped.
///
/// This test verifies that `refresh_size_if_alt_screen_active` is a no-op
/// when the terminal is in blocklist mode, and that it issues a Refresh
/// SizeUpdate (observable via the emitted `Event::Resize`) when the alt
/// screen is active.
#[test]
fn refresh_size_if_alt_screen_active_only_refreshes_in_alt_screen() {
App::test((), |mut app| async move {
initialize_app_for_terminal_view(&mut app);

let (_window_id, terminal) = add_window_with_id_and_terminal(&mut app, None);

// In blocklist mode, the helper should be a no-op. We sample the
// current SizeInfo and expect it to be unchanged afterwards (no
// panic, no resize emission required).
let blocklist_size = terminal.update(&mut app, |view, ctx| {
assert!(!view.model.lock().is_alt_screen_active());
let before = *view.size_info();
view.refresh_size_if_alt_screen_active(ctx);
let after = *view.size_info();
assert_eq!(
before.pane_size_px(),
after.pane_size_px(),
"no-op in blocklist mode"
);
after
});

// Enter alt-screen and call the helper. The helper should perform a
// Refresh-style resize via `refresh_size`, which propagates the
// current `size_info` to the model. The pane size doesn't change,
// but the helper must execute without panicking and leave `size_info`
// identical to what it was before.
terminal.update(&mut app, |view, ctx| {
view.model.lock().set_mode(ansi::Mode::SwapScreen {
save_cursor_and_clear_screen: true,
});
assert!(view.model.lock().is_alt_screen_active());
view.refresh_size_if_alt_screen_active(ctx);
assert_eq!(
view.size_info().pane_size_px(),
blocklist_size.pane_size_px(),
"refresh preserves the existing pane size"
);
});
})
}