diff --git a/packages/tui/src/component/prompt/index.tsx b/packages/tui/src/component/prompt/index.tsx
index 247b20fa58b..66d02d6879c 100644
--- a/packages/tui/src/component/prompt/index.tsx
+++ b/packages/tui/src/component/prompt/index.tsx
@@ -130,7 +130,7 @@ function formatEditorContext(selection: EditorSelection) {
return `${ranges.join("\n")} This may or may not be relevant to the current task.\n`
}
-let stashed: { prompt: PromptInfo; cursor: number } | undefined
+const drafts = new Map()
function argumentSlash(input: string, commands: readonly KeymapCommand[]) {
const head = parseSlashHead(input, /\s/)
@@ -600,22 +600,40 @@ export function Prompt(props: PromptProps) {
},
}
- onMount(() => {
- const saved = stashed
- stashed = undefined
- if (store.prompt.text) return
- if (saved && saved.prompt.text) {
- input.setText(saved.prompt.text)
- setStore("prompt", saved.prompt)
- restoreExtmarksFromPrompt(saved.prompt)
- input.cursorOffset = saved.cursor
+ function saveDraft(sessionID: string | undefined) {
+ if (!store.prompt.text) {
+ drafts.delete(sessionID)
+ return
}
- })
+ drafts.set(sessionID, { prompt: unwrap(store.prompt), cursor: input.cursorOffset })
+ }
+
+ function restoreDraft(sessionID: string | undefined) {
+ const saved = drafts.get(sessionID)
+ drafts.delete(sessionID)
+ ref.reset()
+ if (!saved?.prompt.text) return
+ ref.set(saved.prompt)
+ input.cursorOffset = saved.cursor
+ }
+
+ let draftSessionID = props.sessionID
+ onMount(() => restoreDraft(draftSessionID))
+
+ createEffect(
+ on(
+ () => props.sessionID,
+ (sessionID) => {
+ saveDraft(draftSessionID)
+ draftSessionID = sessionID
+ restoreDraft(sessionID)
+ },
+ { defer: true },
+ ),
+ )
onCleanup(() => {
- if (store.prompt.text) {
- stashed = { prompt: unwrap(store.prompt), cursor: input.cursorOffset }
- }
+ saveDraft(draftSessionID)
setInputTarget(undefined)
props.ref?.(undefined)
})
diff --git a/packages/tui/test/app-lifecycle.test.tsx b/packages/tui/test/app-lifecycle.test.tsx
index ef31b879544..d3e20dbd1d2 100644
--- a/packages/tui/test/app-lifecycle.test.tsx
+++ b/packages/tui/test/app-lifecycle.test.tsx
@@ -294,3 +294,72 @@ test("session startup prompt is submitted exactly once", async () => {
await server.stop()
}
})
+
+test("new session does not inherit the current session prompt draft", async () => {
+ const setup = await createTestRenderer({ width: 80, height: 24, useThread: false })
+ const events = createEventStream()
+ const cwd = process.cwd()
+ const location = { directory: cwd, project: { id: "project", directory: cwd } }
+ const session = {
+ id: "dummy",
+ title: "Demo session",
+ projectID: "project",
+ location: { directory: cwd },
+ agent: "build",
+ model: { providerID: "provider", id: "model" },
+ cost: 0,
+ tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } },
+ time: { created: 0, updated: 0 },
+ }
+ const calls = createFetch((url) => {
+ if (url.pathname === "/api/location") return json(location)
+ if (url.pathname === "/api/session") return json({ data: [session], cursor: {} })
+ if (url.pathname === "/api/session/dummy") return json({ data: session })
+ if (url.pathname === "/api/session/dummy/message") return json({ data: [], cursor: {} })
+ if (url.pathname === "/api/session/dummy/pending") return json({ data: [] })
+ if (url.pathname === "/api/session/dummy/permission") return json({ data: [] })
+ if (url.pathname === "/api/agent")
+ return json({ location, data: [{ id: "build", mode: "primary", hidden: false, permissions: [] }] })
+ if (url.pathname === "/api/model")
+ return json({ location, data: [{ id: "model", providerID: "provider", name: "Model", variants: [] }] })
+ }, events)
+ const server = Bun.serve({ port: 0, fetch: (request) => calls.fetch(request) })
+
+ try {
+ const { run } = await import("../src/app")
+ const task = Effect.runPromise(
+ run({
+ app: { name: "test", version: "test", channel: "test" },
+ server: { endpoint: { url: server.url.toString() } },
+ config: { get: async () => ({}), update: async () => ({}) },
+ packages: { resolve: async () => undefined },
+ terminalHandoff: async () => ({ renderer: setup.renderer, mode: "dark", complete: () => {} }),
+ args: { sessionID: "dummy" },
+ log: () => {},
+ }).pipe(Effect.provide(AppNodeBuilder.build(Global.node)), Effect.provide(FileSystem.layerNoop({}))),
+ )
+
+ await Promise.race([
+ (async () => {
+ while (!setup.renderer.currentFocusedEditor) await Bun.sleep(10)
+ })(),
+ Bun.sleep(2_000).then(() => {
+ throw new Error("session prompt did not focus")
+ }),
+ ])
+ await setup.mockInput.typeText("keep this draft")
+ expect(setup.renderer.currentFocusedEditor?.plainText).toBe("keep this draft")
+
+ setup.mockInput.pressKey("x", { ctrl: true })
+ await Bun.sleep(10)
+ setup.mockInput.pressKey("n")
+ await Bun.sleep(20)
+
+ expect(setup.renderer.currentFocusedEditor?.plainText).toBe("")
+ setup.renderer.destroy()
+ await task
+ } finally {
+ if (!setup.renderer.isDestroyed) setup.renderer.destroy()
+ await server.stop()
+ }
+})