Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions webviews/src/agent-session/acpmux/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(() => {
Expand Down
8 changes: 7 additions & 1 deletion webviews/src/agent-session/acpmux/ComposerContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 });
}
Expand Down Expand Up @@ -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
? {
Expand Down
7 changes: 6 additions & 1 deletion webviews/src/agent-session/acpmux/FolderPopover.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand Down
79 changes: 79 additions & 0 deletions webviews/test/composer-extra-folders.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<HTMLButtonElement>(".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<HTMLElement>(".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());
}
});
Loading