From 8ff12590a3bdc41de95e236224387af1f24f2472 Mon Sep 17 00:00:00 2001 From: James Long Date: Thu, 9 Jul 2026 15:45:28 +0000 Subject: [PATCH] fix(tui): preserve first prompt in fresh sessions --- packages/tui/src/component/prompt/index.tsx | 1 + packages/tui/src/context/route.tsx | 1 + packages/tui/src/routes/session/index.tsx | 5 +- packages/tui/src/routes/session/rows.ts | 3 +- packages/tui/test/cli/tui/data.test.tsx | 70 +++++++++++++++++++++ 5 files changed, 78 insertions(+), 2 deletions(-) diff --git a/packages/tui/src/component/prompt/index.tsx b/packages/tui/src/component/prompt/index.tsx index b256e6ac6b..437ade0ac3 100644 --- a/packages/tui/src/component/prompt/index.tsx +++ b/packages/tui/src/component/prompt/index.tsx @@ -1169,6 +1169,7 @@ export function Prompt(props: PromptProps) { route.navigate({ type: "session", sessionID, + fresh: true, }) }, 50) } diff --git a/packages/tui/src/context/route.tsx b/packages/tui/src/context/route.tsx index 7355fe54d7..e9ff862757 100644 --- a/packages/tui/src/context/route.tsx +++ b/packages/tui/src/context/route.tsx @@ -11,6 +11,7 @@ export type HomeRoute = { export type SessionRoute = { type: "session" sessionID: string + fresh?: boolean prompt?: PromptInfo } diff --git a/packages/tui/src/routes/session/index.tsx b/packages/tui/src/routes/session/index.tsx index 5210097fa9..b051a76df0 100644 --- a/packages/tui/src/routes/session/index.tsx +++ b/packages/tui/src/routes/session/index.tsx @@ -237,7 +237,10 @@ export function Session() { const toast = useToast() const sdk = useSDK() const editor = useEditorContext() - const rows = createSessionRows(() => route.sessionID) + const rows = createSessionRows( + () => route.sessionID, + () => route.fresh !== true, + ) createEffect( on(descendantSessionIDs, (sessionIDs) => { diff --git a/packages/tui/src/routes/session/rows.ts b/packages/tui/src/routes/session/rows.ts index a148f9fd20..3ce54a38d0 100644 --- a/packages/tui/src/routes/session/rows.ts +++ b/packages/tui/src/routes/session/rows.ts @@ -20,7 +20,7 @@ export type SessionRow = } | { type: "assistant-footer"; messageID: string } -export function createSessionRows(sessionID: Accessor) { +export function createSessionRows(sessionID: Accessor, refresh: Accessor = () => true) { const data = useData() const [rows, setRows] = createStore([]) const revertBoundary = () => data.session.get(sessionID())?.revert?.messageID @@ -54,6 +54,7 @@ export function createSessionRows(sessionID: Accessor) { createEffect( on(sessionID, (id) => { setRows(reconcile(reduce())) + if (!refresh()) return void data.session.message.refresh(id).then( () => { if (sessionID() !== id) return diff --git a/packages/tui/test/cli/tui/data.test.tsx b/packages/tui/test/cli/tui/data.test.tsx index 5cdcb51e89..fd7a1de657 100644 --- a/packages/tui/test/cli/tui/data.test.tsx +++ b/packages/tui/test/cli/tui/data.test.tsx @@ -2066,6 +2066,76 @@ test("renders admitted prompts immediately and tracks them until promoted", asyn } }) +test("does not refresh a fresh session timeline", async () => { + const events = createEventStream() + const sessionID = "session-refresh-race" + const messageID = "msg-refresh-race" + let refreshes = 0 + const calls = createFetch((url) => { + if (url.pathname === `/api/session/${sessionID}/message`) { + refreshes++ + return json({ data: [], cursor: {} }) + } + }, events) + let data!: ReturnType + let rows!: ReturnType + + function Probe() { + data = useData() + rows = createSessionRows( + () => sessionID, + () => false, + ) + return + } + + const app = await testRender(() => ( + + + + + + + + + + )) + + try { + emitEvent(events, { + id: "evt-refresh-race-admitted", + created: 1, + type: "session.input.admitted", + durable: durable(sessionID), + data: { + sessionID, + inputID: messageID, + input: { type: "user", data: { text: "do not lose me" }, delivery: "steer" }, + }, + }) + await wait(() => data.session.message.get(sessionID, messageID)?.type === "user") + + emitEvent(events, { + id: "evt-refresh-race-promoted", + created: 2, + type: "session.input.promoted", + durable: durable(sessionID, 1), + data: { sessionID, inputID: messageID }, + }) + await wait(() => data.session.input.list(sessionID).length === 0) + + expect(refreshes).toBe(0) + expect(rows).toContainEqual({ type: "message", messageID }) + expect(data.session.message.get(sessionID, messageID)).toMatchObject({ + id: messageID, + type: "user", + text: "do not lose me", + }) + } finally { + app.renderer.destroy() + } +}) + test("projects live instruction updates with their message ID", async () => { const events = createEventStream() const calls = createFetch(undefined, events)