diff --git a/webviews/src/agent-session/acpmux/ComposerContext.tsx b/webviews/src/agent-session/acpmux/ComposerContext.tsx
index 39a5e00e63b7..a6236923bbc8 100644
--- a/webviews/src/agent-session/acpmux/ComposerContext.tsx
+++ b/webviews/src/agent-session/acpmux/ComposerContext.tsx
@@ -176,7 +176,7 @@ export function ComposerContext({
/// picker offers only these, whatever the machine's kind.
offloads?: readonly OffloadKind[];
/// The New Tab page's saved environments, in the environment picker. While one is picked it
- /// alone names where the chat starts: the folder and New Worktree controls step aside.
+ /// alone names where the chat starts: the folder control steps aside.
saved?: SavedEnvironmentRows;
/// The machine list opened, and every few seconds while it stays open: read the machines and
/// their status again.
@@ -449,12 +449,6 @@ export function ComposerContext({
if (savedRows?.current !== undefined) savedRows.onClear();
pickComputer(id);
};
- // Preview: New Worktree is a control of its own beside the folder, as Codex's Worktree and
- // Claude's chips are (hq#2569 #9), for a new chat on this computer in a folder.
- const worktree =
- environmentsShown && !readOnly && !savedPick && selectedComputer === "local" && currentFolder && onNewWorktree
- ? () => onNewWorktree(currentFolder)
- : undefined;
const folderShown = !savedPick && !(readOnly && !newChats && !currentFolder);
return (
@@ -492,9 +486,7 @@ export function ComposerContext({
onBrowse={onBrowseProject}
onNoProject={onNoProject}
noProject={noProject && !currentFolder}
- onNewWorktree={
- !worktree && currentFolder && onNewWorktree ? () => onNewWorktree(currentFolder) : undefined
- }
+ onNewWorktree={currentFolder && onNewWorktree ? () => onNewWorktree(currentFolder) : undefined}
localName={localName}
peers={peers}
{...(folderNote && !currentFolder ? { note: folderNote } : {})}
@@ -534,13 +526,6 @@ export function ComposerContext({
}}
/>
)}
- {worktree && (
-
-
-
- )}
{environmentsShown ? (
{
+ await act(async () => {
+ root!.render(
+ undefined}
+ />,
+ );
+ });
+ const strip = container.querySelector(".acpmux-composer-context")!;
+ expect(strip).toBeTruthy();
+ const buttons = [...strip.querySelectorAll("button")].map((button) => button.textContent?.trim());
+ expect(buttons.some((text) => text?.includes("New Worktree"))).toBe(false);
+ const folder = strip.querySelector('[aria-label="Folder"]');
+ expect(folder?.textContent).toContain("cmux");
+ await act(async () => folder!.click());
+ const options = [...document.querySelectorAll('[role="option"]')].map((o) => o.textContent?.trim());
+ expect(options).toContain("New Worktree");
+});
+
test("a fresh New Tab exposes the no-project path in its environment card", async () => {
let browsed = 0;
await act(async () => {
diff --git a/webviews/src/agent-session/acpmux/startFolder.gallery.ts b/webviews/src/agent-session/acpmux/startFolder.gallery.ts
index 8f97b60731a7..9fa779acfbbc 100644
--- a/webviews/src/agent-session/acpmux/startFolder.gallery.ts
+++ b/webviews/src/agent-session/acpmux/startFolder.gallery.ts
@@ -10,7 +10,7 @@ const AGENT_HOME = "/Users/you/Library/Application Support/cmux/agent-home/6b16a
const WORKTREE = "/Users/you/.cmux/worktrees/cmux/fix-sidebar";
-/// A new chat in a project folder: New Worktree is a control beside the folder (cx-lyqio.2, hq#2569 #9).
+/// A new chat in a project folder: New Worktree is an action in its folder menu (cx-lyqio.2).
// A new chat (no session yet: the summary's empty id matches the snapshot's), so it offers New Worktree.
const inProject = noChat([session({ sessionId: "cmux", title: "Fix the sidebar", cwd: "/Users/you/src/cmux" })], {
sessionId: "",
@@ -18,7 +18,9 @@ const inProject = noChat([session({ sessionId: "cmux", title: "Fix the sidebar",
});
const newWorktree: Play = async (ctx) => {
- await ctx.click({ role: "button", name: "New Worktree" });
+ await ctx.click({ selector: '[aria-label="Folder"]' });
+ await ctx.waitFor(() => ctx.document.querySelector(".acpmux-location-menu"));
+ await ctx.click({ role: "option", name: "New Worktree" });
};
// The home folder's row in the folder menu. Rows show the repository or folder name since #22724,
@@ -81,7 +83,7 @@ export default agentPaneEntry({
},
},
"new-worktree": {
- note: "Play: New Worktree beside a new chat's folder; the host makes the worktree on its own branch and the chat starts there (the folder chip names it).",
+ note: "Play: New Worktree from a new chat's folder menu; the host makes the worktree on its own branch and the chat starts there (the folder chip names it).",
ready: { newSession: true },
native: {
"git.worktree.create": {
diff --git a/webviews/test/environment-picker.test.tsx b/webviews/test/environment-picker.test.tsx
index d235cb6e3211..35cde57c6e85 100644
--- a/webviews/test/environment-picker.test.tsx
+++ b/webviews/test/environment-picker.test.tsx
@@ -696,7 +696,8 @@ test("⌥⌘1 on the environment already chosen stays the app's", async () => {
expect(calls).toEqual([]);
});
-// hq#2569 #9: a new chat chooses a worktree beside its folder, as Codex's Worktree and Claude's chips do.
+// A new chat makes a worktree from its folder menu. The bar above the composer holds the folder and
+// the machine only (Leo 2026-10-31, on a New Worktree control there: "who the f*** put worktree on there").
const inFolder = {
summary: {
sessionId: "",
@@ -710,13 +711,25 @@ const worktreeButton = () =>
[...host.querySelectorAll(".acpmux-location-button")].find(
(button) => button.textContent?.trim() === t("worktree.new"),
);
+// New Worktree from the folder menu.
+const newWorktreeFromMenu = async () => {
+ const folder = host.querySelector(
+ `.acpmux-composer-context button[aria-label="${t("composer.folder")}"]`,
+ );
+ expect(folder, "no folder control").toBeTruthy();
+ await act(async () => folder!.click());
+ const action = [...doc().querySelectorAll(".acpmux-location-action")].find(
+ (item) => item.textContent?.trim() === t("worktree.new"),
+ );
+ expect(action, "no New Worktree in the folder menu").toBeTruthy();
+ await act(async () => action!.click());
+};
-test("previews on: New Worktree is a control beside the folder, not inside its menu", async () => {
+test("previews on: New Worktree is in the folder menu, not a control beside the folder", async () => {
const worktrees: string[] = [];
await mount(true, { ...inFolder, onNewWorktree: (cwd: string) => worktrees.push(cwd) });
- const button = worktreeButton();
- expect(button, "no New Worktree beside the folder").toBeTruthy();
- await act(async () => button!.click());
+ expect(worktreeButton(), "a New Worktree control beside the folder").toBeUndefined();
+ await newWorktreeFromMenu();
expect(worktrees).toEqual(["/Users/you/src/cmux"]);
});
@@ -771,7 +784,7 @@ test("New Tab, previews on: New Worktree starts the chat in the worktree the hos
return "/Users/you/wt/app-blue-fox";
});
expect(folderName()).toBe("app");
- await act(async () => worktreeButton()!.click());
+ await newWorktreeFromMenu();
expect(asked).toEqual(["/Users/you/src/app"]);
expect(folderName()).toContain("fox");
});
@@ -780,7 +793,7 @@ test("New Tab, previews on: the host's refusal shows above the card, and the fol
await mountNewTab(async () => {
throw new Error("not a git repository");
});
- await act(async () => worktreeButton()!.click());
+ await newWorktreeFromMenu();
expect(host.querySelector('[role="alert"]')?.textContent).toContain("not a git repository");
expect(folderName()).toBe("app");
});
@@ -788,8 +801,8 @@ test("New Tab, previews on: the host's refusal shows above the card, and the fol
test("New Tab, previews on: a slower worktree reply doesn't replace a newer choice", async () => {
const replies: ((folder: string) => void)[] = [];
await mountNewTab(() => new Promise((resolve) => replies.push(resolve)));
- await act(async () => worktreeButton()!.click());
- await act(async () => worktreeButton()!.click());
+ await newWorktreeFromMenu();
+ await newWorktreeFromMenu();
await act(async () => replies[1]!("/Users/you/wt/app-blue-fox"));
await act(async () => replies[0]!("/Users/you/wt/app-red-owl"));
expect(folderName()).toContain("fox");
@@ -803,7 +816,7 @@ test("New Tab, previews on: a worktree reply after a machine pick doesn't take t
peerConnected: { devbox: true },
});
await mountNewTab(() => new Promise((resolve) => replies.push(resolve)), snapshot);
- await act(async () => worktreeButton()!.click());
+ await newWorktreeFromMenu();
await openPicker();
const devbox = machineRows().find((row) => row.textContent?.includes("devbox"))!;
await act(async () => devbox.click());
diff --git a/webviews/test/new-tab-warm-first-frame.test.ts b/webviews/test/new-tab-warm-first-frame.test.ts
index 204e7ac8a319..66dc7fe84b7a 100644
--- a/webviews/test/new-tab-warm-first-frame.test.ts
+++ b/webviews/test/new-tab-warm-first-frame.test.ts
@@ -296,8 +296,8 @@ await requireBrowserLane("new-tab-warm-first-frame.test.ts", async () => {
expect(painted).toHaveLength(1);
expect(painted[0]).toContain("Fix the atlas build");
expect(painted[0]).toContain("Older device chat");
- // The environment picker puts New Worktree beside the folder (not behind Labs since #29059).
- expect(painted[0]).toStartWith("atlas New Worktree cosmic-gray ::");
+ // The bar names the folder and the machine; New Worktree is in the folder menu only.
+ expect(painted[0]).toStartWith("atlas cosmic-gray ::");
},
);
}
@@ -331,7 +331,7 @@ await requireBrowserLane("new-tab-warm-first-frame.test.ts", async () => {
expect(painted).toHaveLength(1);
expect(painted[0]).toContain("Fix the atlas build");
// The adopting tab's project, not the spare's "Choose folder".
- expect(painted[0]).toStartWith("atlas New Worktree cosmic-gray ::");
+ expect(painted[0]).toStartWith("atlas cosmic-gray ::");
},
);