diff --git a/webviews/src/agent-session/acpmux/App.tsx b/webviews/src/agent-session/acpmux/App.tsx index a2c062a1e8f9..49fe50dc1190 100644 --- a/webviews/src/agent-session/acpmux/App.tsx +++ b/webviews/src/agent-session/acpmux/App.tsx @@ -7814,9 +7814,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 cbf1ceb722cf..a3b2a9384ab7 100644 --- a/webviews/src/agent-session/acpmux/ComposerContext.tsx +++ b/webviews/src/agent-session/acpmux/ComposerContext.tsx @@ -507,6 +507,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 }); } @@ -579,7 +581,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..c788958a3168 100644 --- a/webviews/test/composer-extra-folders.test.tsx +++ b/webviews/test/composer-extra-folders.test.tsx @@ -165,3 +165,82 @@ 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()); + } + }); +} + +// 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()); + } +});