Skip to content

feat(desktop): open project in dedicated window#2542

Open
z3thon wants to merge 69 commits into
superset-sh:mainfrom
z3thon:feat/open-project-in-new-window
Open

feat(desktop): open project in dedicated window#2542
z3thon wants to merge 69 commits into
superset-sh:mainfrom
z3thon:feat/open-project-in-new-window

Conversation

@z3thon
Copy link
Copy Markdown
Contributor

@z3thon z3thon commented Mar 17, 2026

Summary

  • Add "Open in New Window" to project context menus in both V1 and V2 sidebars
  • New project-focused window shows only that project's workspaces in the sidebar
  • If the project already has an open window, re-selecting focuses it instead of creating a duplicate

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:

  • WindowManager singleton (window-manager.ts) tracks the main window and a Map<projectId, BrowserWindow> of project windows
  • ProjectWindow factory (project.ts) creates a lightweight BrowserWindow — same preload/partition/frameless style as main, but no notification server, menu rebuilds, or window state persistence
  • The project window loads with ?projectFocus=<projectId> appended to the hash URL
  • trpc-electron's createIPCHandler is already multi-window friendly — we just attachWindow() each new window on the existing singleton handler
  • createContext added to IPC handler exposes ctx.callingWindow for future per-window routing (existing routers still use getWindow for now)

Renderer:

  • useProjectFocus() hook reads projectFocus from window.location.hash query params
  • Both V1 (WorkspaceSidebar) and V2 (DashboardSidebar) filter their project groups when projectFocusId is set

Lifecycle:

  • Closing a project (projects.close) also closes its dedicated window
  • App quit (before-quit) closes all project windows before exit
  • focusMainWindow() now uses WindowManager instead of BrowserWindow.getAllWindows()[0]

Manual QA Checklist

Open in New Window

  • Right-click project in V1 sidebar → "Open in New Window" appears after "Open in Finder"
  • Right-click project in V2 sidebar → "Open in New Window" appears after "Open in Finder"
  • Clicking "Open in New Window" opens a new window
  • New window title shows "Superset — "
  • New window shows only that project's workspaces in the sidebar

Deduplication

  • Open project in new window → try opening same project again → existing window focuses (no duplicate)

Window Lifecycle

  • Close project window → main window unaffected
  • Close main window on macOS → project windows stay open
  • Quit app → all windows close cleanly
  • Close/remove project from main window → project window also closes

tRPC / IPC

  • tRPC calls from project window work correctly (file ops, terminal, etc.)
  • No console errors in DevTools (main or renderer) for either window

No Regressions

  • Main window behavior unchanged when no project windows are open
  • App launches without errors
  • Deep links still focus main window

Testing

  • bun run typecheck — passes (all 22 packages)
  • bun x @biomejs/biome check --write --unsafe — clean

Design Decisions

  • WindowManager vs. BrowserWindow.getAllWindows(): Explicit registry gives us projectId→window lookups and avoids relying on window ordering assumptions.
  • Query param via hash URL (not a separate route): The renderer already uses hash-based routing for Electron compatibility. Appending ?projectFocus=<id> to the hash avoids needing new route definitions — the same app shell loads and the sidebar just filters.
  • Keep getWindow for existing routers: Adding createContext with callingWindow is the foundation, but migrating existing routers (window, projects, hotkeys, ringtone) from getWindow to ctx.callingWindow is deferred to keep this PR focused. Dialogs in project windows will open on the main window — acceptable for MVP.

Known Limitations

  • Dialogs (e.g. "Select Directory") in a project window will open attached to the main window since existing routers use getWindow(). This can be migrated to ctx.callingWindow in a follow-up.
  • Project windows don't persist/restore window state (position, size) across restarts — intentional for MVP simplicity.
  • No keyboard shortcut for "Open in New Window" yet.

Follow-ups

  • Migrate existing routers from getWindow to ctx.callingWindow so dialogs open on the correct window
  • Add window state persistence for project windows
  • Handle deep links that target a project with an open window

New Files

  • apps/desktop/src/main/lib/window-manager.ts — WindowManager singleton
  • apps/desktop/src/main/windows/project.ts — ProjectWindow factory
  • apps/desktop/src/renderer/hooks/useProjectFocus.ts — project focus hook

Summary 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

    • Focus Windows: right‑click → “Open in Focus Window”; window titled “Superset — ”; de‑dupes to existing window; non‑restorable; destroys macOS auto‑restored zombie windows on launch.
    • Project Focus Bar: project header + worktree list above tabs in focus windows; dock left or right; vertical resize.
    • Windows: View → “New Window” (Cmd+Shift+N) opens a full clone; structural tab state syncs across windows (last‑write wins); each window keeps its own active tab and focused pane.
    • Worktrees: global and per‑project Worktree Mode (“always”/“optional”/“disabled”); optional shows an enable/disable dialog on open (incl. drag‑and‑drop); disabled opens directly in the main repo with auto‑init/checkout; close dialogs offer “Recycle” to move worktrees to Trash; the workspace “X” now uses this dialog.
    • Projects settings: new “Projects” page with master‑detail layout and workspace listings; per‑project Worktree Mode and icon controls (Detect/Upload/Remove); expanded favicon discovery (incl. nested app/ and public/ patterns); removed duplicate Projects section from the settings sidebar.
    • File tree as a pane: new “file‑tree” pane type you can split and move like terminals/editors; “Split with File Tree” in pane menus.
    • Panels: Files/Changes tabs can dock left or right; left and right panels toggle independently via top‑bar buttons and resize separately; both sides can be active at once; expand/collapse moved into the Changes header.
    • Sidebar polish for branch‑only projects: single row with inline stats, hover shortcut overlay, and agent status ring.
  • Refactors

    • Introduced WindowManager and ProjectWindow; renderer filters via captured projectFocus hash query; IPC/tRPC now use a callingWindow context; window loader supports query params.
    • Added uiState.tabs.subscribe and a useTabsSync hook to sync tabs/panes across windows without echo writes; added a dataEmitter to invalidate project/workspace queries across windows.
    • Hardened window lifecycle on macOS by destroying OS‑restored zombie windows and marking project windows non‑restorable; quit handler closes all project windows; project windows unregister on “closed” to prevent orphans.
    • Extended settings/projects schema and tRPC with 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

    • Added project focus windows for dedicated project workspaces
    • Worktree mode settings with project-level and global controls (always/optional/disabled)
    • File tree pane in workspace sidebar
    • Left sidebar panel for flexible layout options
    • Enhanced project icon auto-discovery
    • Close workspace dialog with worktree recycling option
  • Improvements

    • Cross-window tab state synchronization
    • Improved window lifecycle management
  • Settings

    • New worktree mode configuration in Git settings

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"
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Mar 17, 2026

Note

Reviews paused

It 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 reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This 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

Cohort / File(s) Summary
TRPCContext & Core Infrastructure
apps/desktop/src/lib/trpc/index.ts, apps/desktop/src/main/windows/main.ts
Introduces TRPCContext interface with callingWindow field; updates tRPC initialization to bind context type and pass createContext deriving callingWindow from IPC event sender.
Window Management System
apps/desktop/src/main/lib/window-manager.ts, apps/desktop/src/main/windows/project.ts, apps/desktop/src/lib/window-loader.ts, apps/desktop/src/main/index.ts
Adds WindowManager singleton tracking main and per-project windows with methods for focus, registration, cleanup; introduces ProjectWindow factory creating frameless project-scoped Electron windows; extends WindowId type to include "project"; integrates manager into main window lifecycle and quit handler.
Worktree Mode Feature – Database & Settings
packages/local-db/drizzle/0037_add_worktree_mode.sql, packages/local-db/src/schema/zod.ts, packages/local-db/src/schema/schema.ts, packages/local-db/drizzle/meta/_journal.json
Adds worktree_mode column to projects and settings tables; introduces WORKTREE_MODES constant ("always", "optional", "disabled") and WorktreeMode type.
Worktree Mode Settings Procedures
apps/desktop/src/lib/trpc/routers/settings/index.ts, apps/desktop/src/renderer/routes/_authenticated/settings/git/components/GitSettings/GitSettings.tsx
Adds getWorktreeMode query and setWorktreeMode mutation; implements worktree mode settings UI with conditional visibility of dependent controls (delete-local-branch toggle, worktree-location section).
Project Window & Focus Feature
apps/desktop/src/lib/trpc/routers/window.ts, apps/desktop/src/renderer/hooks/useProjectFocus.ts, apps/desktop/src/renderer/lib/project-focus.ts, apps/desktop/src/renderer/index.tsx
Adds openProjectInNewWindow tRPC mutation; introduces useProjectFocus hook and initialProjectFocusId extraction from URL query; enables dedicated project windows via focus mode.
Project Focus UI – Sidebar & Layout
apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/DashboardSidebar.tsx, apps/desktop/src/renderer/routes/_authenticated/_dashboard/layout.tsx, apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/WorkspaceSidebar.tsx, apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/ProjectFocusBar/*
Filters workspace groups by projectFocusId when in focus mode; hides workspace sidebar when project focus is active; adds ProjectFocusBar component rendering focused project's workspaces.
Project Context Menu & Opening
apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarProjectSection/DashboardSidebarProjectSection.tsx, apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarProjectSection/components/DashboardSidebarProjectContextMenu/DashboardSidebarProjectContextMenu.tsx, apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarProjectSection/hooks/useDashboardSidebarProjectSectionActions/useDashboardSidebarProjectSectionActions.ts
Adds "Open in Focus Window" context menu item and handleOpenInNewWindow action via electronTrpc.window.openProjectInNewWindow.useMutation.
Workspace Closing Flow
apps/desktop/src/lib/trpc/routers/workspaces/procedures/delete.ts, apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/WorkspaceListItem/components/CloseWorkspaceDialog/*, apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/WorkspaceListItem/WorkspaceContextMenu.tsx, apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/WorkspaceListItem/WorkspaceListItem.tsx
Implements soft-close workspace via new workspaces.close mutation and dialog; supports trashing worktrees with trash flag in delete mutation; adds CloseWorkspaceDialog and context menu "Close Workspace" option.
Project Closing with Worktree Management
apps/desktop/src/lib/trpc/routers/projects/projects.ts, apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/ProjectSection/CloseProjectDialog.tsx, apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/ProjectSection/ProjectHeader.tsx
Extends projects.close mutation to accept deleteWorktrees boolean; gathers worktree paths, moves to trash, and runs git worktree prune; adds "Recycle Worktrees" button in close dialog and UI in ProjectHeader.
Branch-Only Workspace Flow
apps/desktop/src/lib/trpc/routers/workspaces/procedures/create.ts, apps/desktop/src/renderer/components/NewWorkspaceModal/components/NewWorkspaceModalContent/NewWorkspaceModalContent.tsx, apps/desktop/src/renderer/components/NewWorkspaceModal/components/PromptGroup/PromptGroup.tsx, apps/desktop/src/renderer/components/WorktreeChoiceDialog/*
Adds useWorktree flag to workspace creation; creates branch-only workspaces when worktree mode is disabled; implements WorktreeChoiceDialog for users to enable/disable worktrees per project; tracks choice in useWorktreeChoiceDialogStore.
Project Focus in Main Workspace
apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/ProjectSection/ProjectSection.tsx
Derives isBranchOnly and hasWorktrees from worktree mode; conditionally renders workspace list and hides collapse UI in branch-only mode; adds branch navigation handler and passes worktree context to child components.
File Tree Pane Support
apps/desktop/src/shared/tabs-types.ts, apps/desktop/src/renderer/stores/tabs/types.ts, apps/desktop/src/renderer/stores/tabs/utils.ts, apps/desktop/src/renderer/stores/tabs/store.ts, apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/FileTreePane/*, apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/index.tsx, apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/TabPane.tsx, apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/components/PaneContextMenuItems/PaneContextMenuItems.tsx, apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabContentContextMenu.tsx
Adds "file-tree" pane type; implements FileTreePane component and createFileTreePane factory; wires file-tree split actions throughout pane context menus; enables routing and rendering of file-tree panes.
Right Sidebar Refactoring & Dual Panels
apps/desktop/src/renderer/stores/sidebar-state.ts, apps/desktop/src/renderer/screens/main/components/WorkspaceView/RightSidebar/index.tsx, apps/desktop/src/renderer/screens/main/components/WorkspaceView/WorkspaceLayout/WorkspaceLayout.tsx, apps/desktop/src/renderer/screens/main/components/WorkspaceView/RightSidebar/ChangesView/ChangesView.tsx, apps/desktop/src/renderer/screens/main/components/WorkspaceView/RightSidebar/ChangesView/components/ChangesHeader/ChangesHeader.tsx
Adds PanelSide type and store state (tabPositions, leftPanelWidth, projectFocusPosition/Height); enables docking tabs to left/right panels; implements dual-panel layout with independent widths and project focus section in right panel; adds expand/collapse controls to ChangesView.
Tab State Synchronization
apps/desktop/src/main/lib/tabs-events.ts, apps/desktop/src/lib/trpc/routers/ui-state/index.ts, apps/desktop/src/renderer/lib/trpc-storage.ts, apps/desktop/src/renderer/stores/tabs/useTabsSync.ts, apps/desktop/src/renderer/stores/tabs/index.ts
Introduces tabsEmitter for cross-window notifications; adds tabs.subscribe tRPC subscription; implements useTabsSync hook fetching remote tab state and updating local store while preventing echo loops; controls persistence via skipNextTabsPersist flag.
Project Properties & Settings
apps/desktop/src/lib/trpc/routers/projects/projects.ts, apps/desktop/src/renderer/react-query/projects/useOpenProject.tsx, apps/desktop/src/renderer/routes/_authenticated/settings/project/$projectId/components/ProjectSettings/ProjectSettings.tsx, apps/desktop/src/renderer/routes/_authenticated/layout.tsx
Adds worktreeMode field to projects; implements favicon auto-discovery in project upsert and refresh flows; adds project-changed event emission and onProjectChanged subscription; refactors project settings UI with sectioned layout and worktree enable/disable controls.
Favicon Discovery Enhancement
apps/desktop/src/lib/trpc/routers/projects/utils/favicon-discovery.ts
Expands FAVICON_PATTERNS to search nested public directories, Next.js app paths, and monorepo-style structures; enables deep: 4 traversal in glob configuration.
Workspace Query & Navigation
apps/desktop/src/lib/trpc/routers/workspaces/procedures/query.ts, apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/SidebarDropZone/SidebarDropZone.tsx
Adds worktreeMode to grouped workspace response; updates drop-zone navigation to invalidate and refetch workspace cache before navigating to matching workspace.
Settings UI & Search
apps/desktop/src/renderer/routes/_authenticated/settings/components/SettingsSidebar/GeneralSettings.tsx, apps/desktop/src/renderer/routes/_authenticated/settings/utils/settings-search/settings-search.ts
Adds /settings/projects route and worktree mode settings item to search index and sidebar.
Application Setup & State
apps/desktop/src/lib/electron-app/factories/app/setup.ts, apps/desktop/src/main/lib/app-state/index.ts, apps/desktop/src/main/lib/app-state/schemas.ts, apps/desktop/src/main/lib/menu.ts, apps/desktop/src/main/lib/data-events.ts
Adds app state field openFocusWindowProjectIds for persisting focus window state across restarts; cleans up existing windows before creating new managed window; adds "New Window" menu item with meta+shift+n hotkey; introduces dataEmitter for data-change notifications.
Hotkey Updates
apps/desktop/src/shared/hotkeys.ts
Assigns default binding "meta+shift+n" to HOTKEYS.NEW_WINDOW and removes hidden flag.

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
Loading
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
Loading
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
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related issues

Possibly related PRs

  • refactor(desktop): update changes view #595: Modifies workspace/sidebar rendering in ContentView and workspace section components (ProjectSection, WorkspaceSidebar), overlapping with refactored sidebar layout and grouping logic in this PR.

Poem

🐰 Windows now focus, with worktrees at bay,
Workspaces close soft—no deletion today!
Left panels and right, tabs sync cross the air,
File trees and project focus, a workspace made fair! 🌳✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 23.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat(desktop): open project in dedicated window' clearly and concisely summarizes the main feature added—opening projects in dedicated windows. It is specific, on-topic, and directly related to the changeset.
Description check ✅ Passed The pull request description is comprehensive and well-structured, following most of the required template sections with clear explanations of changes, context, manual QA checklist, and design decisions.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

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

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.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

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 openInNewWindow action 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.query is {}, 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 Map while iterating over it with for...of is 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() returns null (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

📥 Commits

Reviewing files that changed from the base of the PR and between 9fecc36 and 1c1f069.

📒 Files selected for processing (15)
  • apps/desktop/src/lib/trpc/index.ts
  • apps/desktop/src/lib/trpc/routers/projects/projects.ts
  • apps/desktop/src/lib/trpc/routers/window.ts
  • apps/desktop/src/lib/window-loader.ts
  • apps/desktop/src/main/index.ts
  • apps/desktop/src/main/lib/window-manager.ts
  • apps/desktop/src/main/windows/main.ts
  • apps/desktop/src/main/windows/project.ts
  • apps/desktop/src/renderer/hooks/useProjectFocus.ts
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/DashboardSidebar.tsx
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarProjectSection/DashboardSidebarProjectSection.tsx
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarProjectSection/components/DashboardSidebarProjectContextMenu/DashboardSidebarProjectContextMenu.tsx
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarProjectSection/hooks/useDashboardSidebarProjectSectionActions/useDashboardSidebarProjectSectionActions.ts
  • apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/ProjectSection/ProjectHeader.tsx
  • apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/WorkspaceSidebar.tsx

Comment on lines +132 to +142
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 };
}),
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.

Comment thread apps/desktop/src/main/windows/project.ts Outdated
z3thon and others added 26 commits March 19, 2026 15:00
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
z3thon added 30 commits March 20, 2026 06:36
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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants