From 92f05f873eb58398d950f84c1c3e918bbec69c06 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 20 Sep 2026 11:11:01 +0000 Subject: [PATCH 1/2] Wait for /new to finish switching sessions in e2e. Sending /new clears the composer before activateSession writes shadow:selected, so the new-task test was reading the old id during the Opening task overlay. Wait for that switch like the delete path. Co-authored-by: Bob Corbin --- ui/e2e/workspace.spec.ts | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/ui/e2e/workspace.spec.ts b/ui/e2e/workspace.spec.ts index 8dafe5e7..06af905f 100644 --- a/ui/e2e/workspace.spec.ts +++ b/ui/e2e/workspace.spec.ts @@ -201,18 +201,13 @@ test("new-task shortcuts and deletion keep task selection usable", async ({ await prompt.fill("/new"); await page.getByRole("button", { name: "Send task", exact: true }).click(); await expect(prompt).toHaveValue(""); + await expect(page.locator(".loading-task")).toHaveCount(0); await expect - .poll(async () => { - const selected = await page.evaluate(() => - localStorage.getItem("shadow:selected"), - ); - return Boolean(selected && selected !== first); - }) - .toBe(true); + .poll(() => page.evaluate(() => localStorage.getItem("shadow:selected"))) + .not.toBe(first); const toDelete = await page.evaluate(() => localStorage.getItem("shadow:selected"), ); - expect(toDelete).not.toBe(first); await page.keyboard.press("Control+k"); await page .getByRole("textbox", { name: "Search commands" }) From 1c818f1a5287d3244f6efb8fe6f2d2db2dfdff0e Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 20 Sep 2026 11:17:07 +0000 Subject: [PATCH 2/2] Select the new session before /new clears the composer. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Sending /new used to empty the prompt immediately, then activate the session and only then write shadow:selected. The e2e contract — and the UI — treat an empty composer as "already on the new task". Commit the selected id when createSession returns, skip persisting /new as a draft, and let openSession clear the composer so selection is in place before the prompt looks idle. Co-authored-by: Bob Corbin --- ui/e2e/workspace.spec.ts | 6 ++---- ui/src/App.tsx | 25 ++++++++++++++++--------- 2 files changed, 18 insertions(+), 13 deletions(-) diff --git a/ui/e2e/workspace.spec.ts b/ui/e2e/workspace.spec.ts index 06af905f..95658f79 100644 --- a/ui/e2e/workspace.spec.ts +++ b/ui/e2e/workspace.spec.ts @@ -201,13 +201,11 @@ test("new-task shortcuts and deletion keep task selection usable", async ({ await prompt.fill("/new"); await page.getByRole("button", { name: "Send task", exact: true }).click(); await expect(prompt).toHaveValue(""); - await expect(page.locator(".loading-task")).toHaveCount(0); - await expect - .poll(() => page.evaluate(() => localStorage.getItem("shadow:selected"))) - .not.toBe(first); const toDelete = await page.evaluate(() => localStorage.getItem("shadow:selected"), ); + expect(toDelete).not.toBe(first); + await expect(page.locator(".loading-task")).toHaveCount(0); await page.keyboard.press("Control+k"); await page .getByRole("textbox", { name: "Search commands" }) diff --git a/ui/src/App.tsx b/ui/src/App.tsx index 7d886b33..a1462158 100644 --- a/ui/src/App.tsx +++ b/ui/src/App.tsx @@ -55,6 +55,8 @@ const formatTokens = (n: number) => n >= 1000 ? `${(n / 1000).toFixed(1)}k` : String(n); const draftKey = (id: string, workspace: string) => `shadow:draft:${id || workspace}`; +const isSessionCommand = (text: string) => + ["/new", "/clear"].includes(text.trim()); export default function App() { const [ready, setReady] = useState(false); @@ -111,6 +113,7 @@ export default function App() { const selectedRef = useRef(""); const selection = useRef(0); const submittingRef = useRef(false); + const taskRef = useRef(""); const toastSeq = useRef(0); const booted = useRef(false); const activationQueue = useRef>(Promise.resolve()); @@ -156,6 +159,7 @@ export default function App() { }); const { transcript, setTranscript, job, busy, connection } = conversation; const locked = busy || submitting || switching; + taskRef.current = task; async function reloadConfig() { const [config, state, modelData, providerData] = await Promise.all([ @@ -173,9 +177,11 @@ export default function App() { async function openSession(id: string) { if (submittingRef.current) return; if (selectedRef.current) { - if (task) - localStorage.setItem(draftKey(selectedRef.current, workspace), task); - else localStorage.removeItem(draftKey(selectedRef.current, workspace)); + const draft = taskRef.current; + if (draft && !isSessionCommand(draft)) + localStorage.setItem(draftKey(selectedRef.current, workspace), draft); + else if (!draft) + localStorage.removeItem(draftKey(selectedRef.current, workspace)); } const ticket = ++selection.current; setSwitching(true); @@ -255,6 +261,7 @@ export default function App() { useEffect(() => { const key = draftKey(sessionId, workspace); const timer = setTimeout(() => { + if (isSessionCommand(task)) return; if (task) localStorage.setItem(key, task); else localStorage.removeItem(key); }, 200); @@ -303,14 +310,15 @@ export default function App() { return () => clearInterval(timer); }, [job?.id, busy]); - async function newSession() { - if (submitting || switching) return; + async function newSession(opts?: { force?: boolean }) { + if (!opts?.force && (submitting || switching)) return; if (!workspace) { setOverlay("project"); return; } try { const created = await api.createSession(workspace, "New task"); + localStorage.setItem("shadow:selected", created.id); await openSession(created.id); promptRef.current?.focus(); } catch (e) { @@ -369,7 +377,7 @@ export default function App() { const [name, ...rest] = text.slice(1).split(" "); const args = rest.join(" "); if (name === "new" || name === "clear") { - await newSession(); + await newSession({ force: true }); return; } const panels: Record = { @@ -397,9 +405,8 @@ export default function App() { async function submit() { if (locked || submittingRef.current || (!task.trim() && !chips.length)) return; - if (["/new", "/clear"].includes(task.trim())) { - setTask(""); - await newSession(); + if (isSessionCommand(task)) { + await newSession({ force: true }); return; } const original = task;