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
1 change: 0 additions & 1 deletion scripts/design-system-audit.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,6 @@ const buttonStylingBaseline = new Set([
"src/features/extensions/ui/ExtensionModal.tsx",
"src/features/home/widgets/ChecklistWidget.tsx",
"src/features/home/widgets/StickyNoteWidget.tsx",
"src/features/projects/ui/CreateProjectDialog.tsx",
"src/features/projects/ui/ProjectsView.tsx",
"src/features/sessions/ui/session-list/SidebarFlatChatsSection.tsx",
"src/features/sessions/ui/session-list/SidebarProjectList.tsx",
Expand Down
18 changes: 15 additions & 3 deletions src-tauri/crates/berdctl/api-surface-feedback.json
Original file line number Diff line number Diff line change
Expand Up @@ -728,7 +728,7 @@
}
},
"set_startup_mode": {
"description": "Set the startup behavior for new chats in a project. `worktree` creates an isolated Git worktree per chat, `branch` creates a branch, and `none` uses the configured folders as-is. Existing chats and project folder paths are not changed.",
"description": "Set the startup behavior for new chats in a project. `auto-worktree` prompts before creating isolated worktrees, `ask-worktree` leaves worktree creation to the user, and `none` uses configured folders as-is. Legacy `worktree` and `branch` values migrate to those current modes.",
"fields": [
{
"name": "project_id",
Expand All @@ -741,7 +741,13 @@
"required": true,
"kind": "string",
"description": "How new chats start from the project's Git workspaces: use them as-is, create a branch, or create an isolated worktree.",
"values": ["none", "branch", "worktree"]
"values": [
"none",
"branch",
"worktree",
"ask-worktree",
"auto-worktree"
]
}
],
"schema": {
Expand All @@ -754,7 +760,13 @@
},
"mode": {
"type": "string",
"enum": ["none", "branch", "worktree"],
"enum": [
"none",
"branch",
"worktree",
"ask-worktree",
"auto-worktree"
],
"description": "How new chats start from the project's Git workspaces: use them as-is, create a branch, or create an isolated worktree."
}
},
Expand Down
18 changes: 15 additions & 3 deletions src-tauri/crates/berdctl/api-surface.json
Original file line number Diff line number Diff line change
Expand Up @@ -728,7 +728,7 @@
}
},
"set_startup_mode": {
"description": "Set the startup behavior for new chats in a project. `worktree` creates an isolated Git worktree per chat, `branch` creates a branch, and `none` uses the configured folders as-is. Existing chats and project folder paths are not changed.",
"description": "Set the startup behavior for new chats in a project. `auto-worktree` prompts before creating isolated worktrees, `ask-worktree` leaves worktree creation to the user, and `none` uses configured folders as-is. Legacy `worktree` and `branch` values migrate to those current modes.",
"fields": [
{
"name": "project_id",
Expand All @@ -741,7 +741,13 @@
"required": true,
"kind": "string",
"description": "How new chats start from the project's Git workspaces: use them as-is, create a branch, or create an isolated worktree.",
"values": ["none", "branch", "worktree"]
"values": [
"none",
"branch",
"worktree",
"ask-worktree",
"auto-worktree"
]
}
],
"schema": {
Expand All @@ -754,7 +760,13 @@
},
"mode": {
"type": "string",
"enum": ["none", "branch", "worktree"],
"enum": [
"none",
"branch",
"worktree",
"ask-worktree",
"auto-worktree"
],
"description": "How new chats start from the project's Git workspaces: use them as-is, create a branch, or create an isolated worktree."
}
},
Expand Down
2 changes: 1 addition & 1 deletion src-tauri/crates/berdctl/cli-surface-feedback.json
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@
"set-startup-mode": {
"action": "set_startup_mode",
"about": "Set how a project's new chats start from its Git workspaces",
"afterHelp": "The mode applies to every Git workspace configured on the project. Non-Git\nfolders remain in the project but use mode \"none\". Branch and worktree modes\nprompt for a startup name when the next chat is created.\n\nExamples:\n berdctl project set-startup-mode --project-id <project-id> --mode worktree\n berdctl project set-startup-mode --project-id <project-id> --mode none\n\nResult:\n {\"ok\": true, \"mode\": \"worktree\", \"workspaces\": [\n {\"path\": \"...\", \"startup_mode\": \"worktree\"}\n ]}"
"afterHelp": "The mode applies to every Git workspace configured on the project. Non-Git\nfolders remain in the project but use mode \"none\". Legacy \"worktree\" becomes\n\"auto-worktree\" and legacy \"branch\" becomes \"ask-worktree\".\n\nExamples:\n berdctl project set-startup-mode --project-id <project-id> --mode auto-worktree\n berdctl project set-startup-mode --project-id <project-id> --mode none\n\nResult:\n {\"ok\": true, \"mode\": \"auto-worktree\", \"workspaces\": [\n {\"path\": \"...\", \"startup_mode\": \"auto-worktree\"}\n ]}"
},
"archive": {
"action": "archive",
Expand Down
2 changes: 1 addition & 1 deletion src-tauri/crates/berdctl/cli-surface.json
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@
"set-startup-mode": {
"action": "set_startup_mode",
"about": "Set how a project's new chats start from its Git workspaces",
"afterHelp": "The mode applies to every Git workspace configured on the project. Non-Git\nfolders remain in the project but use mode \"none\". Branch and worktree modes\nprompt for a startup name when the next chat is created.\n\nExamples:\n berdctl project set-startup-mode --project-id <project-id> --mode worktree\n berdctl project set-startup-mode --project-id <project-id> --mode none\n\nResult:\n {\"ok\": true, \"mode\": \"worktree\", \"workspaces\": [\n {\"path\": \"...\", \"startup_mode\": \"worktree\"}\n ]}"
"afterHelp": "The mode applies to every Git workspace configured on the project. Non-Git\nfolders remain in the project but use mode \"none\". Legacy \"worktree\" becomes\n\"auto-worktree\" and legacy \"branch\" becomes \"ask-worktree\".\n\nExamples:\n berdctl project set-startup-mode --project-id <project-id> --mode auto-worktree\n berdctl project set-startup-mode --project-id <project-id> --mode none\n\nResult:\n {\"ok\": true, \"mode\": \"auto-worktree\", \"workspaces\": [\n {\"path\": \"...\", \"startup_mode\": \"auto-worktree\"}\n ]}"
},
"archive": {
"action": "archive",
Expand Down
45 changes: 34 additions & 11 deletions src/app/AppShell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,12 @@ import { eventMatchesShortcutCommand } from "@/features/shortcuts/lib/shortcutRe
import { useShortcutsDialogStore } from "@/features/shortcuts/stores/shortcutsDialogStore";
import { prefetchProjectArtifactRenderer } from "@/features/projects/artifact/prefetchProjectArtifactRenderer";
import { getPlatform, type Platform } from "@/shared/lib/platform";
import { archiveProject } from "@/features/projects/api/projects";
import type { ProjectInfo } from "@/features/projects/api/projects";
import {
archiveProject,
isWorktreeStartupMode,
requiresWorkspaceStartup,
type ProjectInfo,
} from "@/features/projects/api/projects";
import {
DEFAULT_SETTINGS_SECTION,
resolveEnabledSettingsSection,
Expand Down Expand Up @@ -1054,6 +1058,9 @@ export function AppShell({
const retryFailedSessionsForProjectRef = useRef<
(project: ProjectInfo) => void
>(() => {});
const startChatForCreatedProjectRef = useRef<(project: ProjectInfo) => void>(
() => {},
);
const refreshProjectsAfterDialogSave = useCallback(
(savedProject: ProjectInfo) => {
useProjectStore
Expand Down Expand Up @@ -1084,6 +1091,8 @@ export function AppShell({
openEditProjectDialog,
} = useProjectDialog({
onProjectSaved: refreshProjectsAfterDialogSave,
onProjectCreated: (project) =>
startChatForCreatedProjectRef.current(project),
});
const startup = useAppStartup();
const [startupLoadingMinElapsed, setStartupLoadingMinElapsed] = useState(
Expand Down Expand Up @@ -1941,7 +1950,10 @@ export function AppShell({
transferSessionTargetOwnership(session.id, sessionId);
promoteDraftSession(session.id, sessionId, {
executionTarget: promotedTarget,
workingDir,
workingDir: latestSessionAfterReady.workingDir ?? workingDir,
workspaceAttachments:
latestSessionAfterReady.workspaceAttachments,
activeWorkspaceId: latestSessionAfterReady.activeWorkspaceId,
...latestSessionPatch,
...(resolvedConfigOptionsSnapshot?.reasoningEffort
? {
Expand Down Expand Up @@ -2344,8 +2356,8 @@ export function AppShell({
const chatState = useChatStore.getState();
const needsStartup =
workspaceRepository.mode === "multi" &&
project.projectWorkspaces.some(
(workspace) => workspace.startupMode !== "none",
project.projectWorkspaces.some((workspace) =>
requiresWorkspaceStartup(workspace.startupMode),
);
const existingDraft = findExistingDraft({
sessions: sessionState.sessions,
Expand Down Expand Up @@ -2383,7 +2395,9 @@ export function AppShell({
workspaceAttachments: needsStartup
? asIs?.workspaceAttachments.filter(
(_, index) =>
project.projectWorkspaces[index]?.startupMode === "none",
!requiresWorkspaceStartup(
project.projectWorkspaces[index]?.startupMode ?? "none",
),
)
: asIs?.workspaceAttachments,
});
Expand Down Expand Up @@ -2484,6 +2498,12 @@ export function AppShell({
],
);

startChatForCreatedProjectRef.current = (project) => {
void createNewProjectDraft(DEFAULT_CHAT_TITLE, project).catch((error) => {
logProjectChatStartError("Failed to start chat for new project:", error);
});
};

const activateDeferredChatSession = useCallback(
(sessionId: string) => {
const liveSessionId = resolveLiveSessionId(sessionId);
Expand Down Expand Up @@ -4979,17 +4999,20 @@ export function AppShell({
onCreatePersona={agentBuilder.create}
onStartAgentBuilderSession={agentBuilder.start}
onArchiveChat={handleArchiveChat}
onCreateProject={() => {
onCreateProject={(options) => {
if (starterTasksVisible) {
setStarterTasksAwaitingCompletion((awaiting) =>
new Set(awaiting).add("create-project"),
);
openCreateProjectDialog({
onCreated: handleStarterProjectCreated,
onCreated: (projectId) => {
handleStarterProjectCreated(projectId);
options?.onCreated?.(projectId);
},
});
return;
}
openCreateProjectDialog();
openCreateProjectDialog(options);
}}
onOpenProjectSettings={handleEditProject}
onActivateHomeSession={activateHomeSession}
Expand Down Expand Up @@ -5133,8 +5156,8 @@ export function AppShell({
requestIdentity={pendingWorkspaceName ?? undefined}
workspaces={pendingWorkspaceName?.workspaces ?? []}
requiresWorktreeSafeName={Boolean(
pendingWorkspaceName?.workspaces.some(
(workspace) => workspace.startupMode === "worktree",
pendingWorkspaceName?.workspaces.some((workspace) =>
isWorktreeStartupMode(workspace.startupMode),
),
)}
onCancel={closeWorkspaceName}
Expand Down
5 changes: 3 additions & 2 deletions src/app/SessionWindowApp.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { useCallback, useEffect, useState, type ReactNode } from "react";
import { useTranslation } from "react-i18next";
import { isWorktreeStartupMode } from "@/features/projects/api/projects";

import { runChatRuntimeStartup } from "@/app/lib/chatRuntimeStartup";
import { SessionWindowTopBar } from "@/app/ui/SessionWindowTopBar";
Expand Down Expand Up @@ -447,8 +448,8 @@ export function SessionWindowApp({
requestIdentity={workspaceName ?? undefined}
workspaces={workspaceName?.workspaces ?? []}
requiresWorktreeSafeName={Boolean(
workspaceName?.workspaces.some(
(workspace) => workspace.startupMode === "worktree",
workspaceName?.workspaces.some((workspace) =>
isWorktreeStartupMode(workspace.startupMode),
),
)}
onCancel={cancelWorkspaceNameRequest}
Expand Down
23 changes: 11 additions & 12 deletions src/features/berdctl/__tests__/commands/commands.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3533,7 +3533,7 @@ describe("projects", () => {
expect.objectContaining({
id: "ws-main",
path: "/projects/repo",
startupMode: "worktree",
startupMode: "auto-worktree",
}),
expect.objectContaining({
id: "ws-docs",
Expand All @@ -3545,9 +3545,9 @@ describe("projects", () => {
);
expect(result).toEqual({
ok: true,
mode: "worktree",
mode: "auto-worktree",
workspaces: [
{ path: "/projects/repo", startup_mode: "worktree" },
{ path: "/projects/repo", startup_mode: "auto-worktree" },
{ path: "/projects/docs", startup_mode: "none" },
],
});
Expand Down Expand Up @@ -3582,7 +3582,7 @@ describe("projects", () => {
);
});

it("rejects branch mode across different checkouts of the same repository", async () => {
it("migrates legacy branch mode to manual worktree management", async () => {
const linkedWorkspace = {
...mainWorkspace,
id: "ws-linked",
Expand Down Expand Up @@ -3619,15 +3619,14 @@ describe("projects", () => {
localBranches: ["main", "feature"],
}));

await expectCommandError(
dispatchCommand(
"projects",
{ action: "set_startup_mode", project_id: "p-1", mode: "branch" },
ctx,
),
"invalid_args",
const result = await dispatchCommand(
"projects",
{ action: "set_startup_mode", project_id: "p-1", mode: "branch" },
ctx,
);
expect(mocks.updateProject).not.toHaveBeenCalled();

expect(result).toMatchObject({ mode: "ask-worktree" });
expect(mocks.updateProject).toHaveBeenCalled();
});

it("rejects branch/worktree mode when the project has no Git workspaces", async () => {
Expand Down
7 changes: 6 additions & 1 deletion src/features/berdctl/commands/impl/getProject.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,12 @@ interface GetProjectResult {
working_dirs: string[];
workspaces: Array<{
path: string;
startup_mode: "none" | "branch" | "worktree";
startup_mode:
| "none"
| "branch"
| "worktree"
| "ask-worktree"
| "auto-worktree";
}>;
archived: boolean;
session_count: number;
Expand Down
Loading