Skip to content

fix(tui): anchor offscreen expansion replay - #714

Open
devkade wants to merge 1 commit into
code-yeongyu:mainfrom
devkade:fix/701-render
Open

fix(tui): anchor offscreen expansion replay#714
devkade wants to merge 1 commit into
code-yeongyu:mainfrom
devkade:fix/701-render

Conversation

@devkade

@devkade devkade commented Aug 4, 2026

Copy link
Copy Markdown

Summary

Fix incomplete and mismatched tool output when expanding collapsed results with Ctrl+O.

When multiple completed tool calls were rendered in collapsed form, expanding them could leave some entries showing only headers or error summaries while another entry displayed only a trailing portion of its output.

This happens when an offscreen expansion changes the structure above the current viewport. The renderer's cached line state and the terminal's actual scrollback can then become inconsistent.

This PR:

  • anchors canonical transcript replay at the physical top-left of the terminal;
  • prevents replay from starting at a stale logical hardware-cursor row;
  • preserves the existing bounded viewport repaint path for multiplexer and Herdr sessions;
  • adds regression coverage for the affected replay behavior;
  • documents the remaining scrollback tradeoff and asks for a decision on the long-term policy.

Root cause

Ctrl+O changes the height of tool-rendering components that may already be above the current viewport.

Rows that have already entered terminal scrollback cannot be edited in place. The renderer therefore needs to either:

  1. replay the current canonical transcript; or
  2. repaint only the visible viewport and leave historical rows unchanged.

Previously, the replay path could begin from the renderer's calculated hardware-cursor position rather than the physical top of the screen. The replay could therefore be offset from the expected terminal position and produce mixed output such as:

  • a tool header without its body;
  • an error summary where the full result should be;
  • a trailing portion of a different tool result.

The renderer's internal state could appear correct even though the terminal contained mismatched rows.

What changed

The scrollback replay path now explicitly anchors at:

CSI 1;1H

before writing the canonical transcript.

This makes replay independent of the cursor position used by the previous differential-rendering frame.

Multiplexer and Herdr sessions continue to use bounded viewport repaint rather than canonical scrollback replay. Those environments own their pane history, so replaying the entire transcript would append duplicate content and corrupt the user's pane scrollback.

Known limitation

For regular, non-multiplexer terminals, canonical replay may clear the terminal scrollback before rewriting the transcript:

ESC[3J

As a result, expanding an offscreen tool result can remove content that the user had previously scrolled up to inspect. This may include:

  • earlier Senpi transcript rows in terminal history;
  • shell output that existed before Senpi started;
  • the user's previous native terminal scroll position.

A regular terminal does not provide a reliable way for the application to rewrite arbitrary historical rows or restore the previous native scroll position after the layout changes above the viewport.

This means there is a broader design tradeoff that should be decided explicitly.

Two possible policies for regular terminals

Option 1: Preserve native scrollback where possible

Senpi can avoid clearing the terminal scrollback and replay or repaint the current viewport without destructive history clearing.

Advantages:

  • less destructive to the user's terminal history;
  • less likely to remove shell output from before Senpi started;
  • less visible flicker in some cases.

Risks:

  • previously rendered collapsed content may remain in terminal scrollback;
  • stale and current versions of the transcript may coexist;
  • the logical transcript and native terminal scrollback can become inconsistent;
  • an expanded offscreen tool result may not be visible in the historical rows where the collapsed version was originally rendered.

Option 2: Follow Pi's full-clear and redraw approach

Pi handles this structural-rendering problem in regular/main-screen mode by clearing the terminal scrollback and redrawing the current canonical transcript.

Advantages:

  • the terminal contains one consistent version of the transcript;
  • expanded and collapsed content cannot remain mixed in native scrollback;
  • the renderer does not need to preserve stale historical copies;
  • the final terminal state matches the current logical transcript.

Risks:

  • content that the user had scrolled up to inspect is removed;
  • shell output from before Senpi started may also be cleared;
  • the previous native terminal scroll position cannot be restored.

Relevant Pi implementation and discussion:

Pi's rationale is that a regular terminal cannot set the native scroll position after content above the viewport changes. Clearing and rebuilding the transcript avoids leaving stale collapsed rows mixed with newly expanded content.

Question for review

Which behavior should Senpi prefer for regular terminals?

  1. Preserve as much native scrollback as possible, accepting that stale collapsed content may remain; or
  2. Follow Pi's approach and clear/rebuild the scrollback so the rendered transcript is always canonical and internally consistent?

My current understanding is:

  • Option 1 is less destructive to terminal history but can produce stale or mixed transcript history.
  • Option 2 provides stronger rendering correctness but removes content the user may have scrolled up to inspect.

This PR fixes the incorrect and mismatched expansion rendering without silently hiding that tradeoff. Reviewer guidance on the preferred long-term policy would be appreciated.

Multiplexer and Herdr behavior remains separate: those environments own pane scrollback and should continue using bounded viewport repaint rather than destructive replay.

Verification

  • npm --prefix packages/tui test
  • npm run check
  • Regression coverage for canonical replay anchoring
  • Real Senpi TUI smoke testing with multiple completed tool calls and Ctrl+O
  • Herdr/multiplexer smoke testing for bounded viewport repaint and pane-history preservation

Fixes #701


Summary by cubic

Anchors scrollback replay to the terminal’s top-left to fix mixed or truncated tool output when expanding offscreen results with Ctrl+O, while keeping multiplexer/Herdr on bounded viewport repaint. Includes regression tests; fixes #701.

  • Bug Fixes
    • Start replay at CSI 1;1H instead of a cached hardware-cursor row.
    • Reset stale scrollback only on regular terminals; preserve pane history in multiplexers/Herdr by skipping destructive replay.
    • Add tests that assert top-left anchoring and consistent transcript after collapse/expand.

Written for commit ae42a11. Summary will update on new commits.

Review in cubic

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Multiple tool results render incorrectly after expanding output with Ctrl+O

1 participant