-
Notifications
You must be signed in to change notification settings - Fork 395
fix(pi-tui): make the viewport anchor follow above-viewport content shifts #1364
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
liruifengv
wants to merge
1
commit into
main
Choose a base branch
from
fix/pi-tui-above-viewport-shift
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| --- | ||
| "@moonshot-ai/kimi-code": patch | ||
| --- | ||
|
|
||
| Fix transcript rows vanishing and content creeping upward during streaming, which left a growing blank area under the input box. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| --- | ||
| "@moonshot-ai/pi-tui": patch | ||
| --- | ||
|
|
||
| Make the viewport anchor follow content when lines above the viewport are added or removed, instead of pinning a buffer row index that let the visible window drift and lose rows. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,96 @@ | ||
| import assert from "node:assert"; | ||
| import { describe, it } from "node:test"; | ||
| import { countInBuffer, createHarness } from "./harness.ts"; | ||
|
|
||
| // Case 07 — rows vanish and content creeps upward while streaming. | ||
| // | ||
| // Symptom: during a long streaming turn, visible transcript rows kept | ||
| // disappearing and the remaining content crept upward, leaving an | ||
| // ever-growing blank area under the input box. Scrolling back showed | ||
| // gaps: the vanished rows were in neither the screen nor scrollback. | ||
| // | ||
| // Root cause: the viewport anchor pins a *buffer row index*, not | ||
| // content. When content ABOVE the viewport shrinks by k lines (a | ||
| // finished agent-group row collapsing, a merged step, a spinner line | ||
| // disappearing), every line below the shrink point shifts up by k, so | ||
| // the pinned window suddenly shows content k lines further down: the | ||
| // screen appears to jump up, and the k rows that slid above the window | ||
| // top are lost — scrollback at those indices holds older bytes, and the | ||
| // rows were never committed. Each above-viewport net shrink permanently | ||
| // swallows k rows. | ||
| // | ||
| // The fix makes the anchor follow content instead of row indices: when | ||
| // the frame is best explained as an above-viewport shift, the anchor | ||
| // moves with the shift (screen content stays put), and later growth | ||
| // commits rows in content order — no loss, no duplication. | ||
| // | ||
| // Fixed: the anchor follows the shift, so the screen stays put and later | ||
| // growth commits rows in content order. | ||
|
|
||
| describe("e2e case07: above-viewport shrink must not shift or lose rows", () => { | ||
| it("keeps the visible window unchanged when lines above it are removed", async () => { | ||
| // 60 lines at height 10 -> anchor 50; screen shows row-50..row-58 + input. | ||
| const initial = [...Array.from({ length: 59 }, (_, i) => `row-${i}`), "[INPUT-BOX]"]; | ||
| const h = await createHarness(initial, { rows: 10 }); | ||
| const before = h.terminal.getViewport(); | ||
| assert.ok(before[0]!.includes("row-50"), "sanity: window starts at row-50"); | ||
|
|
||
| // Remove 3 lines far above the viewport (row-10..row-12); everything | ||
| // below shifts up by 3, but the visible content is unchanged. | ||
| const shrunk = initial.filter((_, i) => i < 10 || i > 12); | ||
| await h.frame(shrunk); | ||
|
|
||
| assert.deepStrictEqual( | ||
| h.terminal.getViewport(), | ||
| before, | ||
| "the visible window must not move when only above-viewport lines are removed", | ||
| ); | ||
|
|
||
| // Growth afterwards must commit rows in content order: nothing lost, | ||
| // nothing duplicated. | ||
| const grown = [...shrunk.slice(0, -1), "tail-0", "tail-1", "tail-2", "[INPUT-BOX]"]; | ||
| await h.frame(grown); | ||
|
|
||
| const buffer = h.terminal.getScrollBuffer(); | ||
| for (const marker of ["row-49", "row-50", "row-51", "row-52", "row-53"]) { | ||
| const count = countInBuffer(buffer, marker); | ||
| assert.strictEqual(count, 1, `"${marker}" must appear exactly once, got ${count}`); | ||
| } | ||
| const viewport = h.terminal.getViewport(); | ||
| assert.ok(viewport.some((line) => line.includes("tail-2")), "appended rows must be visible"); | ||
| assert.ok(viewport[9]!.includes("[INPUT-BOX]"), "input box must sit on the bottom row"); | ||
|
|
||
| h.stop(); | ||
| }); | ||
|
|
||
| it("applies the shift even when a live row inside the window also changed", async () => { | ||
| // Same shape, but the window contains a status line that changes in | ||
| // the same frame as the above-viewport shrink (spinner/timer tick). | ||
| const content = (status: string): string[] => [ | ||
| ...Array.from({ length: 58 }, (_, i) => `row-${i}`), | ||
| status, | ||
| "[INPUT-BOX]", | ||
| ]; | ||
| const h = await createHarness(content("status-A"), { rows: 10 }); | ||
| const before = h.terminal.getViewport(); | ||
| assert.ok(before.some((line) => line.includes("status-A")), "sanity: status visible"); | ||
|
|
||
| // Remove 3 above-viewport lines AND tick the status line. | ||
| const shrunk = content("status-B").filter((_, i) => i < 10 || i > 12); | ||
| await h.frame(shrunk); | ||
|
|
||
| const after = h.terminal.getViewport(); | ||
| assert.ok(after.some((line) => line.includes("status-B")), "status line must show the new value"); | ||
| // Every other visible row stays put (no upward creep). | ||
| for (let r = 0; r < 10; r++) { | ||
| if (before[r]!.includes("status-A")) continue; | ||
| assert.strictEqual( | ||
| after[r], | ||
| before[r], | ||
| `row ${r} must not move on an above-viewport shrink`, | ||
| ); | ||
| } | ||
|
|
||
| h.stop(); | ||
| }); | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When a single streaming frame both removes rows above the viewport and appends the same number of rows near the tail, the total
lengthDeltais 0, so this guard skips the new shift-following path entirely. The later clamped diff then repaints the shifted tail in place without scrolling the old top viewport rows into scrollback, recreating the row-loss bug for frames where a status/activity block collapses while new output arrives. The shift amount needs to be derived from the above-viewport edit/alignment, not only from the net buffer length.Useful? React with 👍 / 👎.