Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
101 changes: 100 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,
Copy link
Copy Markdown
Contributor

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

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 @@ -116,6 +117,27 @@ function upsertProject(mainRepoPath: string, defaultBranch: string): Project {
.set({ lastOpenedAt: Date.now(), defaultBranch })
.where(eq(projects.id, existing.id))
.run();

// Discover favicon if no icon is set yet (fire-and-forget)
if (!existing.iconUrl) {
Copy link
Copy Markdown
Contributor

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

Choose a reason for hiding this comment

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

P2: Auto-discovery treats iconUrl: null as missing and re-runs after user removal, so cleared project icons are not persistent.

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 122:

<comment>Auto-discovery treats `iconUrl: null` as missing and re-runs after user removal, so cleared project icons are not persistent.</comment>

<file context>
@@ -117,6 +117,27 @@ function upsertProject(mainRepoPath: string, defaultBranch: string): Project {
 			.run();
+
+		// Discover favicon if no icon is set yet (fire-and-forget)
+		if (!existing.iconUrl) {
+			discoverAndSaveProjectIcon({
+				projectId: existing.id,
</file context>
Fix with Cubic

discoverAndSaveProjectIcon({
projectId: existing.id,
repoPath: mainRepoPath,
})
.then((iconUrl) => {
if (iconUrl) {
localDb
.update(projects)
.set({ iconUrl })
.where(eq(projects.id, existing.id))
.run();
}
})
.catch((err) => {
console.error("[upsertProject] Favicon discovery failed:", err);
});
}

return { ...existing, lastOpenedAt: Date.now(), defaultBranch };
}

Expand All @@ -130,6 +152,24 @@ function upsertProject(mainRepoPath: string, defaultBranch: string): Project {
.returning()
.get();

// Discover favicon for new project (fire-and-forget)
discoverAndSaveProjectIcon({
projectId: project.id,
repoPath: mainRepoPath,
})
.then((iconUrl) => {
if (iconUrl) {
localDb
.update(projects)
.set({ iconUrl })
.where(eq(projects.id, project.id))
.run();
}
})
.catch((err) => {
console.error("[upsertProject] Favicon discovery failed:", err);
});

return project;
}

Expand Down Expand Up @@ -1230,6 +1270,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 +1309,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 +1410,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 +1443,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(
Copy link
Copy Markdown
Contributor

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

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
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@ import {
saveProjectIconFromFile,
} from "main/lib/project-icons";

/** Common favicon file names to search for in project roots */
/** Common favicon file names to search for in project roots and subdirectories */
const FAVICON_PATTERNS = [
// Root level
"favicon.ico",
"favicon.png",
"favicon.svg",
Expand All @@ -17,6 +18,7 @@ const FAVICON_PATTERNS = [
"icon.svg",
".github/logo.png",
".github/logo.svg",
// Common static/public directories
"public/favicon.ico",
"public/favicon.png",
"public/favicon.svg",
Expand All @@ -28,6 +30,19 @@ const FAVICON_PATTERNS = [
"assets/favicon.ico",
"assets/favicon.png",
"assets/icon.png",
// Next.js / app directory patterns (up to 2 levels deep)
"app/favicon.ico",
"app/icon.png",
"app/icon.svg",
"**/app/favicon.ico",
"**/app/icon.png",
"**/app/icon.svg",
// Deeper public directories in monorepos/nested projects
"**/public/favicon.ico",
Copy link
Copy Markdown
Contributor

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

Choose a reason for hiding this comment

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

P2: Selecting matches[0] is now fragile because fast-glob does not guarantee pattern-priority ordering, so newly added recursive patterns can cause incorrect icon choice.

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/utils/favicon-discovery.ts, line 41:

<comment>Selecting `matches[0]` is now fragile because fast-glob does not guarantee pattern-priority ordering, so newly added recursive patterns can cause incorrect icon choice.</comment>

<file context>
@@ -28,6 +30,19 @@ const FAVICON_PATTERNS = [
+	"**/app/icon.png",
+	"**/app/icon.svg",
+	// Deeper public directories in monorepos/nested projects
+	"**/public/favicon.ico",
+	"**/public/favicon.png",
+	"**/public/favicon.svg",
</file context>
Fix with Cubic

"**/public/favicon.png",
"**/public/favicon.svg",
"**/public/logo.png",
"**/public/logo.svg",
];

/** Max file size for discovered favicons: 256KB */
Expand All @@ -48,6 +63,7 @@ export async function discoverAndSaveProjectIcon({
const matches = await fg(FAVICON_PATTERNS, {
cwd: repoPath,
absolute: true,
deep: 4,
ignore: ["**/node_modules/**", "**/.git/**", "**/dist/**", "**/build/**"],
onlyFiles: true,
});
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 @@ -750,6 +751,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
@@ -1,4 +1,4 @@
import { projects, workspaces, worktrees } from "@superset/local-db";
import { projects, settings, workspaces, worktrees } from "@superset/local-db";
import { and, eq, isNull, not } from "drizzle-orm";
import { track } from "main/lib/analytics";
import { localDb } from "main/lib/local-db";
Expand Down Expand Up @@ -294,6 +294,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 @@ -306,6 +307,92 @@ 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
Copy link
Copy Markdown
Contributor

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

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");
}

// Checkout the target branch so the main repo matches the workspace
if (input.branchName?.trim()) {
await safeCheckoutBranch(project.mainRepoPath, 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 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);
Copy link
Copy Markdown
Contributor

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

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

Copy link
Copy Markdown
Contributor

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

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 @@ -153,6 +153,7 @@ export const createQueryProcedures = () => {
mainRepoPath: string;
hideImage: boolean;
iconUrl: string | null;
worktreeMode: string | null;
Copy link
Copy Markdown
Contributor

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

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 @@ -185,6 +186,7 @@ export const createQueryProcedures = () => {
mainRepoPath: project.mainRepoPath,
hideImage: project.hideImage ?? false,
iconUrl: project.iconUrl ?? null,
worktreeMode: project.worktreeMode ?? null,
},
workspaces: [],
sections: projectSections,
Expand Down
Loading