From c6e95f04b8f578030017e547519ed85705008108 Mon Sep 17 00:00:00 2001 From: Leo Li Date: Wed, 4 Nov 2026 00:34:37 +0000 Subject: [PATCH 1/2] Composer extra folders: outside dismiss ends Add folder mode; Cloud and Sandboxed offer and send none Fixes #34408: the folder popover's non-Escape close cleared only the query, so Add folder mode survived and the next project pick added a folder. Fixes #34403: Cloud and Sandboxed starts carry no additional directories, so in those run modes the composer hides Add folder and +N, and the client gets no extra folders for its next start. Co-Authored-By: Claude Opus 5.5 Agent-Session: https://cmux.com/deeplink/agent?id=aid-244b9c62b2e8fedec46c --- webviews/src/agent-session/acpmux/App.tsx | 6 ++- .../agent-session/acpmux/ComposerContext.tsx | 6 ++- .../agent-session/acpmux/FolderPopover.tsx | 7 +++- webviews/test/composer-extra-folders.test.tsx | 42 +++++++++++++++++++ 4 files changed, 57 insertions(+), 4 deletions(-) diff --git a/webviews/src/agent-session/acpmux/App.tsx b/webviews/src/agent-session/acpmux/App.tsx index ce15174f8ac4..c8edf450f847 100644 --- a/webviews/src/agent-session/acpmux/App.tsx +++ b/webviews/src/agent-session/acpmux/App.tsx @@ -7800,9 +7800,11 @@ function AcpmuxPane({ initialChat }: { initialChat?: InitialChat }) { // so the client knows it as `local` (ownMachineFolders). const paneMachine = snapshot.summary?.peer || (snapshot.summary?.hostKind === "cloud" && snapshot.summary.host) || "local"; + // Only a start here takes them: a Cloud or Sandboxed start carries no extra folders (cmux#34403). + const extraFoldersSent = sandbox.mode === "persistent" ? extraFolderDraft : undefined; useEffect(() => { - directClient.current?.setNewChatFolders(ownMachineFolders(extraFolderDraft, paneMachine)); - }, [extraFolderDraft, paneMachine, clientEpoch]); + directClient.current?.setNewChatFolders(ownMachineFolders(extraFoldersSent, paneMachine)); + }, [extraFoldersSent, paneMachine, clientEpoch]); const continuationProject = useRef(projectDraft); const continuationProjectGeneration = useRef(0); useLayoutEffect(() => { diff --git a/webviews/src/agent-session/acpmux/ComposerContext.tsx b/webviews/src/agent-session/acpmux/ComposerContext.tsx index 4adf360fb747..82e3156e3c2a 100644 --- a/webviews/src/agent-session/acpmux/ComposerContext.tsx +++ b/webviews/src/agent-session/acpmux/ComposerContext.tsx @@ -580,7 +580,11 @@ export function ComposerContext({ }; const folderShown = !savedPick && !(readOnly && !newChats && !currentFolder); // The new chat's extra folders on the picked machine (a pick of the chat's own folder is never one). - const extraShown = extraFolders && !readOnly ? draftFolders(extraFolders.draft, selectedComputer) : undefined; + // A Cloud or Sandboxed start carries none (cmux#34403), so those modes offer no Add folder… or +N. + const extraShown = + extraFolders && !readOnly && !cloudPicked && !sandboxPicked + ? draftFolders(extraFolders.draft, selectedComputer) + : undefined; const extra: ExtraFolderRows | undefined = extraFolders && extraShown ? { diff --git a/webviews/src/agent-session/acpmux/FolderPopover.tsx b/webviews/src/agent-session/acpmux/FolderPopover.tsx index a2284071a1e4..f5050605de02 100644 --- a/webviews/src/agent-session/acpmux/FolderPopover.tsx +++ b/webviews/src/agent-session/acpmux/FolderPopover.tsx @@ -332,7 +332,12 @@ export function FolderPopover({ close(); } else { setOpen(next); - if (!next) setQuery(""); + // Every close ends Add folder…'s mode, an outside click as much as Escape (cmux#34408). + if (!next) { + setQuery(""); + setExpanded(new Set()); + setAddingExtra(false); + } } }} anchor={open ? anchor : null} diff --git a/webviews/test/composer-extra-folders.test.tsx b/webviews/test/composer-extra-folders.test.tsx index 6cfd975839db..be5f45096ed0 100644 --- a/webviews/test/composer-extra-folders.test.tsx +++ b/webviews/test/composer-extra-folders.test.tsx @@ -165,3 +165,45 @@ test("a started chat offers no Add folder…", async () => { await act(async () => root.unmount()); } }); + +// cmux#34408: an outside dismiss after Add folder… ends add mode, so the next pick changes the project. +test("an outside dismiss after Add folder… leaves the next pick changing the project", async () => { + const picked: string[] = []; + const { root, changes } = await renderTray({ onProject: (cwd: string) => picked.push(cwd) }); + try { + await openFolderMenu(); + await press(menuItem("Add folder…")!); + const outside = dom.window.document.body; + await act(async () => { + for (const type of ["pointerdown", "mousedown", "pointerup", "mouseup", "click"]) + outside.dispatchEvent(new dom.window.MouseEvent(type, { bubbles: true, button: 0 })); + }); + await settle(); + expect(dom.window.document.querySelector(".acpmux-folder-popover")).toBeNull(); + await openFolderMenu(); + expect(menuItem("Add folder…")).toBeDefined(); + await press(menuItem("notes")!); + expect(picked).toEqual(["/Users/me/notes"]); + expect(changes).toEqual([]); + } finally { + await act(async () => root.unmount()); + } +}); + +// cmux#34403: Cloud and Sandboxed starts carry no extra folders, so the composer offers none. +for (const mode of ["cloud", "sandboxed"] as const) { + test(`${mode} run mode offers no Add folder… and shows no +N`, async () => { + const { root } = await renderTray( + { runMode: { mode, onMode: () => {} } }, + { machine: "local", folders: ["/Users/me/notes"] }, + ); + try { + expect(face()).not.toContain("+1"); + await openFolderMenu(); + expect(menuItem("Add folder…")).toBeUndefined(); + expect(dom.window.document.querySelectorAll(".acpmux-extra-folder").length).toBe(0); + } finally { + await act(async () => root.unmount()); + } + }); +} From 61a536cd4c970da6d8f5686b8f3541b174aab821 Mon Sep 17 00:00:00 2001 From: Leo Li Date: Wed, 4 Nov 2026 01:31:31 +0000 Subject: [PATCH 2/2] Composer extra folders: a starting Cloud machine's pick drops this machine's folders at once Fixes #34527: pickCloudMachine returned before pickComputer's draft clear, so while the machine started (or after it failed) the previous machine's +N stayed and could go with the wrong target. Co-Authored-By: Claude Opus 5.5 Agent-Session: https://cmux.com/deeplink/agent?id=aid-244b9c62b2e8fedec46c --- .../agent-session/acpmux/ComposerContext.tsx | 2 + webviews/test/composer-extra-folders.test.tsx | 37 +++++++++++++++++++ 2 files changed, 39 insertions(+) diff --git a/webviews/src/agent-session/acpmux/ComposerContext.tsx b/webviews/src/agent-session/acpmux/ComposerContext.tsx index 82e3156e3c2a..e5ccf54c96b6 100644 --- a/webviews/src/agent-session/acpmux/ComposerContext.tsx +++ b/webviews/src/agent-session/acpmux/ComposerContext.tsx @@ -508,6 +508,8 @@ export function ComposerContext({ const pickCloudMachine = (id: string) => { const machine = computers.find((computer) => computer.id === id)?.cloudMachine; if (machine && !readOnly) { + // Folders are the machine's: the pick drops this machine's at once, not when (or if) it arrives (cmux#34527). + if (extraFolders?.draft) extraFolders.onChange(undefined); onCloudMachine(machine); setWaiting({ key: pickKey, id: machine.id, name: machine.name }); } diff --git a/webviews/test/composer-extra-folders.test.tsx b/webviews/test/composer-extra-folders.test.tsx index be5f45096ed0..c788958a3168 100644 --- a/webviews/test/composer-extra-folders.test.tsx +++ b/webviews/test/composer-extra-folders.test.tsx @@ -207,3 +207,40 @@ for (const mode of ["cloud", "sandboxed"] as const) { } }); } + +// cmux#34527: a pick of an account Cloud machine that is still starting drops this machine's folders +// at once, not only once it arrives (or never, when it fails to start). +test("picking a starting Cloud machine drops this machine's extra folders at once", async () => { + const cloud: string[] = []; + const { root, changes } = await renderTray( + { + sessions: [], + onConnect: () => undefined, + onCloudMachine: (machine: { id: string }) => cloud.push(machine.id), + cloudMachines: [{ id: "vm-ready", name: "eval box", stage: "ready" }], + }, + { machine: "local", folders: ["/Users/me/notes"] }, + ); + try { + expect(face()).toContain("+1"); + const machine = [...dom.window.document.querySelectorAll(".acpmux-location-button")].find( + (button) => (button.getAttribute("aria-label") ?? "").includes(":"), + ); + expect(machine).toBeDefined(); + await act(async () => { + machine!.dispatchEvent(new dom.window.MouseEvent("pointerdown", { bubbles: true })); + machine!.click(); + }); + await settle(); + const row = [...dom.window.document.querySelectorAll(".acpmux-location-menu .acpmux-menu-item")].find( + (item) => item.textContent?.includes("eval box"), + ); + expect(row).toBeDefined(); + await press(row!); + expect(cloud).toEqual(["vm-ready"]); + expect(changes.at(-1)).toBeUndefined(); + expect(changes.length).toBe(1); + } finally { + await act(async () => root.unmount()); + } +});