feat(desktop): open project in dedicated window#2542
Conversation
Add a `worktreeMode` setting (global + per-project override) with three values: "always" (current behavior), "optional" (user chooses per workspace via a toggle in the New Workspace modal), and "disabled" (never create worktrees — open directly in main repo). - Add WorktreeMode type and WORKTREE_MODES constant to local-db schema - Add worktree_mode column to both settings and projects tables - Add getWorktreeMode/setWorktreeMode tRPC procedures for global setting - Add worktreeMode to project update procedure for per-project override - Modify workspace create procedure to respect effective worktree mode - Add Worktree Mode dropdown to global Git & Worktrees settings page - Add per-project Worktree Mode override in project settings - Register GIT_WORKTREE_MODE in settings search - Show worktree toggle in New Workspace modal when mode is "optional" - Hide branch prefix/worktree location settings when mode is "disabled"
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughThis PR introduces comprehensive workspace and window management enhancements for the desktop application, including project-scoped focus windows, worktree mode configuration (always/optional/disabled), a soft-close workspace flow alongside deletion, cross-window tab state synchronization, file-tree pane support, and sidebar refactoring to accommodate left-panel and project-focus sections. Changes
Sequence Diagram(s)sequenceDiagram
actor User
participant Dashboard as Dashboard Sidebar
participant tRPC as tRPC (Electron)
participant WindowMgr as WindowManager
participant Electron as Electron
User->>Dashboard: Click "Open in Focus Window"
Dashboard->>tRPC: openProjectInNewWindow({projectId})
tRPC->>WindowMgr: focusProjectWindow(projectId)
alt Window exists and is valid
WindowMgr->>Electron: window.focus()
Electron-->>WindowMgr: focused
WindowMgr-->>tRPC: true
tRPC-->>Dashboard: {focused: true, opened: false}
else Window missing or destroyed
WindowMgr->>Electron: new BrowserWindow()
Electron-->>WindowMgr: window instance
WindowMgr->>WindowMgr: registerProjectWindow(projectId, window)
Electron->>Electron: Load project renderer with projectFocus
WindowMgr-->>tRPC: false
tRPC-->>Dashboard: {focused: false, opened: true}
end
sequenceDiagram
actor User
participant WSidebar as Workspace Sidebar
participant Dialog as Close Dialog
participant tRPC as tRPC (Electron)
participant DB as Database
participant Trash as OS Trash
User->>WSidebar: Close project
WSidebar->>Dialog: Show dialog with hasWorktrees
User->>Dialog: Click "Recycle Worktrees"
Dialog->>tRPC: projects.close({id, deleteWorktrees: true})
tRPC->>DB: Query worktree paths
DB-->>tRPC: worktree paths
loop For each worktree
tRPC->>Trash: shell.trashItem(worktreePath)
end
tRPC->>tRPC: git worktree prune
tRPC->>DB: Close project UI window
tRPC-->>WSidebar: Success
sequenceDiagram
participant Window1 as Window 1 (Renderer)
participant tRPC1 as tRPC (Window 1)
participant Main as Main (Electron)
participant Emitter as tabsEmitter
participant Window2 as Window 2 (Renderer)
participant tRPC2 as tRPC (Window 2)
Window1->>tRPC1: tabs.set.mutate(newState)
tRPC1->>Main: Persist to appState
Main->>Emitter: tabsEmitter.emit("change", {updatedAt})
Emitter->>tRPC2: Broadcast to other windows
Window2->>tRPC2: Subscribe active (via useTabsSync)
tRPC2->>Main: uiState.tabs.get.query()
Main-->>tRPC2: Remote tabs state
tRPC2->>Window2: Update local tabs store
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related issues
Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
|
There was a problem hiding this comment.
2 issues found across 15 files
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/DashboardSidebar.tsx">
<violation number="1" location="apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/DashboardSidebar.tsx:23">
P2: Project-focus filtering logic is duplicated across V1 and V2 sidebar components, increasing risk of behavior drift and maintenance overhead.</violation>
</file>
<file name="apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/WorkspaceSidebar.tsx">
<violation number="1" location="apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/WorkspaceSidebar.tsx:24">
P1: Project-focus filtering is applied after `useWorkspaceShortcuts`, so workspace hotkeys still target hidden projects in focused project windows.</violation>
</file>
Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (4)
apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/ProjectSection/ProjectHeader.tsx (1)
128-133: Consider centralizing this action and adding a pending guard.The same
openInNewWindowaction is duplicated in both menu variants. A shared handler improves maintainability and prevents rapid double-submit.♻️ Proposed refactor
const openInNewWindow = electronTrpc.window.openProjectInNewWindow.useMutation({ onError: (error) => toast.error(`Failed to open in new window: ${error.message}`), }); +const handleOpenInNewWindow = () => { + if (!openInNewWindow.isPending) { + openInNewWindow.mutate({ projectId }); + } +}; ... -<ContextMenuItem - onSelect={() => openInNewWindow.mutate({ projectId })} -> +<ContextMenuItem + onSelect={handleOpenInNewWindow} + disabled={openInNewWindow.isPending} +>Also applies to: 251-259, 390-398
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/ProjectSection/ProjectHeader.tsx` around lines 128 - 133, Duplicate openInNewWindow mutation usage should be centralized into a single handler: create a shared function (e.g., handleOpenInNewWindow) that uses the existing electronTrpc.window.openProjectInNewWindow.useMutation instance and checks its isLoading/pending state before calling mutate to prevent double submits; replace the duplicated inline onClick handlers in both menu variants with calls to handleOpenInNewWindow and use the mutation's isLoading to disable UI elements while the request is pending. Ensure the handler references the same mutation object (openInNewWindow) and preserves the onError toast logic.apps/desktop/src/lib/window-loader.ts (1)
23-25: Handle empty query objects to avoid dangling?.If
props.queryis{}, current code builds"?", which yields#/?(dev) //?(prod). This is harmless but noisy.♻️ Proposed tweak
- const qs = props.query - ? `?${new URLSearchParams(props.query).toString()}` - : ""; + const rawQs = props.query ? new URLSearchParams(props.query).toString() : ""; + const qs = rawQs ? `?${rawQs}` : "";🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/desktop/src/lib/window-loader.ts` around lines 23 - 25, The current qs construction creates a dangling "?" when props.query is an empty object; update the logic that builds qs so it only adds `?` when props.query is present and the URLSearchParams string is non-empty (e.g., compute const params = new URLSearchParams(props.query).toString() and set qs = params ? `?${params}` : ""), referencing the existing qs variable, props.query and URLSearchParams usage in window-loader.ts.apps/desktop/src/main/lib/window-manager.ts (1)
61-66: Consider creating a snapshot before iterating to avoid potential issues.Deleting from a
Mapwhile iterating over it withfor...ofis technically safe in JavaScript (the spec guarantees it), but some teams prefer explicit snapshot iteration for clarity. This is a minor stylistic preference.♻️ Optional: Snapshot iteration for clarity
closeAllProjectWindows(): void { - for (const [id, win] of this.projectWindows) { + for (const [id, win] of [...this.projectWindows]) { if (!win.isDestroyed()) win.close(); - this.projectWindows.delete(id); } + this.projectWindows.clear(); }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/desktop/src/main/lib/window-manager.ts` around lines 61 - 66, In closeAllProjectWindows(), avoid deleting entries from this.projectWindows while iterating it directly; instead take a snapshot (e.g., Array.from(this.projectWindows.keys()) or Array.from(this.projectWindows.entries())) and iterate that snapshot to call win.close() and then this.projectWindows.delete(id), so update the closeAllProjectWindows method to iterate over the snapshot of keys/entries before deleting to ensure clearer behavior.apps/desktop/src/main/windows/project.ts (1)
96-100: Verify IPC handler is available before window interaction.If
getIpcHandler()returnsnull(e.g., if the main window hasn't been created yet), the project window won't have tRPC functionality. This is likely acceptable given the expected flow, but consider logging a warning for debugging purposes.📝 Optional: Add debug logging when handler is unavailable
// Attach to the singleton IPC handler const handler = getIpcHandler(); if (handler) { handler.attachWindow(window); +} else { + console.warn("[project-window] IPC handler not available, tRPC calls will fail"); }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/desktop/src/main/windows/project.ts` around lines 96 - 100, The code calls getIpcHandler() and only calls handler.attachWindow(window) when non-null, but doesn't log when the handler is missing; update the block around getIpcHandler() so that if getIpcHandler() returns null you emit a clear debug/warning (e.g., console.warn or your app logger) indicating the IPC handler is unavailable and the project window will not be attached; keep the existing handler.attachWindow(window) behavior when handler is present and reference getIpcHandler() and handler.attachWindow(window) in your change.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@apps/desktop/src/lib/trpc/routers/window.ts`:
- Around line 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.
In `@apps/desktop/src/main/windows/project.ts`:
- Line 115: Remove the redundant listener that calls window.destroy on the
"closed" event: delete the line registering window.on("closed", window.destroy)
in the Project window code (the listener on the BrowserWindow instance named
window), since "closed" fires after the window is already destroyed and the call
is unnecessary and may lose context; if any explicit cleanup is needed keep a
properly scoped callback (e.g., an inline arrow function) instead, otherwise
simply remove the registration.
---
Nitpick comments:
In `@apps/desktop/src/lib/window-loader.ts`:
- Around line 23-25: The current qs construction creates a dangling "?" when
props.query is an empty object; update the logic that builds qs so it only adds
`?` when props.query is present and the URLSearchParams string is non-empty
(e.g., compute const params = new URLSearchParams(props.query).toString() and
set qs = params ? `?${params}` : ""), referencing the existing qs variable,
props.query and URLSearchParams usage in window-loader.ts.
In `@apps/desktop/src/main/lib/window-manager.ts`:
- Around line 61-66: In closeAllProjectWindows(), avoid deleting entries from
this.projectWindows while iterating it directly; instead take a snapshot (e.g.,
Array.from(this.projectWindows.keys()) or
Array.from(this.projectWindows.entries())) and iterate that snapshot to call
win.close() and then this.projectWindows.delete(id), so update the
closeAllProjectWindows method to iterate over the snapshot of keys/entries
before deleting to ensure clearer behavior.
In `@apps/desktop/src/main/windows/project.ts`:
- Around line 96-100: The code calls getIpcHandler() and only calls
handler.attachWindow(window) when non-null, but doesn't log when the handler is
missing; update the block around getIpcHandler() so that if getIpcHandler()
returns null you emit a clear debug/warning (e.g., console.warn or your app
logger) indicating the IPC handler is unavailable and the project window will
not be attached; keep the existing handler.attachWindow(window) behavior when
handler is present and reference getIpcHandler() and
handler.attachWindow(window) in your change.
In
`@apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/ProjectSection/ProjectHeader.tsx`:
- Around line 128-133: Duplicate openInNewWindow mutation usage should be
centralized into a single handler: create a shared function (e.g.,
handleOpenInNewWindow) that uses the existing
electronTrpc.window.openProjectInNewWindow.useMutation instance and checks its
isLoading/pending state before calling mutate to prevent double submits; replace
the duplicated inline onClick handlers in both menu variants with calls to
handleOpenInNewWindow and use the mutation's isLoading to disable UI elements
while the request is pending. Ensure the handler references the same mutation
object (openInNewWindow) and preserves the onError toast logic.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: bfdadb15-27f7-4e25-a408-0df59af09739
📒 Files selected for processing (15)
apps/desktop/src/lib/trpc/index.tsapps/desktop/src/lib/trpc/routers/projects/projects.tsapps/desktop/src/lib/trpc/routers/window.tsapps/desktop/src/lib/window-loader.tsapps/desktop/src/main/index.tsapps/desktop/src/main/lib/window-manager.tsapps/desktop/src/main/windows/main.tsapps/desktop/src/main/windows/project.tsapps/desktop/src/renderer/hooks/useProjectFocus.tsapps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/DashboardSidebar.tsxapps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarProjectSection/DashboardSidebarProjectSection.tsxapps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarProjectSection/components/DashboardSidebarProjectContextMenu/DashboardSidebarProjectContextMenu.tsxapps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarProjectSection/hooks/useDashboardSidebarProjectSectionActions/useDashboardSidebarProjectSectionActions.tsapps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/ProjectSection/ProjectHeader.tsxapps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/WorkspaceSidebar.tsx
| 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 }; | ||
| }), |
There was a problem hiding this comment.
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.
Overhaul the worktree optional flow:
- Show worktree choice dialog when opening any new project ("With
Worktrees" vs "Without Worktrees")
- Remove "Open without worktree" button from new workspace modal
- Filter worktree-disabled projects from new workspace project picker
- Branch-only projects render as single unified sidebar item with
project name, branch, and diff stats inline (no +, no collapse)
Project settings reorganization:
- Project section (top): worktree on/off toggle + appearance
- Workspace Defaults section (below, hidden when worktrees off):
branch prefix, base branch, worktree location/import
- Indented sections with left border for visual separation
Close project/workspace with worktree cleanup:
- Close Project dialog: 3 buttons (Cancel / Close Project / Recycle
Worktrees) — Recycle moves worktree dirs to macOS Trash
- Close Workspace: new context menu item + dialog with same 3-button
pattern for individual workspaces
- Uses shell.trashItem() for recoverable trash, git worktree prune
for cleanup
Other improvements:
- Prompt textarea placeholder: "What do you want to do? (optional)"
- worktreeMode exposed in getAllGrouped query for sidebar detection
- Auto-init git silently when opening project without worktrees
…ct open - Add `trash` option to workspaces.delete procedure — uses shell.trashItem() + git worktree prune instead of permanent delete - "Disable & Recycle" in project settings now sends worktrees to Trash - "Recycle Worktree" in workspace close dialog also uses Trash - Invalidate getAllGrouped + getRecents after worktree choice dialog so new projects appear in the sidebar immediately - Capitalize "Worktrees" in dialog titles, use "Enable"/"Disable" button labels instead of "With/Without Worktrees"
Branch-only projects now show the same hover UX as worktree workspaces: diff stats fade out and keyboard shortcut (⌘1-9) fades in on hover. Uses the same CSS grid overlay pattern as WorkspaceListItem.
When an agent is running in a branch-only project's workspace, the project thumbnail in the sidebar gets a colored ring: - Working (amber): pulsing ring while agent is processing - Permission (red): pulsing ring when agent needs user input - Review (green): static ring when agent finished but user hasn't seen it Uses the same tabs store status aggregation as WorkspaceListItem.
When a project is opened or created, automatically search for favicon/logo files in the repo (favicon.ico, logo.png, public/favicon, etc.) and set it as the project icon. Only runs if no icon is already set. Manually uploaded icons take priority. Fire-and-forget so it doesn't block project creation.
Add patterns for Next.js app directory (app/favicon.ico, app/icon.png), monorepo nested public directories (**/public/favicon.*), and increase glob depth to 4 levels. Fixes discovery for projects where the favicon is in a subdirectory like NEXTapp/app/ or peliguard-website/app/.
Replace the individual project collapsibles in the settings sidebar with a single "Projects" link that opens a clean project list page. Selecting a project navigates to its settings. This scales much better when users have many active projects. - Replace per-project collapsibles in sidebar with single "Projects" nav item - Create /settings/projects route with a searchable project list - Each project shows name, color indicator, and repo path - Clicking a project navigates to /settings/project/$projectId/general
- Projects list page shows workspace count badges and workspace list per project (local branch + worktree workspaces) - Branch-only projects show "no worktrees" badge and single branch row - Project settings page gains "Workspaces" section between Project and Workspace Defaults, listing all workspaces with type badges - Rebased onto per-project-worktree-mode for all worktree UX changes
- Convert projects page to master-detail split view (project list on left, settings on right) instead of two-page drill-down - Always expand workspace/worktree sub-items under each project - Branch-only projects show collapsed (no sub-items, just project row) - Worktree items indented with left border line showing hierarchy - Move "Projects" into the "Editor & Workflow" sidebar group right after "Git & Worktrees" instead of its own separate section - Add workspace listing section to project settings detail view
- Replace color dots with ProjectThumbnail in the settings project list (shows favicon, GitHub avatar, or letter badge) - Add "Detect" button to project icon section that clears the current icon and re-discovers favicon from the project directory - Spinning icon while detection is in progress - Upload and Remove buttons still available alongside Detect
Add a new "file-tree" pane type to the react-mosaic pane system, so the file tree can be opened as a draggable, splittable pane alongside terminals, editors, and chat — instead of being confined to the sidebar. - Add "file-tree" to PaneType union and SplitPaneOptions - Add createFileTreePane() factory function - Update split operations in store to handle "file-tree" pane creation - Create FileTreePane component wrapping existing FilesView - Register pane routing in TabView's renderPane - Add "Split with File Tree" to pane context menu - Wire up context menu action in TabPane and TabContentContextMenu
react-dnd's connectDragSource requires native DOM elements, not React components. Wrapping PaneToolbarActions in a <div> fixes the error.
- Right-click Changes or Files tab to move it to left or right side - Each tab can be independently positioned (e.g. Files on left, Changes on right, or both on one side) - Left panel renders with its own resizable handle - Close left panel: tabs return to right side - Close right panel with lone tab: resets all tabs to right - Expand/collapse button moved from tab bar into ChangesHeader toolbar (only relevant to Changes view) - Tab positions persisted in sidebar store
* Update colors * Refine theme tests * Lint
Move windowManager.unregisterProjectWindow from the "close" event (fires before close, can be intercepted) to the "closed" event (fires after window is actually closed). This prevents orphaned windows that are untracked but still open. Also fix closeAllProjectWindows to let each window's "closed" handler do the map cleanup instead of deleting during iteration.
# Conflicts: # apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/TopBar.tsx
…rktree-mode # Conflicts: # apps/desktop/src/lib/trpc/routers/workspaces/procedures/create.ts # apps/desktop/src/renderer/components/NewWorkspaceModal/components/PromptGroup/PromptGroup.tsx # apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/ProjectSection/ProjectSection.tsx
Summary
Why / Context
The desktop app is single-window. Users working across multiple projects want to focus on one project at a time in a dedicated window without sidebar clutter from other projects.
How It Works
Main process:
WindowManagersingleton (window-manager.ts) tracks the main window and aMap<projectId, BrowserWindow>of project windowsProjectWindowfactory (project.ts) creates a lightweightBrowserWindow— same preload/partition/frameless style as main, but no notification server, menu rebuilds, or window state persistence?projectFocus=<projectId>appended to the hash URLtrpc-electron'screateIPCHandleris already multi-window friendly — we justattachWindow()each new window on the existing singleton handlercreateContextadded to IPC handler exposesctx.callingWindowfor future per-window routing (existing routers still usegetWindowfor now)Renderer:
useProjectFocus()hook readsprojectFocusfromwindow.location.hashquery paramsWorkspaceSidebar) and V2 (DashboardSidebar) filter their project groups whenprojectFocusIdis setLifecycle:
projects.close) also closes its dedicated windowbefore-quit) closes all project windows before exitfocusMainWindow()now uses WindowManager instead ofBrowserWindow.getAllWindows()[0]Manual QA Checklist
Open in New Window
Deduplication
Window Lifecycle
tRPC / IPC
No Regressions
Testing
bun run typecheck— passes (all 22 packages)bun x @biomejs/biome check --write --unsafe— cleanDesign Decisions
?projectFocus=<id>to the hash avoids needing new route definitions — the same app shell loads and the sidebar just filters.getWindowfor existing routers: AddingcreateContextwithcallingWindowis the foundation, but migrating existing routers (window, projects, hotkeys, ringtone) fromgetWindowtoctx.callingWindowis deferred to keep this PR focused. Dialogs in project windows will open on the main window — acceptable for MVP.Known Limitations
getWindow(). This can be migrated toctx.callingWindowin a follow-up.Follow-ups
getWindowtoctx.callingWindowso dialogs open on the correct windowNew Files
apps/desktop/src/main/lib/window-manager.ts— WindowManager singletonapps/desktop/src/main/windows/project.ts— ProjectWindow factoryapps/desktop/src/renderer/hooks/useProjectFocus.ts— project focus hookSummary by cubic
Lets you open each project in its own Focus Window with cross‑window tab sync, a movable Project Focus Bar, and a New Window shortcut. Adds Worktree Mode (global/per‑project) with safer close/recycle flows, a Projects settings page, a draggable file‑tree pane, independent left/right panels, and fixes macOS window restore quirks and orphaned project windows.
New Features
app/andpublic/patterns); removed duplicate Projects section from the settings sidebar.Refactors
WindowManagerandProjectWindow; renderer filters via capturedprojectFocushash query; IPC/tRPC now use acallingWindowcontext; window loader supports query params.uiState.tabs.subscribeand auseTabsSynchook to sync tabs/panes across windows without echo writes; added adataEmitterto invalidate project/workspace queries across windows.worktreeMode; workspace creation honors effective mode; reliability fixes for branch checkout when worktrees are disabled and for dialog/drag‑and‑drop flows.Written for commit bf62698. Summary will update on new commits.
Summary by CodeRabbit
New Features
Improvements
Settings