Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
69 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
d30adb6
feat: consolidate project settings into a dedicated Projects page
z3thon Mar 17, 2026
c9f3bcb
feat(desktop): enhance projects page with workspace listings
z3thon Mar 19, 2026
44c055f
feat(desktop): polish projects settings page
z3thon Mar 19, 2026
433d3d7
fix: deduplicate allGroups query after rebase
z3thon Mar 19, 2026
85d7416
feat(desktop): project icons in settings list + detect/refresh button
z3thon Mar 19, 2026
1d49c37
fix(desktop): remove icon Remove button, keep Detect and Upload only
z3thon Mar 19, 2026
028c388
fix(desktop): show Remove button when any icon is set
z3thon Mar 19, 2026
cb8f280
fix(desktop): only show Remove icon button after manual upload
z3thon Mar 19, 2026
dfd7cea
fix(desktop): swap Detect/Remove visibility based on upload state
z3thon Mar 19, 2026
7bd787a
feat: add file tree as a draggable pane type
z3thon Mar 17, 2026
534c2f6
fix(desktop): wrap FileTreePane toolbar in div for react-dnd compat
z3thon Mar 19, 2026
b758f1f
feat(desktop): movable sidebar panels and expand button relocation
z3thon Mar 19, 2026
f68c9d7
chore(desktop): bump xterm betas and stabilize CI (#2529)
Kitenite Mar 17, 2026
5b6ee05
Adjust diff comment token color (#2522)
Kitenite Mar 17, 2026
cf171a9
fix(desktop): align tracking remote handling across git calls (#2531)
Kitenite Mar 17, 2026
b4d6476
feat(desktop): open project in dedicated window
z3thon Mar 17, 2026
152f22f
feat(desktop): multi-window with state sync and project focus mode
z3thon Mar 19, 2026
c328fca
fix(desktop): allow both sidebar tabs to be active when on separate s…
z3thon Mar 19, 2026
68c6196
fix(desktop): remove focus window restoration to prevent Chrome tabs …
z3thon Mar 19, 2026
53b5f62
feat(desktop): sync project changes across windows
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
869fd78
Merge branch 'feat/per-project-worktree-mode' into feat/projects-sett…
z3thon Mar 19, 2026
6a594a2
fix(desktop): address PR review feedback for file-tree-pane
z3thon Mar 19, 2026
5f9c120
Merge branch 'feat/file-tree-pane' into feat/open-project-in-new-window
z3thon Mar 19, 2026
1fef62b
feat(desktop): independent left/right panel toggles with dedicated bu…
z3thon Mar 19, 2026
b4e3cf4
Merge branch 'feat/file-tree-pane' into feat/open-project-in-new-window
z3thon Mar 19, 2026
96d15b2
feat(desktop): move panel toggle buttons to top nav bar
z3thon Mar 19, 2026
e4a6a30
Merge branch 'feat/file-tree-pane' into feat/open-project-in-new-window
z3thon Mar 19, 2026
f9a6c6c
fix(desktop): restore split-pane project settings page lost during merge
z3thon Mar 20, 2026
257a1ba
Merge branch 'feat/projects-settings-list' into feat/file-tree-pane
z3thon Mar 20, 2026
39566e4
Merge branch 'feat/file-tree-pane' into feat/open-project-in-new-window
z3thon Mar 20, 2026
2417163
fix(desktop): remove duplicate Projects section from settings sidebar…
z3thon Mar 20, 2026
4f5d3e9
Merge branch 'feat/projects-settings-list' into feat/file-tree-pane
z3thon Mar 20, 2026
47a4d7b
Merge branch 'feat/file-tree-pane' into feat/open-project-in-new-window
z3thon Mar 20, 2026
b824b4b
fix(desktop): remove back button from project settings (now inline in…
z3thon Mar 20, 2026
954595f
Merge branch 'feat/projects-settings-list' into feat/file-tree-pane
z3thon Mar 20, 2026
b5bf372
Merge branch 'feat/file-tree-pane' into feat/open-project-in-new-window
z3thon Mar 20, 2026
d34f945
feat(desktop): match panel toggle buttons to existing sidebar toggle …
z3thon Mar 20, 2026
746f8b3
Merge branch 'feat/file-tree-pane' into feat/open-project-in-new-window
z3thon Mar 20, 2026
c4a0324
fix(desktop): X button on workspace uses close dialog with recycle op…
z3thon Mar 20, 2026
4d6c419
Merge branch 'feat/per-project-worktree-mode' into feat/projects-sett…
z3thon Mar 20, 2026
9409aae
Merge branch 'feat/projects-settings-list' into feat/file-tree-pane
z3thon Mar 20, 2026
9785543
Merge branch 'feat/file-tree-pane' into feat/open-project-in-new-window
z3thon Mar 20, 2026
ba1c13f
fix(desktop): unregister project windows after close is confirmed
z3thon Mar 20, 2026
3e531b8
Merge remote-tracking branch 'upstream/main' into feat/per-project-wo…
z3thon Mar 20, 2026
cdd1ff4
Merge branch 'feat/per-project-worktree-mode' into feat/projects-sett…
z3thon Mar 20, 2026
41184a4
Merge branch 'feat/projects-settings-list' into feat/file-tree-pane
z3thon Mar 20, 2026
cae5110
Merge branch 'feat/file-tree-pane' into feat/open-project-in-new-window
z3thon Mar 20, 2026
7b18481
Merge remote-tracking branch 'upstream/main' into feat/per-project-wo…
z3thon Mar 20, 2026
664570c
Merge branch 'feat/per-project-worktree-mode' into feat/projects-sett…
z3thon Mar 20, 2026
03d7fab
Merge branch 'feat/projects-settings-list' into feat/file-tree-pane
z3thon Mar 20, 2026
d2a7f5e
Merge branch 'feat/file-tree-pane' into feat/open-project-in-new-window
z3thon Mar 20, 2026
d14d19d
fix(desktop): add missing settings import in workspace create procedure
z3thon Mar 20, 2026
16f2b70
Merge branch 'feat/per-project-worktree-mode' into feat/projects-sett…
z3thon Mar 20, 2026
798c166
Merge branch 'feat/projects-settings-list' into feat/file-tree-pane
z3thon Mar 20, 2026
bf62698
Merge branch 'feat/file-tree-pane' into feat/open-project-in-new-window
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
14 changes: 8 additions & 6 deletions apps/desktop/src/lib/electron-app/factories/app/setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,17 @@ export async function makeAppSetup(
await restoreWindows();
}

// If no windows were restored, create a new one
// Destroy any windows that macOS auto-restored from the previous session.
// We manage our own window lifecycle — these zombie windows would otherwise
// trigger will-navigate → shell.openExternal, opening Chrome tabs.
// destroy() is used instead of close() to prevent any navigation or events.
const existingWindows = BrowserWindow.getAllWindows();
let window: BrowserWindow;
if (existingWindows.length > 0) {
window = existingWindows[0];
} else {
window = await createWindow();
for (const win of existingWindows) {
if (!win.isDestroyed()) win.destroy();
}

let window: BrowserWindow = await createWindow();

app.on("activate", async () => {
const windows = BrowserWindow.getAllWindows();

Expand Down
7 changes: 6 additions & 1 deletion apps/desktop/src/lib/trpc/index.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,19 @@
import { createTRPCReact } from "@trpc/react-query";
import { initTRPC } from "@trpc/server";
import type { BrowserWindow } from "electron";
import superjson from "superjson";
import type { AppRouter } from "./routers";
import { NotGitRepoError } from "./routers/workspaces/utils/git";

export interface TRPCContext {
callingWindow: BrowserWindow | null;
}

/**
* Core tRPC initialization
* This provides the base router and procedure builders used by all routers
*/
const t = initTRPC.create({
const t = initTRPC.context<TRPCContext>().create({
transformer: superjson,
isServer: true,
});
Expand Down
127 changes: 126 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,20 +7,24 @@ import {
projects,
type SelectProject,
settings,
WORKTREE_MODES,
workspaceSections,
workspaces,
worktrees,
} from "@superset/local-db";
import { TRPCError } from "@trpc/server";
import { observable } from "@trpc/server/observable";
import { and, desc, eq, inArray, isNotNull, isNull, not } from "drizzle-orm";
import type { BrowserWindow } from "electron";
import { dialog } from "electron";
import { track } from "main/lib/analytics";
import { dataEmitter } from "main/lib/data-events";
import { localDb } from "main/lib/local-db";
import {
deleteProjectIcon,
saveProjectIconFromDataUrl,
} from "main/lib/project-icons";
import { windowManager } from "main/lib/window-manager";
import { getWorkspaceRuntimeRegistry } from "main/lib/workspace-runtime";
import { PROJECT_COLOR_VALUES } from "shared/constants/project-colors";
import { z } from "zod";
Expand Down Expand Up @@ -116,6 +120,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) {
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 +155,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 +1273,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,11 +1312,19 @@ 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))
.run();

dataEmitter.emit("projectChanged", {
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: Cross-window project sync is incomplete: projectChanged is emitted only from update, while other project-mutating procedures do not emit, causing stale data in other windows.

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

<comment>Cross-window project sync is incomplete: `projectChanged` is emitted only from `update`, while other project-mutating procedures do not emit, causing stale data in other windows.</comment>

<file context>
@@ -1318,6 +1320,11 @@ export const createProjectsRouter = (getWindow: () => BrowserWindow | null) => {
 					.where(eq(projects.id, input.id))
 					.run();
 
+				dataEmitter.emit("projectChanged", {
+					projectId: input.id,
+					updatedAt: new Date().toISOString(),
</file context>
Fix with Cubic

projectId: input.id,
updatedAt: new Date().toISOString(),
});

return { success: true };
}),

Expand Down Expand Up @@ -1366,7 +1418,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 +1451,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");
Comment on lines +1454 to +1457
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

Remove redundant dynamic import of existsSync.

existsSync is already imported statically at line 1 (import { existsSync, statSync } from "node:fs";). The dynamic import here is unnecessary.

🔧 Proposed fix
 			// Optionally move worktree directories to Trash
 			if (input.deleteWorktrees) {
-				const { existsSync } = await import("node:fs");
 				const { shell } = await import("electron");
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/src/lib/trpc/routers/projects/projects.ts` around lines 1454 -
1457, The dynamic import for existsSync inside the deleteWorktrees block is
redundant because existsSync is already statically imported at module top
(import { existsSync, statSync } from "node:fs"); remove the dynamic import of
existsSync and use the top-level existsSync symbol directly, keeping only the
dynamic import for electron's shell if still needed (refer to the block handling
input.deleteWorktrees and the surrounding code that currently does const {
existsSync } = await import("node:fs"); and 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(
`[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 Expand Up @@ -1431,6 +1538,12 @@ export const createProjectsRouter = (getWindow: () => BrowserWindow | null) => {

track("project_closed", { project_id: input.id });

// Close the project's dedicated window if one is open
const projectWin = windowManager.getProjectWindow(input.id);
if (projectWin && !projectWin.isDestroyed()) {
projectWin.close();
}

return { success: true, terminalWarning };
}),

Expand Down Expand Up @@ -1602,6 +1715,18 @@ export const createProjectsRouter = (getWindow: () => BrowserWindow | null) => {

return { iconUrl };
}),

onProjectChanged: publicProcedure.subscription(() => {
return observable<{ projectId: string; updatedAt: string }>((emit) => {
const onChange = (data: { projectId: string; updatedAt: string }) => {
emit.next(data);
};
dataEmitter.on("projectChanged", onChange);
return () => {
dataEmitter.off("projectChanged", onChange);
};
});
}),
});
};

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",
"**/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
16 changes: 16 additions & 0 deletions apps/desktop/src/lib/trpc/routers/ui-state/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { observable } from "@trpc/server/observable";
import { appState } from "main/lib/app-state";
import type { TabsState, ThemeState } from "main/lib/app-state/schemas";
import { hotkeysEmitter } from "main/lib/hotkeys-events";
import { tabsEmitter } from "main/lib/tabs-events";
import {
buildOverridesFromBindings,
HOTKEYS_STATE_VERSION,
Expand Down Expand Up @@ -259,8 +260,23 @@ export const createUiStateRouter = () => {
.mutation(async ({ input }) => {
appState.data.tabsState = input;
await appState.write();
tabsEmitter.emit("change", {
updatedAt: new Date().toISOString(),
});
return { success: true };
}),

subscribe: publicProcedure.subscription(() => {
return observable<{ updatedAt: string }>((emit) => {
const onChange = (data: { updatedAt: string }) => {
emit.next(data);
};
tabsEmitter.on("change", onChange);
return () => {
tabsEmitter.off("change", onChange);
};
});
}),
}),

// Theme state procedures
Expand Down
14 changes: 14 additions & 0 deletions apps/desktop/src/lib/trpc/routers/window.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import { homedir } from "node:os";
import path from "node:path";
import type { BrowserWindow } from "electron";
import { dialog } from "electron";
import { windowManager } from "main/lib/window-manager";
import { ProjectWindow } from "main/windows/project";
import { z } from "zod";
import { publicProcedure, router } from "..";

Expand Down Expand Up @@ -126,6 +128,18 @@ export const createWindowRouter = (getWindow: () => BrowserWindow | null) => {

return { canceled: false, dataUrl };
}),

openProjectInNewWindow: publicProcedure
.input(z.object({ projectId: z.string() }))
.mutation(async ({ input }) => {
// If already open, just focus the existing window
if (windowManager.focusProjectWindow(input.projectId)) {
return { focused: true, opened: false };
}
// Create a new project-focused window
await ProjectWindow(input.projectId);
return { focused: false, opened: true };
}),
Comment on lines +132 to +142
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 | 🟠 Major

Make open/focus dedup atomic for concurrent calls.

Line 136 checks existing state, then Line 140 awaits creation. Two near-simultaneous calls for the same projectId can both pass the check and open duplicates.

🛠️ Proposed in-flight dedupe guard
+const openingProjectWindows = new Map<string, Promise<void>>();
+
 export const createWindowRouter = (getWindow: () => BrowserWindow | null) => {
 	return router({
 ...
 		openProjectInNewWindow: publicProcedure
 			.input(z.object({ projectId: z.string() }))
 			.mutation(async ({ input }) => {
 				// If already open, just focus the existing window
 				if (windowManager.focusProjectWindow(input.projectId)) {
 					return { focused: true, opened: false };
 				}
+
+				const existingOpen = openingProjectWindows.get(input.projectId);
+				if (existingOpen) {
+					await existingOpen;
+					return {
+						focused: windowManager.focusProjectWindow(input.projectId),
+						opened: false,
+					};
+				}
+
 				// Create a new project-focused window
-				await ProjectWindow(input.projectId);
+				const openPromise = ProjectWindow(input.projectId)
+					.then(() => undefined)
+					.finally(() => openingProjectWindows.delete(input.projectId));
+				openingProjectWindows.set(input.projectId, openPromise);
+				await openPromise;
 				return { focused: false, opened: true };
 			}),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/src/lib/trpc/routers/window.ts` around lines 132 - 142,
openProjectInNewWindow can race: two concurrent calls can both see
focusProjectWindow false and create duplicate windows; fix by adding an
in-flight dedupe guard (e.g., a module-level Map<string,
Promise<{focused:boolean,opened:boolean}>> or mutex keyed by projectId) inside
openProjectInNewWindow so that before creating a window you check and return any
existing in-flight promise, otherwise create and store a promise that calls
windowManager.focusProjectWindow, awaits ProjectWindow(input.projectId) only if
needed, and removes the entry in finally; reference the existing
openProjectInNewWindow, windowManager.focusProjectWindow and ProjectWindow
symbols when implementing the Map-based in-flight lock so concurrent callers
share the same result and no duplicate windows are created.

});
};

Expand Down
Loading