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
27 changes: 27 additions & 0 deletions src/components/Sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2983,6 +2983,33 @@ function ProjectFolderView({
<Plus size={12} />
</button>
</Tooltip>
{inSourceRoot && isDefaultProjectFolder(folder) ? (
<Tooltip
label={sidebarText(
t,
"sidebar.aria.newIsolatedSessionInProjectFolder",
)}
shortcut={formatHotkey(shortcuts.newIsolatedSession)}
side="bottom"
>
<button
type="button"
aria-label={sidebarText(
t,
"sidebar.aria.newIsolatedSessionInProjectFolder",
)}
onPointerDown={(e) => e.stopPropagation()}
onClick={(e) => {
e.stopPropagation();
onAddSession(true, "regular", "terminal");
}}
onKeyDown={(e) => e.stopPropagation()}
className="flex size-5 items-center justify-center rounded text-fg-muted transition hover:bg-bg-elevated hover:text-fg"
>
<GitBranch size={12} />
</button>
</Tooltip>
) : null}
<Tooltip
label={sidebarText(
t,
Expand Down
1 change: 1 addition & 0 deletions src/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -1297,6 +1297,7 @@
"newAutonomousGoalSession": "New Loop session",
"newGraphSession": "New Graph session",
"newIsolatedSessionInProject": "New worktree session in this project",
"newIsolatedSessionInProjectFolder": "New worktree session in this workspace",
"newChatSessionInProject": "New chat session in this project",
"newControlSessionInProject": "New control session in this project",
"goalSession": "Loop session",
Expand Down
1 change: 1 addition & 0 deletions src/locales/ja.json
Original file line number Diff line number Diff line change
Expand Up @@ -1297,6 +1297,7 @@
"newAutonomousGoalSession": "新しい Loop セッション",
"newGraphSession": "新しい Graph セッション",
"newIsolatedSessionInProject": "このプロジェクトの新しいワークツリー セッション",
"newIsolatedSessionInProjectFolder": "このワークスペースの新しいワークツリー セッション",
"newChatSessionInProject": "このプロジェクトの新しいチャット セッション",
"newControlSessionInProject": "このプロジェクトの新しいコントロール セッション",
"goalSession": "Loop セッション",
Expand Down
1 change: 1 addition & 0 deletions src/locales/ko.json
Original file line number Diff line number Diff line change
Expand Up @@ -1297,6 +1297,7 @@
"newAutonomousGoalSession": "새 Loop 세션",
"newGraphSession": "새 Graph 세션",
"newIsolatedSessionInProject": "이 프로젝트에 새 워크트리 세션 만들기",
"newIsolatedSessionInProjectFolder": "이 작업공간에 새 워크트리 세션 만들기",
"newChatSessionInProject": "이 프로젝트에 새 채팅 세션 만들기",
"newControlSessionInProject": "이 프로젝트에 새 컨트롤 세션 만들기",
"goalSession": "Loop 세션",
Expand Down
1 change: 1 addition & 0 deletions src/locales/zh-CN.json
Original file line number Diff line number Diff line change
Expand Up @@ -1297,6 +1297,7 @@
"newAutonomousGoalSession": "新建 Loop 会话",
"newGraphSession": "新建 Graph 会话",
"newIsolatedSessionInProject": "此项目中的新工作树会话",
"newIsolatedSessionInProjectFolder": "此工作区中的新工作树会话",
"newChatSessionInProject": "此项目中的新聊天会话",
"newControlSessionInProject": "此项目中的新控制会话",
"goalSession": "Loop 会话",
Expand Down
85 changes: 85 additions & 0 deletions tests/e2e/sidebar.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4459,6 +4459,91 @@ test.describe("sidebar: project lifecycle", () => {
).toContainText("demo-api");
});

test("a source folder header creates a worktree session in that source repository", async ({
page,
tauri,
}) => {
await tauri.respond("list_projects", [
{
repo_path: "/tmp/demo",
name: "demo",
created_at: "2026-01-01T00:00:00Z",
position: 0,
source_paths: ["/tmp/acorn"],
},
]);
await tauri.handle("list_sessions", () => {
const w = window as unknown as {
__sessions?: Array<Record<string, unknown>>;
};
return w.__sessions ?? [];
});
await tauri.handle("create_session", (args) => {
const input = args as {
name: string;
repoPath: string;
isolated: boolean;
kind: string;
};
const w = window as unknown as {
__createCalls?: unknown[];
__sessions?: Array<Record<string, unknown>>;
};
w.__createCalls = [...(w.__createCalls ?? []), args];
const created = {
id: "created-worktree-session",
name: input.name,
repo_path: input.repoPath,
worktree_path: `${input.repoPath}/.acorn/worktrees/${input.name}`,
branch: "worktree",
isolated: input.isolated,
project_scoped: true,
status: "ready",
created_at: "2026-01-01T00:00:00Z",
updated_at: "2026-01-01T00:00:00Z",
last_message: null,
title_source: "default",
kind: input.kind,
owner: { kind: "user" },
position: 0,
in_worktree: true,
};
w.__sessions = [...(w.__sessions ?? []), created];
return created;
});

await page.goto("/");

const sourceFolderHeader = page.locator(
'aside [data-sidebar-workspace-id="/tmp/acorn"]',
);
await sourceFolderHeader.hover();
const createWorktreeSessionButton = sourceFolderHeader.getByRole("button", {
name: "New worktree session in this workspace",
});
await expect(createWorktreeSessionButton).toBeVisible();
await createWorktreeSessionButton.click();

await expect
.poll(async () =>
page.evaluate(
() =>
(
window as unknown as {
__createCalls?: Array<Record<string, unknown>>;
}
).__createCalls ?? [],
),
)
.toEqual([
expect.objectContaining({
repoPath: "/tmp/acorn",
isolated: true,
kind: "regular",
}),
]);
});

test("each root's row holds the sessions started in that root", async ({
page,
tauri,
Expand Down
Loading