From 699e74ae8619e9e91ef059507760922e3a2257fd Mon Sep 17 00:00:00 2001 From: Luke Parker <10430890+Hona@users.noreply.github.com> Date: Fri, 16 Oct 2026 10:23:51 +1000 Subject: [PATCH] fix(app): keep the file view in place when a link rereads it (#55899) --- packages/app/e2e/regression/review.spec.ts | 26 ++++++++++++++++++---- packages/gui-extensions/src/file/view.tsx | 14 +++++++----- 2 files changed, 30 insertions(+), 10 deletions(-) diff --git a/packages/app/e2e/regression/review.spec.ts b/packages/app/e2e/regression/review.spec.ts index 9e6dcd24457..631806b42c0 100644 --- a/packages/app/e2e/regression/review.spec.ts +++ b/packages/app/e2e/regression/review.spec.ts @@ -537,7 +537,9 @@ test("a restored file tab keeps the file tree on Files Changed", async ({ page } }) test("rereads a file each time an artifact link opens it", async ({ page }) => { - const file = { content: "first draft" } + // Enough lines to scroll the code view, ending in the draft's name. + const draft = (name: string) => [...Array.from({ length: 200 }, (_, index) => `item ${index + 1}`), name].join("\n") + const file = { content: draft("first draft") } const messages = [ { id: "msg_prompt", type: "user", text: "Write the shopping list", time: { created: 1 } }, @@ -565,13 +567,29 @@ test("rereads a file each time an artifact link opens it", async ({ page }) => { await expect(tab).toHaveAttribute("aria-selected", "true") await expect(panel.getByText("first draft", { exact: true })).toBeVisible() - file.content = "second draft" + // Rereading the open file updates it in place: the code view keeps its node and never jumps back to the top. + const code = panel.locator(".scroll-view__viewport", { has: page.locator('[data-component="file"]') }) + await code.hover() + await page.mouse.wheel(0, 100_000) + await expect + .poll(() => code.evaluate((element) => element.scrollHeight - element.clientHeight - element.scrollTop)) + .toBeLessThanOrEqual(1) + const scrolled = await code.evaluate((element) => element.scrollTop) + expect(scrolled).toBeGreaterThan(0) + await code.evaluate((element) => void (element.dataset.e2eProbe = "original")) + file.content = draft("second draft") + await link.click() + await expect(panel.getByText("second draft", { exact: true })).toBeVisible() + expect(await code.evaluate((element) => element.dataset.e2eProbe)).toBe("original") + expect(await code.evaluate((element) => element.scrollTop)).toBe(scrolled) + + file.content = draft("third draft") await tab.locator("..").getByRole("button", { name: "Close tab" }).click() await expect(tab).toHaveCount(0) await link.click() await expect(tab).toHaveAttribute("aria-selected", "true") - await expect(panel.getByText("second draft", { exact: true })).toBeVisible() - await expect(panel.getByText("first draft", { exact: true })).toHaveCount(0) + await expect(panel.getByText("third draft", { exact: true })).toBeVisible() + await expect(panel.getByText("second draft", { exact: true })).toHaveCount(0) // A restored file tab whose file was deleted shows its inline not-found state without a toast; clicking a broken link // when no tab is open for it shows the toast. diff --git a/packages/gui-extensions/src/file/view.tsx b/packages/gui-extensions/src/file/view.tsx index 9f49fac6e75..529c1db7eb6 100644 --- a/packages/gui-extensions/src/file/view.tsx +++ b/packages/gui-extensions/src/file/view.tsx @@ -448,14 +448,14 @@ export function SessionFileView(props: { session: MountedSession; screen: Sessio }, ) - const renderFile = (source: string) => ( + const renderFile = () => (
( + // It reads the text itself: `Show` tracks its fallback, so passing the content in would rebuild the view, scrolled to + // the top, each time a reread stores a new content object. + const codeView = () => ( - {renderFile(source)} + {renderFile()} ) @@ -504,13 +506,13 @@ export function SessionFileView(props: { session: MountedSession; screen: Sessio {(value) => ( - + )}