Skip to content

Enhance alt-screen scrollback handling and rendering logic#9377

Open
JivinSardine wants to merge 8 commits intowarpdotdev:masterfrom
JivinSardine:master
Open

Enhance alt-screen scrollback handling and rendering logic#9377
JivinSardine wants to merge 8 commits intowarpdotdev:masterfrom
JivinSardine:master

Conversation

@JivinSardine
Copy link
Copy Markdown

Summary
Fixes a rendering bug where Claude Code's TUI prompt box (> line + status bar) appears mid-scrollback during heavy streaming output, causing visual content to be overlapped or clipped.
Root Cause
The alt-screen grid was initialized with max_scroll_limit = 0, meaning any content that scrolled off the visible alt-screen was permanently discarded. When streaming TUI apps like Claude Code use cursor-up overshoot sequences (Ink framework's full-buffer redraw pattern), the cursor references positions in content that Warp's alt-screen had already dropped. This caused the prompt to render at an incorrect position in the scrollback.
Changes

  • terminal_model.rs: Changed AltScreen max_scroll_limit from 0 → 1000 so scrolled-off content is preserved in GridStorage's internal history
  • alt_screen_element.rs (paint): Rendering now starts at history_offset (history size) instead of absolute index 0, anchoring visible content to the bottom of the buffer
  • alt_screen_element.rs (coord_to_point): Pixel-to-row conversion accounts for history_offset so mouse hit-detection maps to the correct visible rows
    Verification
  • Scrollback is bounded at 1000 lines via GridStorage::increase_scroll_limit's existing cap
  • flat_storage remains unused for the alt-screen (verified via existing debug assertion + resize fast-path)
  • Resize operations correctly preserve/truncate history through existing GridStorage methods
  • Edge case: with no scrolling, history_offset = 0 and rendering is identical to before
    Affected issues
    Fixes Flickering scroll when using Ink-based CLI (Claude Code, Gemini) #7200

Account for alt-screen scrollback when mapping coords and rendering. coord_to_point now computes a history_offset (total_rows - visible_rows) and anchors visible rows to the bottom of the buffer, and painting uses history_offset when computing start_row/end_row so the visible content aligns with preserved history. Also set AltScreen's default max_scroll_limit from 0 to 1000 to retain alt-screen scrollback and avoid content loss during heavy streaming (e.g. apps relying on cursor-up overshoot / full-buffer redraws like Claude Code). Includes comments and bounds checks to clamp row calculations to visible/total row limits.
Copilot AI review requested due to automatic review settings April 29, 2026 07:08
@cla-bot
Copy link
Copy Markdown

cla-bot Bot commented Apr 29, 2026

Thank you for your pull request and welcome to our community. We require contributors to sign our Contributor License Agreement, and we don't seem to have the users @JivinSardine on file. In order for us to review and merge your code, each contributor must visit https://cla.warp.dev to read and agree to our CLA. Once you have done so, please comment @cla-bot check to trigger another check.

@oz-for-oss
Copy link
Copy Markdown
Contributor

oz-for-oss Bot commented Apr 29, 2026

@JivinSardine

I'm starting a first review of this pull request.

I requested changes on this pull request and posted feedback.

Comment /oz-review on this pull request to retrigger a review (up to 3 times on the same pull request).

I'm re-reviewing this pull request in response to a review request.

You can view the conversation on Warp.

I requested changes on this pull request and posted feedback.

Comment /oz-review on this pull request to retrigger a review (up to 3 times on the same pull request).

Powered by Oz

Copy link
Copy Markdown
Contributor

@oz-for-oss oz-for-oss Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overview

This PR attempts to preserve alt-screen history and render from the bottom of the buffer so cursor-up redraw patterns do not lose content.

Concerns

  • The new nonzero alt-screen scroll limit stores rows in GridStorage, but the GridHandler APIs used by rendering and hit-testing still derive history from flat_storage, which remains empty for alt-screen grids. This means the new history_offset remains zero and row(0..visible) can read hidden grid-storage history rather than the current visible screen after scrolling.
  • If history_offset is made nonzero, the paint origin must also be offset by that history amount because render_grid positions cells with absolute offset_row values.

Verdict

Found: 0 critical, 2 important, 0 suggestions

Request changes

Comment /oz-review on this pull request to retrigger a review (up to 3 times on the same pull request).

Powered by Oz

let alt_screen = AltScreen::new(
sizes.size,
0, /* max_scroll_limit */
1000, /* max_scroll_limit — preserve alt-screen scrollback to prevent content loss
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ [IMPORTANT] This only grows GridStorage history, but GridHandler still reports history via flat_storage (which alt-screen keeps empty), so history_offset stays 0 and row(0..visible) reads hidden GridStorage history instead of the visible rows once scrolling occurs. Expose GridStorage history through GridHandler for alt-screen, or render/index via GridStorage absolute rows before enabling this.

.as_f64())
.min(grid.visible_rows() as f64);
.min(grid.total_rows() as f64);
let adjusted_grid_origin = origin - self.vertical_scroll_pixels();
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ [IMPORTANT] If history_offset becomes non-zero, this origin only subtracts self.scroll_top while render_grid places cells at offset_row = start_row + offset, so the first visible row renders history_offset rows below the element. Subtract the full start_row from the origin.

Suggested change
let adjusted_grid_origin = origin - self.vertical_scroll_pixels();
let adjusted_grid_origin =
origin - vec2f(0., start_row as f32 * self.line_height().as_f32());

@JivinSardine
Copy link
Copy Markdown
Author

@cla-bot check

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Updates Warp’s alt-screen handling to retain a bounded scrollback history and adjusts rendering / hit-testing so the visible alt-screen content is anchored to the bottom of the (history + visible) buffer, preventing mis-rendering during heavy streaming output (e.g., cursor-up overshoot redraw patterns).

Changes:

  • Set alt-screen max_scroll_limit to 1000 (was 0) to preserve a bounded amount of alt-screen scrollback.
  • Render alt-screen starting from history_offset (history size) and adjust end_row bounds to use total_rows.
  • Update pixel→grid coordinate mapping to account for the history offset.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

File Description
app/src/terminal/model/terminal_model.rs Enables bounded alt-screen scrollback by initializing AltScreen with a non-zero scroll limit.
app/src/terminal/alt_screen/alt_screen_element.rs Anchors rendering to history offset and updates coordinate mapping for correct row targeting.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +517 to +520
let row = history_offset
+ (coord.y() / size.cell_height_px().as_f32())
.max(0.)
.min(visible_height.saturating_sub(1) as f32) as usize;
Copy link

Copilot AI Apr 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

coord_to_point clamps the computed row using visible_height, but coord has already been adjusted by vertical_scroll_pixels (see to_local), meaning coord.y() includes scroll_top. When scroll_top > 0 (e.g. shared-session viewer vertical scrolling), this clamp will cap rows to the first visible_height lines and map mouse hit-testing/selection to the wrong grid rows. Clamp against the grid’s total row range (or against scroll_top + visible_height) so the computed row stays consistent with the start_row = history_offset + scroll_top rendering logic.

Suggested change
let row = history_offset
+ (coord.y() / size.cell_height_px().as_f32())
.max(0.)
.min(visible_height.saturating_sub(1) as f32) as usize;
let max_visible_row = self
.scroll_top
.saturating_add(visible_height.saturating_sub(1));
let row = history_offset
+ (coord.y() / size.cell_height_px().as_f32())
.max(0.)
.min(max_visible_row as f32) as usize;

Copilot uses AI. Check for mistakes.
Comment on lines +741 to +753
// Render grid cells. The alt screen preserves scrollback for apps that
// rely on cursor-up overshoot and full-buffer redraws (e.g. Claude Code).
// Start rendering from the history offset so visible content anchors to
// the bottom of the buffer rather than the top.
record_trace_event!("alt_screen_element:paint:preparing_to_render_grid");
let start_row = self.scroll_top.as_f64();
let history_offset = (grid.total_rows().saturating_sub(grid.visible_rows())) as f64;
let start_row = history_offset + self.scroll_top.as_f64();
let end_row = (start_row
+ self
.visible_lines
.expect("should be set after layout")
.as_f64())
.min(grid.visible_rows() as f64);
.min(grid.total_rows() as f64);
Copy link

Copilot AI Apr 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new history_offset rendering path is a regression-prone behavior change (rendering from history_offset and changing end_row bounds). There are existing alt-screen interaction tests in app/src/terminal/view_test.rs, but none appear to cover an alt-screen with non-zero history + verifying paint/mouse mapping stays correct. Adding a regression test that scrolls the alt-screen enough to create history, then asserts selection/mouse coordinates target the expected rows, would help prevent future breakage.

Copilot uses AI. Check for mistakes.
@cla-bot cla-bot Bot added the cla-signed label Apr 29, 2026
@cla-bot
Copy link
Copy Markdown

cla-bot Bot commented Apr 29, 2026

The cla-bot has been summoned, and re-checked this pull request!

GridHandler::history_size() derives from flat_storage (always empty
for alt-screen), so history_offset was 0 despite GridStorage preserving
scrollback rows internally via max_scroll_limit. This caused visible
rows to render at wrong positions and mouse coordinates to map
incorrectly.

- Use GridStorage::total_rows/visible_rows directly for history_offset
- Offset paint origin by history_offset so cells land at correct screen y
- Fix coord_to_point clamp to account for scroll_top baked into coord.y
- Bump grid_storage() visibility from pub(super) to pub(crate)
@JivinSardine
Copy link
Copy Markdown
Author

@oz-agent check code again

@oz-for-oss
Copy link
Copy Markdown
Contributor

oz-for-oss Bot commented Apr 29, 2026

@JivinSardine

I'm working on changes requested in this PR (responding to a PR conversation comment).

I analyzed the request but did not produce any changes.

You can view the conversation on Warp.

Powered by Oz

@JivinSardine
Copy link
Copy Markdown
Author

@oz-agent check the changes

Copy link
Copy Markdown
Contributor

@oz-for-oss oz-for-oss Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overview

This PR enables bounded alt-screen scrollback and updates rendering/mouse coordinate conversion to account for GridStorage history.

Concerns

  • Rendering is not wired through a row source that includes GridStorage history; render_grid still asks GridHandler::row for history_offset-adjusted rows, which are out of bounds according to GridHandler::total_rows().
  • The coordinate change feeds history-adjusted rows into terminal mouse reporting, so mouse-enabled TUIs receive invalid row numbers after scrollback accumulates.
  • Cursor rendering still uses GridHandler cursor points without the storage-history offset, so the cursor/cursor cell state is no longer aligned with the rendered cells.
  • Security pass: no security-specific issues found.

Verdict

Found: 1 critical, 2 important, 0 suggestions

Request changes

Comment /oz-review on this pull request to retrigger a review (up to 3 times on the same pull request).

Powered by Oz

// the bottom of the buffer rather than the top.
record_trace_event!("alt_screen_element:paint:preparing_to_render_grid");
let start_row = self.scroll_top.as_f64();
let start_row = history_offset as f64 + self.scroll_top.as_f64();
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚨 [CRITICAL] render_grid still reads rows through GridHandler::row, whose total_rows() ignores GridStorage history for alt-screen, so adding history_offset here makes the renderer request out-of-bounds rows once scrollback exists; the bottom row is skipped immediately and the alt-screen renders blank once history_offset >= visible_rows.

// coord.y already includes scroll_top via to_local(), so the clamp
// upper bound must account for it.
let max_row = (self.scroll_top.as_f64() as usize) + visible_height.saturating_sub(1);
let row = history_offset
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ [IMPORTANT] This point is also passed to AltMouseAction, and MouseState::to_escape_sequence serializes point.row + 1 to the TUI, so adding history_offset sends mouse rows larger than the alt-screen height after scrollback accumulates.

// Offset the paint origin upward by the scrollback height so that
// render_grid's absolute offset_row positioning maps cells to the
// correct screen positions (the first visible row lands at origin.y).
let adjusted_grid_origin =
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ [IMPORTANT] The same adjusted_grid_origin is used by render_cursor below, but grid.cursor_render_point() still returns a point based on GridHandler::history_size() (flat storage, 0 for alt-screen), so the cursor and cursor-cell styling are shifted above the viewport when history_offset > 0.

Co-Authored-By: Jivin Sardine M <89859763+JivinSardine@users.noreply.github.com>
@captainsafia captainsafia added the external-contributor Indicates that a PR has been opened by someone outside the Warp team. label Apr 30, 2026 — with Warp Dev Github Integration
@JivinSardine JivinSardine reopened this May 4, 2026
@oz-for-oss
Copy link
Copy Markdown
Contributor

oz-for-oss Bot commented May 4, 2026

@JivinSardine

I'm starting a first review of this pull request.

You can view the conversation on Warp.

I completed the review and no human review was requested for this pull request.

Comment /oz-review on this pull request to retrigger a review (up to 3 times on the same pull request).

Powered by Oz

Copy link
Copy Markdown
Contributor

@oz-for-oss oz-for-oss Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overview

This PR preserves bounded alt-screen scrollback in GridStorage and offsets alt-screen rendering and mouse coordinate conversion so streaming TUIs that redraw with cursor-up overshoot do not lose rows.

Concerns

  • num_lines_truncated() now adds GridStorage truncation for every grid. Normal grids intentionally keep GridStorage at a zero scroll limit while moving scrolled rows into flat_storage, so this makes ordinary scrollback look truncated even when it was preserved.
  • This is a user-visible terminal rendering change, but the PR description does not include screenshots or video. For faster review, please upload screenshots or a video of the rendering behavior working end to end.

Verdict

Found: 0 critical, 2 important, 0 suggestions

Request changes

Comment /oz-review on this pull request to retrigger a review (up to 3 times on the same pull request).

Powered by Oz

/// grid's maximum scrollback limit.
pub fn num_lines_truncated(&self) -> u64 {
self.flat_storage.num_truncated_rows()
self.flat_storage.num_truncated_rows() + self.grid.num_lines_truncated
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ [IMPORTANT] Non-alt grids set GridStorage's scroll limit to 0 while preserving scrolled rows in flat_storage, so self.grid.num_lines_truncated increments for every normal scroll; adding it here makes untruncated block rows look truncated. Only include grid truncation for alt-screen grids or storage modes where grid history owns scrollback.

…sible rows

Now that GridHandler::history_size() includes GridStorage history,
switch alt-screen rendering and hit-testing from GridStorage back
to the canonical GridHandler API. Also convert absolute grid rows
to visible screen coordinates for AltMouseAction escape sequences,
preventing mouse-enabled TUIs from receiving history-offset row
numbers that exceed the visible alt-screen height.
@JivinSardine
Copy link
Copy Markdown
Author

/oz-review

@oz-for-oss
Copy link
Copy Markdown
Contributor

oz-for-oss Bot commented May 4, 2026

@JivinSardine

I'm re-reviewing this pull request in response to a review request.

You can view the conversation on Warp.

I completed the review and no human review was requested for this pull request.

Comment /oz-review on this pull request to retrigger a review (up to 3 times on the same pull request).

Powered by Oz

Copy link
Copy Markdown
Contributor

@oz-for-oss oz-for-oss Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overview

This PR preserves bounded GridStorage history for the alt screen and adjusts rendering/mouse-coordinate handling so the visible viewport is anchored below that preserved history.

Concerns

  • The internal alt-screen history is now part of the shared GridHandler dimensions, so existing alt-screen find/output APIs can include hidden rows that paint deliberately excludes.
  • This is a user-visible terminal rendering change, but the PR description does not include screenshots or a video. For faster review, please upload screenshots or a video of the feature working end to end.

Verdict

Found: 0 critical, 2 important, 0 suggestions

Request changes

Comment /oz-review on this pull request to retrigger a review (up to 3 times on the same pull request).

Powered by Oz

#[inline]
fn history_size(&self) -> usize {
self.flat_storage.total_rows()
self.flat_storage.total_rows() + self.grid.history_size()
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ [IMPORTANT] This makes hidden alt-screen history visible to every GridHandler consumer of history_size()/total_rows(), including AltScreen::find() and output_to_string(), even though paint deliberately starts at history_offset; keep the preserved rows out of user-facing/search/output APIs or add an alt-screen-specific renderable-row accessor.

@JivinSardine
Copy link
Copy Markdown
Author

/oz-review

@oz-for-oss
Copy link
Copy Markdown
Contributor

oz-for-oss Bot commented May 4, 2026

@JivinSardine

I'm re-reviewing this pull request in response to a review request.

You can view the conversation on Warp.

I completed the review and no human review was requested for this pull request.

Comment /oz-review on this pull request to retrigger a review (up to 3 times on the same pull request).

Powered by Oz

Copy link
Copy Markdown
Contributor

@oz-for-oss oz-for-oss Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overview

This PR attempts to preserve bounded alt-screen scrollback in GridStorage and adjust rendering plus mouse coordinate mapping around the hidden history rows.

Concerns

  • The new offset plumbing reads GridHandler::history_size(), which still represents only flat-storage history in the displayed diff. Because alt-screen flat storage is now capped at 0, the offset remains 0, so rendering and hit detection do not account for the preserved GridStorage history, and the added grid_handler.history_size() test assertion should fail.

Verdict

Found: 0 critical, 1 important, 0 suggestions

Request changes

Comment /oz-review on this pull request to retrigger a review (up to 3 times on the same pull request).

Powered by Oz


/// Returns the grid's scrollback size (history rows above the visible viewport).
fn grid_history_offset(&self) -> usize {
self.model.lock().alt_screen().grid_handler().history_size()
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ [IMPORTANT] GridHandler::history_size() still reports flat-storage rows, but alt-screen scrollback is now stored in GridStorage, so this returns 0 and the render/hit-test offsets never account for preserved history. Use grid_storage().history_size() here or update the GridHandler dimension implementation for alt-screen grids.

The previous commit inadvertently reverted the Oz bot's fix that added
self.grid.history_size() to the GridHandler history calculation. Without
this, GridHandler::total_rows() excludes GridStorage's internal scrollback,
causing storage_row() bounds checks to reject valid history-offset rows
and making history_offset always 0 for alt-screen rendering.
@JivinSardine
Copy link
Copy Markdown
Author

/oz-review

@oz-for-oss
Copy link
Copy Markdown
Contributor

oz-for-oss Bot commented May 4, 2026

@JivinSardine

I'm re-reviewing this pull request in response to a review request.

You can view the conversation on Warp.

I completed the review and no human review was requested for this pull request.

Comment /oz-review on this pull request to retrigger a review (up to 3 times on the same pull request).

Powered by Oz

@JivinSardine
Copy link
Copy Markdown
Author

/oz-review

@oz-for-oss
Copy link
Copy Markdown
Contributor

oz-for-oss Bot commented May 4, 2026

@JivinSardine

I'm re-reviewing this pull request in response to a review request.

You can view the conversation on Warp.

I completed the review and no human review was requested for this pull request.

Comment /oz-review on this pull request to retrigger a review (up to 3 times on the same pull request).

Powered by Oz

Copy link
Copy Markdown
Contributor

@oz-for-oss oz-for-oss Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overview

This PR moves alt-screen scrollback into GridStorage and adjusts alt-screen rendering and mouse coordinate mapping around the preserved history offset.

Concerns

  • num_lines_truncated() now double-counts regular grid scrolls because non-alt grids intentionally keep GridStorage scrollback disabled while preserving scrolled rows in flat_storage.

Verdict

Found: 0 critical, 1 important, 0 suggestions

Request changes

Comment /oz-review on this pull request to retrigger a review (up to 3 times on the same pull request).

Powered by Oz

Comment thread app/src/terminal/model/grid/grid_handler.rs Outdated
Co-authored-by: oz-for-oss[bot] <277970191+oz-for-oss[bot]@users.noreply.github.com>
Copy link
Copy Markdown
Contributor

@oz-for-oss oz-for-oss Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overview

This PR preserves bounded alt-screen history in grid storage and updates rendering/mouse-coordinate paths to anchor the visible viewport below that history.

Concerns

  • Mouse hit-testing now truncates fractional shared-session scroll offsets when computing the visible row clamp, which can map clicks on partially visible bottom rows to the wrong grid row.

Verdict

Found: 0 critical, 1 important, 0 suggestions

Request changes

Comment /oz-review on this pull request to retrigger a review (up to 3 times on the same pull request).

Powered by Oz

.as_f64() as usize;
// coord.y already includes scroll_top via to_local(), so the clamp
// upper bound must account for it.
let max_row = (self.scroll_top.as_f64() as usize) + visible_height.saturating_sub(1);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ [IMPORTANT] scroll_top can be fractional for precise shared-session scrolling, but casting it to usize truncates the visible upper bound; the bottom partially visible row can map to the row above. Compute the clamp using the fractional scroll offset, such as ceil(scroll_top + visible_height) - 1, before converting to an integer.

@JivinSardine
Copy link
Copy Markdown
Author

/oz-review

@oz-for-oss
Copy link
Copy Markdown
Contributor

oz-for-oss Bot commented May 4, 2026

@JivinSardine

I'm re-reviewing this pull request in response to a review request.

You can view the conversation on Warp.

I completed the review and no human review was requested for this pull request.

Comment /oz-review on this pull request to retrigger a review (up to 3 times on the same pull request).

Powered by Oz

Copy link
Copy Markdown
Contributor

@oz-for-oss oz-for-oss Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overview

This PR preserves bounded alt-screen history in GridStorage and adjusts rendering/mouse coordinates so visible rows are anchored after the new history offset.

Concerns

  • This is a user-visible terminal rendering change, but the PR description does not include screenshots or video evidence. For faster review, please upload screenshots or a video of the bug before/after or the new behavior working end to end.

Verdict

Found: 0 critical, 1 important, 0 suggestions

Request changes

Comment /oz-review on this pull request to retrigger a review (up to 3 times on the same pull request).

Powered by Oz

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

cla-signed external-contributor Indicates that a PR has been opened by someone outside the Warp team.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Flickering scroll when using Ink-based CLI (Claude Code, Gemini)

4 participants