From 34f8eb70f09e34c8d451b5eedbfad7c018cb42de Mon Sep 17 00:00:00 2001 From: can1357 Date: Thu, 16 Jul 2026 03:22:54 +0200 Subject: [PATCH] fix(tui): honor ends during plan review reflow --- .../modes/components/plan-review-overlay.ts | 4 +- .../components/plan-review-overlay.test.ts | 55 +++++++++++++++++++ 2 files changed, 58 insertions(+), 1 deletion(-) diff --git a/packages/coding-agent/src/modes/components/plan-review-overlay.ts b/packages/coding-agent/src/modes/components/plan-review-overlay.ts index b66da9c6e..05e1a9a09 100644 --- a/packages/coding-agent/src/modes/components/plan-review-overlay.ts +++ b/packages/coding-agent/src/modes/components/plan-review-overlay.ts @@ -477,7 +477,9 @@ export class PlanReviewOverlay implements Component { */ #handleBodyScroll(data: string): void { if (this.#scrollView.handleScrollKey(data)) { - this.#captureScrollProgress(); + if (matchesKey(data, "home")) this.#scrollProgress = 0; + else if (matchesKey(data, "end")) this.#scrollProgress = 1; + else this.#captureScrollProgress(); return; } if (data === "g") { diff --git a/packages/coding-agent/test/modes/components/plan-review-overlay.test.ts b/packages/coding-agent/test/modes/components/plan-review-overlay.test.ts index 8240736ad..34a80a9bf 100644 --- a/packages/coding-agent/test/modes/components/plan-review-overlay.test.ts +++ b/packages/coding-agent/test/modes/components/plan-review-overlay.test.ts @@ -212,6 +212,61 @@ describe("PlanReviewOverlay", () => { } }); + it("honors Home while a transient render is non-scrollable", () => { + const originalRows = Object.getOwnPropertyDescriptor(process.stdout, "rows"); + const setRows = (rows: number): void => { + Object.defineProperty(process.stdout, "rows", { configurable: true, value: rows }); + }; + const codeRows = Array.from({ length: 400 }, (_, i) => `L${String(i).padStart(3, "0")}`).join("\n"); + const overlay = new PlanReviewOverlay( + `# Plan\n\n\`\`\`\n${codeRows}\n\`\`\`\n`, + { promptTitle: "next", options: APPROVAL_OPTIONS }, + { onPick: vi.fn(), onCancel: vi.fn() }, + ); + + try { + setRows(40); + render(overlay); + overlay.handleInput("G"); + setRows(1000); + render(overlay); + overlay.handleInput("\x1b[H"); + setRows(40); + const restored = render(overlay); + expect(restored).toContain("L000"); + expect(restored).not.toContain("L399"); + } finally { + if (originalRows) Object.defineProperty(process.stdout, "rows", originalRows); + else Reflect.deleteProperty(process.stdout, "rows"); + } + }); + + it("honors End while a transient render is non-scrollable", () => { + const originalRows = Object.getOwnPropertyDescriptor(process.stdout, "rows"); + const setRows = (rows: number): void => { + Object.defineProperty(process.stdout, "rows", { configurable: true, value: rows }); + }; + const codeRows = Array.from({ length: 400 }, (_, i) => `L${String(i).padStart(3, "0")}`).join("\n"); + const overlay = new PlanReviewOverlay( + `# Plan\n\n\`\`\`\n${codeRows}\n\`\`\`\n`, + { promptTitle: "next", options: APPROVAL_OPTIONS }, + { onPick: vi.fn(), onCancel: vi.fn() }, + ); + + try { + setRows(1000); + render(overlay); + overlay.handleInput("\x1b[F"); + setRows(40); + const restored = render(overlay); + expect(restored).toContain("L399"); + expect(restored).not.toContain("L000"); + } finally { + if (originalRows) Object.defineProperty(process.stdout, "rows", originalRows); + else Reflect.deleteProperty(process.stdout, "rows"); + } + }); + it("swaps the displayed plan and resets scroll on setPlanContent", () => { const longPlan = Array.from({ length: 200 }, (_, i) => `para ${i}`).join("\n\n"); const overlay = new PlanReviewOverlay(