Skip to content
Closed
Show file tree
Hide file tree
Changes from 4 commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
e7a3779
feat: add per-project worktree mode setting
z3thon Mar 17, 2026
1cf4a0f
feat(desktop): improve worktree optional UX and add workspace close
z3thon Mar 19, 2026
6867bd2
fix(desktop): worktree recycle uses Trash, sidebar refreshes on proje…
z3thon Mar 19, 2026
60c9f87
feat(desktop): add hover shortcut overlay for branch-only projects
z3thon Mar 19, 2026
5c8d783
feat(desktop): add agent status ring to branch-only project thumbnails
z3thon Mar 19, 2026
f878bcd
feat(desktop): auto-discover project favicon on open
z3thon Mar 19, 2026
797c226
fix(desktop): match status ring radius to project thumbnail (rounded)
z3thon Mar 19, 2026
6f86de9
fix(desktop): expand favicon discovery to find nested app icons
z3thon Mar 19, 2026
8dfa3ce
style(desktop): workspace count in badge tile instead of parentheses
z3thon Mar 19, 2026
5b9a80f
fix(desktop): clear review status when clicking branch-only project
z3thon Mar 19, 2026
3ecf401
fix(desktop): show worktree choice dialog on drag-and-drop project open
z3thon Mar 19, 2026
93a5305
fix(desktop): navigate to workspace after drag-and-drop project open
z3thon Mar 19, 2026
2784f60
Merge remote-tracking branch 'upstream/main' into feat/per-project-wo…
z3thon Mar 19, 2026
48da360
fix(desktop): address PR review feedback
z3thon Mar 19, 2026
c4a0324
fix(desktop): X button on workspace uses close dialog with recycle op…
z3thon Mar 20, 2026
3e531b8
Merge remote-tracking branch 'upstream/main' into feat/per-project-wo…
z3thon Mar 20, 2026
7b18481
Merge remote-tracking branch 'upstream/main' into feat/per-project-wo…
z3thon Mar 20, 2026
d14d19d
fix(desktop): add missing settings import in workspace create procedure
z3thon Mar 20, 2026
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
62 changes: 61 additions & 1 deletion apps/desktop/src/lib/trpc/routers/projects/projects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
projects,

@cubic-dev-ai cubic-dev-ai Bot Mar 19, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: cloneRepo bypasses upsertProject, so cloned projects skip the newly added favicon discovery/init behavior and diverge from other project creation flows.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/desktop/src/lib/trpc/routers/projects/projects.ts, line 155:

<comment>`cloneRepo` bypasses `upsertProject`, so cloned projects skip the newly added favicon discovery/init behavior and diverge from other project creation flows.</comment>

<file context>
@@ -131,6 +152,24 @@ function upsertProject(mainRepoPath: string, defaultBranch: string): Project {
 		.returning()
 		.get();
 
+	// Discover favicon for new project (fire-and-forget)
+	discoverAndSaveProjectIcon({
+		projectId: project.id,
</file context>
Fix with Cubic

type SelectProject,
settings,
WORKTREE_MODES,
workspaceSections,
workspaces,
worktrees,
Expand Down Expand Up @@ -1230,6 +1231,7 @@ export const createProjectsRouter = (getWindow: () => BrowserWindow | null) => {
worktreeBaseDir: z.string().nullable().optional(),
hideImage: z.boolean().optional(),
defaultApp: z.enum(EXTERNAL_APPS).nullable().optional(),
worktreeMode: z.enum(WORKTREE_MODES).nullable().optional(),
}),
}),
)
Expand Down Expand Up @@ -1268,6 +1270,9 @@ export const createProjectsRouter = (getWindow: () => BrowserWindow | null) => {
...(input.patch.defaultApp !== undefined && {
defaultApp: input.patch.defaultApp,
}),
...(input.patch.worktreeMode !== undefined && {
worktreeMode: input.patch.worktreeMode,
}),
lastOpenedAt: Date.now(),
})
.where(eq(projects.id, input.id))
Expand Down Expand Up @@ -1366,7 +1371,12 @@ export const createProjectsRouter = (getWindow: () => BrowserWindow | null) => {
}),

close: publicProcedure
.input(z.object({ id: z.string() }))
.input(
z.object({
id: z.string(),
deleteWorktrees: z.boolean().optional().default(false),
}),
)
.mutation(async ({ input }) => {
const project = localDb
.select()
Expand Down Expand Up @@ -1394,6 +1404,56 @@ export const createProjectsRouter = (getWindow: () => BrowserWindow | null) => {

const closedWorkspaceIds = projectWorkspaces.map((w) => w.id);

// Optionally move worktree directories to Trash
if (input.deleteWorktrees) {
const { existsSync } = await import("node:fs");
const { shell } = await import("electron");

// Collect worktree paths from both the worktrees table and workspace records
const projectWorktrees = localDb
.select()
.from(worktrees)
.where(eq(worktrees.projectId, input.id))
.all();

const worktreePaths = new Set<string>(
projectWorktrees.map((wt) => wt.path),
);

for (const ws of projectWorkspaces) {
if (ws.type === "worktree" && ws.worktreeId) {
const wt = localDb
.select()
.from(worktrees)
.where(eq(worktrees.id, ws.worktreeId))
.get();
if (wt?.path) {
worktreePaths.add(wt.path);
}
}
}

for (const wtPath of worktreePaths) {
if (!existsSync(wtPath)) continue;
try {
await shell.trashItem(wtPath);
} catch (error) {
console.error(

@cubic-dev-ai cubic-dev-ai Bot Mar 19, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Worktree trash failures are swallowed, but project/worktree metadata is still deleted and the mutation returns success, causing silent partial cleanup and potential orphaned directories.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/desktop/src/lib/trpc/routers/projects/projects.ts, line 1441:

<comment>Worktree trash failures are swallowed, but project/worktree metadata is still deleted and the mutation returns success, causing silent partial cleanup and potential orphaned directories.</comment>

<file context>
@@ -1399,6 +1404,56 @@ export const createProjectsRouter = (getWindow: () => BrowserWindow | null) => {
+						try {
+							await shell.trashItem(wtPath);
+						} catch (error) {
+							console.error(
+								`[projects/close] Failed to trash worktree ${wtPath}:`,
+								error,
</file context>
Fix with Cubic

`[projects/close] Failed to trash worktree ${wtPath}:`,
error,
);
}
}

// Clean up stale git worktree references
try {
const git = await getSimpleGitWithShellPath(project.mainRepoPath);
await git.raw(["worktree", "prune"]);
} catch (error) {
console.error("[projects/close] Failed to prune worktrees:", error);
}
}

if (closedWorkspaceIds.length > 0) {
localDb
.delete(workspaces)
Expand Down
21 changes: 21 additions & 0 deletions apps/desktop/src/lib/trpc/routers/settings/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
settings,
TERMINAL_LINK_BEHAVIORS,
type TerminalPreset,
WORKTREE_MODES,
} from "@superset/local-db";
import {
AGENT_PRESET_COMMANDS,
Expand Down Expand Up @@ -749,6 +750,26 @@ export const createSettingsRouter = () => {
return { success: true };
}),

getWorktreeMode: publicProcedure.query(() => {
const row = getSettings();
return row.worktreeMode ?? "always";
}),

setWorktreeMode: publicProcedure
.input(z.object({ mode: z.enum(WORKTREE_MODES) }))
.mutation(({ input }) => {
localDb
.insert(settings)
.values({ id: 1, worktreeMode: input.mode })
.onConflictDoUpdate({
target: settings.id,
set: { worktreeMode: input.mode },
})
.run();

return { success: true };
}),

getOpenLinksInApp: publicProcedure.query(() => {
const row = getSettings();
return row.openLinksInApp ?? DEFAULT_OPEN_LINKS_IN_APP;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,7 @@ export const createCreateProcedures = () => {
baseBranch: z.string().optional(),
useExistingBranch: z.boolean().optional(),
applyPrefix: z.boolean().optional().default(true),
useWorktree: z.boolean().optional(),
}),
)
.mutation(async ({ input }) => {
Expand All @@ -307,6 +308,87 @@ export const createCreateProcedures = () => {
throw new Error(`Project ${input.projectId} not found`);
}

// Resolve effective worktree mode
const globalSettings = localDb.select().from(settings).get();
const effectiveWorktreeMode =
project.worktreeMode ?? globalSettings?.worktreeMode ?? "always";

// If worktrees are disabled (or user explicitly chose no worktree),
// open directly in the main repo
if (
effectiveWorktreeMode === "disabled" ||
input.useWorktree === false

@cubic-dev-ai cubic-dev-ai Bot Mar 17, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: always worktree policy can be bypassed because useWorktree: false is accepted unconditionally, allowing branch workspace creation even when mode resolves to always.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/desktop/src/lib/trpc/routers/workspaces/procedures/create.ts, line 320:

<comment>`always` worktree policy can be bypassed because `useWorktree: false` is accepted unconditionally, allowing branch workspace creation even when mode resolves to `always`.</comment>

<file context>
@@ -307,6 +308,87 @@ export const createCreateProcedures = () => {
+				// open directly in the main repo
+				if (
+					effectiveWorktreeMode === "disabled" ||
+					input.useWorktree === false
+				) {
+					const branch =
</file context>
Suggested change
input.useWorktree === false
+ (effectiveWorktreeMode === "optional" && input.useWorktree === false)
Fix with Cubic

) {
const branch =
input.branchName?.trim() ||
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
(await getCurrentBranch(project.mainRepoPath));
if (!branch) {
throw new Error("Could not determine current branch");
}

const existing = getBranchWorkspace(input.projectId);

if (existing) {
if (existing.branch !== branch) {
localDb
.update(workspaces)
.set({ branch })
.where(eq(workspaces.id, existing.id))
.run();
}
touchWorkspace(existing.id);
setLastActiveWorkspace(existing.id);
return {
workspace: {
...existing,
branch,
lastOpenedAt: Date.now(),
},
worktreePath: project.mainRepoPath,
projectId: project.id,
isInitializing: false,
wasExisting: true,
};
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

const maxTabOrder = getMaxProjectChildTabOrder(input.projectId);
const workspace = localDb
.insert(workspaces)
.values({
projectId: input.projectId,
type: "branch",
branch,
name: input.name ?? branch,
tabOrder: maxTabOrder + 1,
})
.onConflictDoNothing()
.returning()
.all();

const ws = workspace[0] ?? getBranchWorkspace(input.projectId);
if (!ws) {
throw new Error("Failed to create or find branch workspace");
}

setLastActiveWorkspace(ws.id);
activateProject(project);

track("workspace_created", {
workspace_id: ws.id,
project_id: project.id,
type: "branch",
});

return {
workspace: ws,
initialCommands: null,
worktreePath: project.mainRepoPath,
projectId: project.id,
isInitializing: false,
wasExisting: false,
};
Comment on lines +358 to +393

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Preserve wasExisting if the insert loses the race.

After onConflictDoNothing(), workspace[0] ?? getBranchWorkspace(input.projectId) can resolve to a row created by another request, but this branch still returns wasExisting: false and emits workspace_created. openMainRepoWorkspace below already handles this by deriving wasExisting from the insert result.

Suggested adjustment
-					const workspace = localDb
+					const insertResult = localDb
 						.insert(workspaces)
 						.values({
 							projectId: input.projectId,
 							type: "branch",
 							branch,
 							name: input.name ?? branch,
 							tabOrder: maxTabOrder + 1,
 						})
 						.onConflictDoNothing()
 						.returning()
 						.all();
 
-					const ws = workspace[0] ?? getBranchWorkspace(input.projectId);
+					const wasExisting = insertResult.length === 0;
+					const ws =
+						insertResult[0] ?? getBranchWorkspace(input.projectId);
 					if (!ws) {
 						throw new Error("Failed to create or find branch workspace");
 					}
@@
-					track("workspace_created", {
-						workspace_id: ws.id,
-						project_id: project.id,
-						type: "branch",
-					});
+					if (!wasExisting) {
+						track("workspace_created", {
+							workspace_id: ws.id,
+							project_id: project.id,
+							type: "branch",
+						});
+					}
@@
-						wasExisting: false,
+						wasExisting,
 					};
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const maxTabOrder = getMaxProjectChildTabOrder(input.projectId);
const workspace = localDb
.insert(workspaces)
.values({
projectId: input.projectId,
type: "branch",
branch,
name: input.name ?? branch,
tabOrder: maxTabOrder + 1,
})
.onConflictDoNothing()
.returning()
.all();
const ws = workspace[0] ?? getBranchWorkspace(input.projectId);
if (!ws) {
throw new Error("Failed to create or find branch workspace");
}
setLastActiveWorkspace(ws.id);
activateProject(project);
track("workspace_created", {
workspace_id: ws.id,
project_id: project.id,
type: "branch",
});
return {
workspace: ws,
initialCommands: null,
worktreePath: project.mainRepoPath,
projectId: project.id,
isInitializing: false,
wasExisting: false,
};
const maxTabOrder = getMaxProjectChildTabOrder(input.projectId);
const insertResult = localDb
.insert(workspaces)
.values({
projectId: input.projectId,
type: "branch",
branch,
name: input.name ?? branch,
tabOrder: maxTabOrder + 1,
})
.onConflictDoNothing()
.returning()
.all();
const wasExisting = insertResult.length === 0;
const ws =
insertResult[0] ?? getBranchWorkspace(input.projectId);
if (!ws) {
throw new Error("Failed to create or find branch workspace");
}
setLastActiveWorkspace(ws.id);
activateProject(project);
if (!wasExisting) {
track("workspace_created", {
workspace_id: ws.id,
project_id: project.id,
type: "branch",
});
}
return {
workspace: ws,
initialCommands: null,
worktreePath: project.mainRepoPath,
projectId: project.id,
isInitializing: false,
wasExisting,
};
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/src/lib/trpc/routers/workspaces/procedures/create.ts` around
lines 354 - 389, The insert with onConflictDoNothing() can return an empty array
if another request created the workspace, but the code unconditionally sets
wasExisting: false and still emits track("workspace_created"); update the logic
around getMaxProjectChildTabOrder, the
localDb.insert(...).onConflictDoNothing().returning().all() result, and the ws =
workspace[0] ?? getBranchWorkspace(...) branch to detect whether the insert
returned a row (wasExisting = false) or we fell back to getBranchWorkspace
(wasExisting = true); set wasExisting accordingly and only call
track("workspace_created", ...) when the insert actually created the row (i.e.,
when workspace[0] exists).

}

let existingBranchName: string | undefined;
if (input.useExistingBranch) {
existingBranchName = input.branchName?.trim();
Expand All @@ -332,7 +414,6 @@ export const createCreateProcedures = () => {

let branchPrefix: string | undefined;
if (input.applyPrefix) {
const globalSettings = localDb.select().from(settings).get();
const projectOverrides = project.branchPrefixMode != null;
const prefixMode = projectOverrides
? project.branchPrefixMode
Expand Down
30 changes: 23 additions & 7 deletions apps/desktop/src/lib/trpc/routers/workspaces/procedures/delete.ts
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,7 @@ export const createDeleteProcedures = () => {
id: z.string(),
deleteLocalBranch: z.boolean().optional(),
force: z.boolean().optional(),
trash: z.boolean().optional(),
}),
)
.mutation(async ({ input }) => {
Expand Down Expand Up @@ -252,13 +253,28 @@ export const createDeleteProcedures = () => {
await workspaceInitManager.acquireProjectLock(project.id);

try {
const removeResult = await removeWorktreeFromDisk({
mainRepoPath: project.mainRepoPath,
worktreePath: worktree.path,
});
if (!removeResult.success) {
clearWorkspaceDeletingStatus(input.id);
return removeResult;
if (input.trash) {
// Move to Trash (recoverable) instead of permanent delete
const { existsSync } = await import("node:fs");
if (existsSync(worktree.path)) {
const { shell } = await import("electron");
await shell.trashItem(worktree.path);

@cubic-dev-ai cubic-dev-ai Bot Mar 19, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: Trash-mode deletion lacks failure cleanup; thrown errors can leave deletingAt stuck and block future deletes.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/desktop/src/lib/trpc/routers/workspaces/procedures/delete.ts, line 261:

<comment>Trash-mode deletion lacks failure cleanup; thrown errors can leave `deletingAt` stuck and block future deletes.</comment>

<file context>
@@ -252,13 +253,28 @@ export const createDeleteProcedures = () => {
+							const { existsSync } = await import("node:fs");
+							if (existsSync(worktree.path)) {
+								const { shell } = await import("electron");
+								await shell.trashItem(worktree.path);
+							}
+							// Clean up stale git worktree references
</file context>
Fix with Cubic

@cubic-dev-ai cubic-dev-ai Bot Mar 19, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: Trash-mode delete lacks failure cleanup, so thrown async errors can leave deletingAt set and block future deletion attempts.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/desktop/src/lib/trpc/routers/workspaces/procedures/delete.ts, line 261:

<comment>Trash-mode delete lacks failure cleanup, so thrown async errors can leave `deletingAt` set and block future deletion attempts.</comment>

<file context>
@@ -252,13 +253,28 @@ export const createDeleteProcedures = () => {
+							const { existsSync } = await import("node:fs");
+							if (existsSync(worktree.path)) {
+								const { shell } = await import("electron");
+								await shell.trashItem(worktree.path);
+							}
+							// Clean up stale git worktree references
</file context>
Fix with Cubic

}
// Clean up stale git worktree references
const { getSimpleGitWithShellPath } = await import(
"../utils/git-client"
);
const git = await getSimpleGitWithShellPath(project.mainRepoPath);
await git.raw(["worktree", "prune"]);
Comment on lines +256 to +268

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Redundant import and missing error handling in trash path.

  1. existsSync is already imported at line 1 - the dynamic import on line 258 is unnecessary.
  2. Unlike the non-trash path (lines 270-277) which handles failures gracefully, the trash path has no error handling. If shell.trashItem or git worktree prune fails, the procedure silently continues and deletes the workspace record, potentially leaving inconsistent state.
Proposed fix
 					if (input.trash) {
 						// Move to Trash (recoverable) instead of permanent delete
-						const { existsSync } = await import("node:fs");
 						if (existsSync(worktree.path)) {
-							const { shell } = await import("electron");
-							await shell.trashItem(worktree.path);
+							try {
+								const { shell } = await import("electron");
+								await shell.trashItem(worktree.path);
+							} catch (error) {
+								console.error(
+									`[workspace/delete] Failed to move worktree to trash:`,
+									error instanceof Error ? error.message : String(error),
+								);
+								clearWorkspaceDeletingStatus(input.id);
+								return {
+									success: false,
+									error: `Failed to move worktree to trash: ${error instanceof Error ? error.message : String(error)}`,
+								};
+							}
 						}
 						// Clean up stale git worktree references
-						const { getSimpleGitWithShellPath } = await import(
-							"../utils/git-client"
-						);
-						const git = await getSimpleGitWithShellPath(project.mainRepoPath);
-						await git.raw(["worktree", "prune"]);
+						try {
+							const { getSimpleGitWithShellPath } = await import(
+								"../utils/git-client"
+							);
+							const git = await getSimpleGitWithShellPath(project.mainRepoPath);
+							await git.raw(["worktree", "prune"]);
+						} catch (error) {
+							// Non-blocking: prune failure shouldn't prevent workspace deletion
+							console.warn(
+								`[workspace/delete] git worktree prune failed (non-blocking):`,
+								error instanceof Error ? error.message : String(error),
+							);
+						}
 					} else {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/src/lib/trpc/routers/workspaces/procedures/delete.ts` around
lines 256 - 268, Remove the redundant dynamic import of existsSync (use the
top-level existsSync already imported) and wrap the trash-path operations
(shell.trashItem and the subsequent getSimpleGitWithShellPath +
git.raw(["worktree","prune"])) in a try/catch; on error log the failure (using
the existing logger) and abort/throw a TRPC-friendly error so the workspace
record is not deleted, mirroring the non-trash path’s graceful failure handling.

Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
} else {
const removeResult = await removeWorktreeFromDisk({
mainRepoPath: project.mainRepoPath,
worktreePath: worktree.path,
});
if (!removeResult.success) {
clearWorkspaceDeletingStatus(input.id);
return removeResult;
}
}
} finally {
workspaceInitManager.releaseProjectLock(project.id);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,7 @@ export const createQueryProcedures = () => {
mainRepoPath: string;
hideImage: boolean;
iconUrl: string | null;
worktreeMode: string | null;

@cubic-dev-ai cubic-dev-ai Bot Mar 19, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: worktreeMode is typed as string | null in getAllGrouped response shape, widening the tRPC contract and losing strict WorktreeMode union guarantees.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/desktop/src/lib/trpc/routers/workspaces/procedures/query.ts, line 155:

<comment>`worktreeMode` is typed as `string | null` in `getAllGrouped` response shape, widening the tRPC contract and losing strict `WorktreeMode` union guarantees.</comment>

<file context>
@@ -152,6 +152,7 @@ export const createQueryProcedures = () => {
 						mainRepoPath: string;
 						hideImage: boolean;
 						iconUrl: string | null;
+						worktreeMode: string | null;
 					};
 					workspaces: WorkspaceItem[];
</file context>
Fix with Cubic

};
workspaces: WorkspaceItem[];
sections: SectionItem[];
Expand Down Expand Up @@ -184,6 +185,7 @@ export const createQueryProcedures = () => {
mainRepoPath: project.mainRepoPath,
hideImage: project.hideImage ?? false,
iconUrl: project.iconUrl ?? null,
worktreeMode: project.worktreeMode ?? null,
},
workspaces: [],
sections: projectSections,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,10 @@ export function NewWorkspaceModalContent({
<PromptGroup
projectId={draft.selectedProjectId}
selectedProject={selectedProject}
recentProjects={recentProjects.filter((project) => Boolean(project.id))}
recentProjects={recentProjects.filter(
(project) =>
Boolean(project.id) && project.worktreeMode !== "disabled",

@cubic-dev-ai cubic-dev-ai Bot Mar 19, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: Filtering out worktreeMode === "disabled" projects from picker options introduces a behavior regression and can create selected-value/options mismatch in the New Workspace modal.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/desktop/src/renderer/components/NewWorkspaceModal/components/NewWorkspaceModalContent/NewWorkspaceModalContent.tsx, line 89:

<comment>Filtering out `worktreeMode === "disabled"` projects from picker options introduces a behavior regression and can create selected-value/options mismatch in the New Workspace modal.</comment>

<file context>
@@ -84,7 +84,10 @@ export function NewWorkspaceModalContent({
-				recentProjects={recentProjects.filter((project) => Boolean(project.id))}
+				recentProjects={recentProjects.filter(
+					(project) =>
+						Boolean(project.id) && project.worktreeMode !== "disabled",
+				)}
 				onSelectProject={(selectedProjectId) =>
</file context>
Suggested change
Boolean(project.id) && project.worktreeMode !== "disabled",
Boolean(project.id),
Fix with Cubic

)}
onSelectProject={(selectedProjectId) =>
updateDraft({ selectedProjectId })
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -471,6 +471,11 @@ function PromptGroupInner({
const { data: globalBranchPrefix } =
electronTrpc.settings.getBranchPrefix.useQuery();
const { data: gitInfo } = electronTrpc.settings.getGitInfo.useQuery();
const { data: globalWorktreeMode } =
electronTrpc.settings.getWorktreeMode.useQuery();

const effectiveWorktreeMode =
project?.worktreeMode ?? globalWorktreeMode ?? "always";

const resolvedPrefix = useMemo(() => {
const projectOverrides = project?.branchPrefixMode != null;
Expand Down Expand Up @@ -637,6 +642,9 @@ function PromptGroupInner({
? sanitizeBranchNameWithMaxLength(branchName.trim())
: branchSlug) || undefined,
baseBranch: baseBranch || undefined,
...(effectiveWorktreeMode === "disabled" && {
useWorktree: false,
}),
Comment on lines +719 to +721

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check if the "optional" mode toggle UI is implemented elsewhere in NewWorkspaceModal
rg -n "optional|useWorktree|worktreeMode" --type=tsx apps/desktop/src/renderer/components/NewWorkspaceModal/ -C 3

# Also check for any toggle component that might be conditionally rendered
ast-grep --pattern 'effectiveWorktreeMode === "optional"'

Repository: superset-sh/superset

Length of output: 91


🏁 Script executed:

#!/bin/bash
# Check if the "optional" mode toggle UI is implemented
find apps/desktop/src/renderer/components/NewWorkspaceModal -name "*.tsx" -o -name "*.ts" | head -20

Repository: superset-sh/superset

Length of output: 1429


🏁 Script executed:

#!/bin/bash
# Search for worktreeMode and optional handling in the files
rg -n "optional|useWorktree|worktreeMode" apps/desktop/src/renderer/components/NewWorkspaceModal/ -C 3

Repository: superset-sh/superset

Length of output: 4883


🏁 Script executed:

#!/bin/bash
# Read the PromptGroup.tsx file to see the context around lines 645-647
wc -l apps/desktop/src/renderer/components/NewWorkspaceModal/components/PromptGroup/PromptGroup.tsx

Repository: superset-sh/superset

Length of output: 161


🏁 Script executed:

#!/bin/bash
# Read the context around lines 645-647 in PromptGroup.tsx
sed -n '630,670p' apps/desktop/src/renderer/components/NewWorkspaceModal/components/PromptGroup/PromptGroup.tsx | cat -n

Repository: superset-sh/superset

Length of output: 1388


🏁 Script executed:

#!/bin/bash
# Search for any state related to worktree toggle or useWorktree variable assignments
rg -n "useState|useWorktree.*=" apps/desktop/src/renderer/components/NewWorkspaceModal/components/PromptGroup/PromptGroup.tsx | head -30

Repository: superset-sh/superset

Length of output: 429


🏁 Script executed:

#!/bin/bash
# Check if there's a toggle component for worktree mode selection
rg -n "Toggle|Checkbox|Switch|optional" apps/desktop/src/renderer/components/NewWorkspaceModal/components/PromptGroup/PromptGroup.tsx

Repository: superset-sh/superset

Length of output: 170


🏁 Script executed:

#!/bin/bash
# Check the PromptGroupAdvancedOptions component which might have the worktree toggle
cat -n apps/desktop/src/renderer/components/NewWorkspaceModal/components/PromptGroup/components/PromptGroupAdvancedOptions/PromptGroupAdvancedOptions.tsx

Repository: superset-sh/superset

Length of output: 8443


🏁 Script executed:

#!/bin/bash
# Check for any "optional" worktree mode handling throughout the entire NewWorkspaceModal
rg -n "optional" apps/desktop/src/renderer/components/NewWorkspaceModal/ -A 5 -B 5

Repository: superset-sh/superset

Length of output: 2991


🏁 Script executed:

#!/bin/bash
# Check the full useState declarations to understand what state is tracked
sed -n '40,500p' apps/desktop/src/renderer/components/NewWorkspaceModal/components/PromptGroup/PromptGroup.tsx | grep -n "useState\|const \[" | head -20

Repository: superset-sh/superset

Length of output: 428


🏁 Script executed:

#!/bin/bash
# Check the draft context to see if worktree choice is stored there
cat -n apps/desktop/src/renderer/components/NewWorkspaceModal/NewWorkspaceModalDraftContext.tsx | head -100

Repository: superset-sh/superset

Length of output: 3420


🏁 Script executed:

#!/antml:bin/bash
# Search for the mutation definition to understand what useWorktree expects
rg -n "createWorkspace|useWorktree" apps/desktop/src/renderer/components/NewWorkspaceModal/ -B 3 -A 3

Repository: superset-sh/superset

Length of output: 8755


🏁 Script executed:

#!/bin/bash
# Check if there's any documentation or PR info about worktree modes
rg -n "worktreeMode.*optional|optional.*worktree" --type=md --type=txt apps/desktop/

Repository: superset-sh/superset

Length of output: 398


🏁 Script executed:

#!/bin/bash
# Search for the useCreateWorkspace mutation definition to understand what happens when useWorktree is not passed
fd -t f "useCreateWorkspace" apps/desktop/src/renderer/react-query/workspaces/ | head -5

Repository: superset-sh/superset

Length of output: 134


🏁 Script executed:

#!/bin/bash
# Read the useCreateWorkspace hook to understand the mutation schema
cat apps/desktop/src/renderer/react-query/workspaces/useCreateWorkspace.ts

Repository: superset-sh/superset

Length of output: 4905


🏁 Script executed:

#!/bin/bash
# Check if there are any tests or comments about worktree mode "optional" behavior
rg -n "optional" apps/desktop/src/renderer/components/NewWorkspaceModal/ -B 2 -A 2

Repository: superset-sh/superset

Length of output: 1442


🏁 Script executed:

#!/bin/bash
# Find the tRPC workspaces router to understand the mutation schema
find apps/desktop -name "*.ts" -o -name "*.tsx" | xargs grep -l "workspaces.*create.*procedure\|workspaces.*create.*router" | head -5

Repository: superset-sh/superset

Length of output: 46


🏁 Script executed:

#!/bin/bash
# Search for worktree-related code in the backend/shared types
rg -n "worktreeMode.*optional\|optional.*worktree" apps/desktop/ --type=ts --type=tsx -B 5 -A 5

Repository: superset-sh/superset

Length of output: 91


🏁 Script executed:

#!/antml/bash
# Check the trpc-electron router definition
find apps -name "*router*" -o -name "*trpc*" | grep -E "workspaces|create" | head -10

Repository: superset-sh/superset

Length of output: 46


🏁 Script executed:

#!/bin/bash
# Search for where the workspaces.create mutation is defined
rg -n "workspaces\.create\|\.create\(" apps/desktop/src --type ts | grep -i router | head -10

Repository: superset-sh/superset

Length of output: 46


🏁 Script executed:

#!/bin/bash
# Look for trpc router or workspace routes
find apps/desktop -type f -name "*.ts" | xargs grep -l "workspaces.*create" | head -10

Repository: superset-sh/superset

Length of output: 740


🏁 Script executed:

#!/bin/bash
# Search for the electron trpc definition
rg -n "electronTrpc\|trpc-electron" apps/desktop/src -l | head -10

Repository: superset-sh/superset

Length of output: 46


🏁 Script executed:

#!/bin/bash
# Read the create procedure to understand what parameters it accepts
cat apps/desktop/src/lib/trpc/routers/workspaces/procedures/create.ts

Repository: superset-sh/superset

Length of output: 29567


🏁 Script executed:

#!/bin/bash
# Search for any recent changes or TODOs related to optional worktree mode
rg -n "TODO|FIXME|optional.*worktree\|worktree.*optional" apps/desktop/src/renderer/components/NewWorkspaceModal/ -B 2 -A 2

Repository: superset-sh/superset

Length of output: 46


🏁 Script executed:

#!/bin/bash
# Check if there's an "always" mode to understand the pattern
rg -n "always\|optional\|disabled" apps/desktop/src/lib/trpc/routers/workspaces/procedures/create.ts | head -20

Repository: superset-sh/superset

Length of output: 46


Add worktree toggle UI for "optional" mode.

When effectiveWorktreeMode === "optional", the modal should display a toggle allowing the user to choose whether to create a worktree. Currently, "optional" mode falls through without presenting this choice to the user—worktrees are created by default without user control.

Implement:

  1. Add a useWorktree field to NewWorkspaceModalDraft to track the user's choice
  2. Render a toggle in the advanced options when mode is "optional"
  3. Pass the user's choice to the mutation payload
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/desktop/src/renderer/components/NewWorkspaceModal/components/PromptGroup/PromptGroup.tsx`
around lines 645 - 647, The UI currently forces worktrees on/off without letting
users choose when effectiveWorktreeMode === "optional"; add a boolean
useWorktree to the NewWorkspaceModalDraft type/state, render a toggle switch in
PromptGroup's advanced options when effectiveWorktreeMode === "optional" (use
the same UI pattern as other advanced options), bind it to the draft.useWorktree
state so the user can toggle, and ensure the create workspace mutation payload
(the function that submits the draft in PromptGroup or its submit handler)
includes draft.useWorktree instead of implicitly assuming true; update any
defaulting logic so only when effectiveWorktreeMode === "disabled" you set
useWorktree: false automatically.

},
{
agentLaunchRequest: launchRequest ?? undefined,
Expand All @@ -661,6 +669,7 @@ function PromptGroupInner({
convertBlobUrlToDataUrl,
createFromPr,
createWorkspace,
effectiveWorktreeMode,
linkedPR,
projectId,
runAsyncAction,
Expand Down Expand Up @@ -791,7 +800,7 @@ function PromptGroupInner({
)}
<PromptInputTextarea
autoFocus
placeholder="What do you want to do?"
placeholder="What do you want to do? (optional)"
className="min-h-10"
value={prompt}
onChange={(e) => updateDraft({ prompt: e.target.value })}
Expand Down Expand Up @@ -903,9 +912,11 @@ function PromptGroupInner({
)}
</AnimatePresence>
</div>
<span className="text-[11px] text-muted-foreground/50">
{modKey}+↵ to create
</span>
<div className="flex items-center gap-2">
<span className="text-[11px] text-muted-foreground/50">
{modKey}+↵ to create
</span>
</div>
</div>
</div>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { useWorktreeChoiceDialogStore } from "renderer/stores/worktree-choice-dialog";
import { WorktreeChoiceDialog } from "./WorktreeChoiceDialog";

export function ConnectedWorktreeChoiceDialog() {
const { isOpen, projectName, onChoice, close } =
useWorktreeChoiceDialogStore();

return (
<WorktreeChoiceDialog
projectName={projectName}
open={isOpen}
onOpenChange={(open) => {
if (!open) close();
}}
onChoice={(enableWorktrees) => {
onChoice?.(enableWorktrees);
close();
}}
/>
);
}
Loading