Skip to content

refactor(desktop): extract db helpers from workspaces router#681

Merged
Kitenite merged 8 commits intomainfrom
refactor-workspace-file
Jan 9, 2026
Merged

refactor(desktop): extract db helpers from workspaces router#681
Kitenite merged 8 commits intomainfrom
refactor-workspace-file

Conversation

@Kitenite
Copy link
Copy Markdown
Collaborator

@Kitenite Kitenite commented Jan 8, 2026

Summary

  • Extract repeated database operations from workspaces.ts into a dedicated db-helpers.ts module
  • Reduces code duplication (~87 lines removed net) and improves readability
  • Extracted helpers include: getWorkspace, getProject, getWorktree, setLastActiveWorkspace, touchWorkspace, activateProject, hideProjectIfNoWorkspaces, updateActiveWorkspaceIfRemoved, getBranchWorkspace, getMaxWorkspaceTabOrder

Test plan

  • Verify typecheck passes: bun tsc --noEmit in desktop app
  • Verify lint passes: bun run lint
  • Test workspace creation, deletion, and switching in the desktop app
  • Test branch workspace operations (create, switch branch, delete)

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Comprehensive workspace workflows: create, open, close, and delete workspaces (worktree & branch-based)
    • Branch workspace support and branch switching
    • Git status monitoring, GitHub PR status, and worktree metadata views
    • Workspace initialization with progress streaming and retry
    • Workspace reordering, activation, unread state, and improved tab ordering behaviors

✏️ Tip: You can customize this high-level summary in your review settings.

Extract repeated database operations from workspaces.ts into a dedicated
db-helpers.ts module to reduce code duplication and improve readability.

Extracted helpers include:
- getWorkspace, getProject, getWorktree - basic fetches
- setLastActiveWorkspace - settings upsert pattern
- touchWorkspace - timestamp updates with optional fields
- activateProject - project activation with tab order management
- hideProjectIfNoWorkspaces - cleanup when last workspace removed
- updateActiveWorkspaceIfRemoved - cascading active workspace updates
- getBranchWorkspace - branch workspace lookup
- getMaxWorkspaceTabOrder - tab order calculation

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Jan 8, 2026

📝 Walkthrough

Walkthrough

Refactors the workspaces tRPC router into modular procedure modules (create, delete, query, branch, status, git-status, init), adds workspace DB helpers, and implements background worktree initialization; exposes mergeRouters from the tRPC entrypoint for router composition.

Changes

Cohort / File(s) Summary
tRPC export
apps/desktop/src/lib/trpc/index.ts
Exports mergeRouters = t.mergeRouters to enable external router composition.
Main router composition
apps/desktop/src/lib/trpc/routers/workspaces/workspaces.ts
Replaces large inline workspaces router with a 1-line composition that merges modular procedure routers via mergeRouters.
Procedure modules
apps/desktop/src/lib/trpc/routers/workspaces/procedures/*
apps/desktop/src/lib/trpc/routers/workspaces/procedures/query.ts, create.ts, delete.ts, branch.ts, status.ts, git-status.ts, init.ts
Adds seven new procedure factories (createXxxProcedures) exposing the workspaces API in focused routers (queries, creation/open, deletion/close, branch ops, status/reorder, git status, init/subscription/retry).
DB helpers
apps/desktop/src/lib/trpc/routers/workspaces/utils/db-helpers.ts
Adds workspace/project/worktree helper utilities (get/set last active, tab order, fetch with relations, touch/delete workspace, update default branch, etc.).
Workspace init orchestration
apps/desktop/src/lib/trpc/routers/workspaces/utils/workspace-init.ts
Implements initializeWorkspaceWorktree and WorkspaceInitParams: per-project locking, remote sync, branch/ref verification, worktree creation, config copy, git status updates, progress reporting, and cleanup.

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant tRPC_Server
  participant DB
  participant Git
  participant InitManager

  Client->>tRPC_Server: call createWorkspace / openWorktree
  tRPC_Server->>DB: insert workspace/worktree records
  tRPC_Server->>InitManager: start background initializeWorkspaceWorktree(params)
  InitManager->>Git: refresh default branch / fetch refs
  Git-->>InitManager: branch/ref existence, fetch results
  InitManager->>DB: update worktree gitStatus & paths
  InitManager->>InitManager: emit progress events
  InitManager->>Client: (via subscription) progress updates
  InitManager->>DB: mark initialized, track analytics
  Client->>tRPC_Server: query getInitProgress / getWorktreeInfo
  tRPC_Server->>DB: return persisted worktree + git/github status
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • saddlepaddle

Poem

🐰 I hopped through code and stitched the seams,

split the monolith into tidy dreams.
Routers merged and worktrees grow,
progress hops and updates flow.
A carrot for each clean, small scheme.

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title 'refactor(desktop): extract db helpers from workspaces router' accurately and clearly describes the main refactoring change: extracting database helper functions into a dedicated module.
Description check ✅ Passed The PR description provides a clear summary of changes, lists extracted helpers, and includes a concrete test plan with specific commands. All primary template sections are addressed appropriately.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

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

✨ Finishing touches
  • 📝 Generate docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Kitenite and others added 5 commits January 8, 2026 16:24
Break up the large workspaces.ts file (~1560 lines) into smaller,
focused modules organized by domain:

utils/
- workspace-init.ts: Background worktree initialization logic

procedures/
- create.ts: create, createBranchWorkspace, openWorktree
- delete.ts: delete, close, canDelete
- query.ts: get, getAll, getAllGrouped, getActive
- branch.ts: getBranches, switchBranchWorkspace
- git-status.ts: refreshGitStatus, getGitHubStatus, getWorktreeInfo, getWorktreesByProject
- status.ts: setActive, reorder, update, setUnread
- init.ts: onInitProgress, retryInit, getInitProgress, getSetupCommands

The main workspaces.ts is now a thin orchestrator (~35 lines) that
merges all sub-routers using tRPC's mergeRouters.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
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: 4

🤖 Fix all issues with AI agents
In @apps/desktop/src/lib/trpc/routers/workspaces/procedures/branch.ts:
- Around line 17-56: The current getBranches implementation builds
worktreeBranchMap as Record<string,string> and assigns
worktreeBranchMap[ws.branch] = ws.id which silently overwrites if multiple
worktrees share a branch; change worktreeBranchMap to Record<string,string[]>
and when iterating projectWorkspaces push ws.id into
worktreeBranchMap[ws.branch] (initializing an array if needed), then update the
returned payload so inUse remains Object.keys(worktreeBranchMap) and
inUseWorkspaces maps branch -> string[] (or if API must stay string, pick a
deterministic winner like the first id and document that choice); update any
consumers accordingly (symbols: getBranches, worktreeBranchMap,
projectWorkspaces, inUseWorkspaces).

In @apps/desktop/src/lib/trpc/routers/workspaces/procedures/delete.ts:
- Around line 186-199: The teardown is being fire-and-forget via
runTeardown(...).then(...) which can race with the subsequent call to
removeWorktree; change the flow so you await runTeardown(project.mainRepoPath,
worktree.path, workspace.name) (or await its Promise result) before calling
removeWorktree so teardown scripts run against the live worktree, and
handle/report errors from runTeardown (log or decide whether to abort removal)
based on the returned result.success/value from runTeardown.

In @apps/desktop/src/lib/trpc/routers/workspaces/procedures/query.ts:
- Around line 30-100: getAllGrouped currently calls getWorkspacePath per
workspace causing N+1 DB reads; instead preload related worktree rows once and
use project data already in groupsMap. Query all worktrees in one select (e.g.,
localDb.select().from(worktrees).all()) and build a map by worktree id, then in
the allWorkspaces loop avoid calling getWorkspacePath: for type === "worktree"
read the path from the preloaded worktreesMap, for type === "branch" use the
project's mainRepoPath from groupsMap.get(workspace.projectId).project (or
fallback ""), and remove any per-workspace localDb calls to getWorkspacePath or
projects to eliminate repeated DB access.
- Around line 134-173: The condition checking for "not yet attempted" uses
baseBranch === undefined which is impossible because
SelectWorktree["baseBranch"] is string | null; change the logic in the block
around worktree/baseBranch so that you treat null as "not yet attempted" (i.e.,
check baseBranch === null) or introduce a separate sentinel (e.g.,
baseBranchDetectionAttemptedAt) to distinguish states; update the branch where
detection runs (the code calling hasOriginRemote(project.mainRepoPath),
detectBaseBranch(worktree.path, worktree.branch, defaultBranch), and the
localDb.update(worktrees).set({ baseBranch: ... })) to use the new condition and
persist either the detected branch or null/appropriate sentinel consistently.
🧹 Nitpick comments (14)
apps/desktop/src/lib/trpc/index.ts (1)

18-45: Avoid fully silent failure in Sentry capture path.
The catch {} (Line 39-41) can hide regressions (e.g., Sentry import path changes). At least emit a debug/warn with context (and avoid logging PII).

Proposed tweak
 } catch {
-  // Sentry not available
+  console.debug("[trpc/sentry] Sentry capture failed or unavailable");
 }
apps/desktop/src/lib/trpc/routers/workspaces/procedures/query.ts (2)

22-28: Push ordering into SQL instead of sorting in JS.
Sorting after .all() reads all rows and does extra work; use Drizzle orderBy(workspaces.tabOrder) where possible.

Also applies to: 80-85


12-20: Use TRPCError with appropriate error codes instead of generic Error.

The procedures at lines 12-20 and 102-119 throw generic Error which clients receive as internal errors. Use TRPCError with specific codes for better error handling in UI:

  • Line 17 (get procedure): use code "NOT_FOUND" for missing workspace by ID
  • Lines 116-118 (getActive procedure): use code "INTERNAL_SERVER_ERROR" for data consistency issues

This pattern is already established elsewhere in the codebase (e.g., usability.ts, external/index.ts).

apps/desktop/src/lib/trpc/routers/workspaces/utils/workspace-init.ts (2)

195-207: Don’t fully swallow fetch errors; log + surface minimally.
The empty catch for fetchDefaultBranch (Line 204-206) makes diagnosing “stale default branch” issues hard; at least console.warn with [workspace-init].


279-306: Log the error object (or stack), not only error.message.
Current logging loses stack/context; prefer console.error("[workspace-init] ...", error) and then derive a user-facing message separately.

apps/desktop/src/lib/trpc/routers/workspaces/utils/db-helpers.ts (3)

17-26: Extract the magic settings row id (1) into a constant.
Keeps the invariant explicit and reduces duplication across helpers/procedures.


32-56: Avoid .all() for “max/existence/first row” helpers.
getMaxWorkspaceTabOrder, getMaxProjectTabOrder, hideProjectIfNoWorkspaces, selectNextActiveWorkspace can be done with orderBy(desc(...)).get() or existence checks to avoid loading full tables.

Also applies to: 92-115


189-207: Guidelines: switch 2+ parameter helpers to a params object (e.g., touchWorkspace, updateProjectDefaultBranch).
This improves call-site clarity and future extensibility.

Based on coding guidelines, functions with 2+ parameters should accept a single params object.

Also applies to: 243-252

apps/desktop/src/lib/trpc/routers/workspaces/procedures/status.ts (1)

32-69: reorder: consider transaction + only updating changed rows.
Current loop does N writes even if few positions changed; wrapping in a transaction also prevents partial reorder if something fails mid-loop.

apps/desktop/src/lib/trpc/routers/workspaces/procedures/init.ts (2)

55-95: Fire-and-forget init should be explicitly detached (and guarded against unhandled rejections).
Even if initializeWorkspaceWorktree “does not throw”, future changes could reintroduce rejections; consider void initializeWorkspaceWorktree(...).catch(...).


58-72: Prefer typed tRPC errors for missing workspace/worktree/project.
Returning NOT_FOUND (or BAD_REQUEST) helps renderer decide whether to retry vs prompt user.

apps/desktop/src/lib/trpc/routers/workspaces/procedures/branch.ts (1)

67-79: Use TRPCError with NOT_FOUND for missing project/workspace cases.
Keeps error codes stable for UI and aligns with tRPC best practices. This pattern is already established elsewhere in the codebase (e.g., usability.ts).

Also applies to: 95-97

apps/desktop/src/lib/trpc/routers/workspaces/procedures/git-status.ts (1)

151-162: Consider optimizing N+1 query pattern.

The current implementation queries each workspace individually inside the map loop, resulting in N+1 database queries. For small datasets this is acceptable, but if projects can have many worktrees, consider using a single query with a left join.

♻️ Suggested optimization using a join
 		getWorktreesByProject: publicProcedure
 			.input(z.object({ projectId: z.string() }))
 			.query(({ input }) => {
-				const projectWorktrees = localDb
-					.select()
-					.from(worktrees)
-					.where(eq(worktrees.projectId, input.projectId))
-					.all();
-
-				return projectWorktrees.map((wt) => {
-					const workspace = localDb
-						.select()
-						.from(workspaces)
-						.where(eq(workspaces.worktreeId, wt.id))
-						.get();
-					return {
-						...wt,
-						hasActiveWorkspace: workspace !== undefined,
-						workspace: workspace ?? null,
-					};
-				});
+				const results = localDb
+					.select({
+						worktree: worktrees,
+						workspace: workspaces,
+					})
+					.from(worktrees)
+					.leftJoin(workspaces, eq(workspaces.worktreeId, worktrees.id))
+					.where(eq(worktrees.projectId, input.projectId))
+					.all();
+
+				return results.map(({ worktree, workspace }) => ({
+					...worktree,
+					hasActiveWorkspace: workspace !== null,
+					workspace,
+				}));
 			}),
apps/desktop/src/lib/trpc/routers/workspaces/procedures/create.ts (1)

221-227: Consider batching tabOrder updates.

The loop performs individual UPDATE statements for each workspace. This could be optimized to a single batch update if workspace counts grow large, though the current approach is functionally correct.

♻️ Batch update alternative
-				for (const ws of projectWorkspaces) {
-					localDb
-						.update(workspaces)
-						.set({ tabOrder: ws.tabOrder + 1 })
-						.where(eq(workspaces.id, ws.id))
-						.run();
-				}
+				// Batch increment all existing workspace tabOrders
+				localDb
+					.update(workspaces)
+					.set({ tabOrder: sql`${workspaces.tabOrder} + 1` })
+					.where(
+						and(
+							eq(workspaces.projectId, input.projectId),
+							not(eq(workspaces.id, newWorkspaceId)),
+						),
+					)
+					.run();

Note: Requires importing sql from drizzle-orm.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c0e24f8 and f56b400.

📒 Files selected for processing (11)
  • apps/desktop/src/lib/trpc/index.ts
  • apps/desktop/src/lib/trpc/routers/workspaces/procedures/branch.ts
  • apps/desktop/src/lib/trpc/routers/workspaces/procedures/create.ts
  • apps/desktop/src/lib/trpc/routers/workspaces/procedures/delete.ts
  • apps/desktop/src/lib/trpc/routers/workspaces/procedures/git-status.ts
  • apps/desktop/src/lib/trpc/routers/workspaces/procedures/init.ts
  • apps/desktop/src/lib/trpc/routers/workspaces/procedures/query.ts
  • apps/desktop/src/lib/trpc/routers/workspaces/procedures/status.ts
  • apps/desktop/src/lib/trpc/routers/workspaces/utils/db-helpers.ts
  • apps/desktop/src/lib/trpc/routers/workspaces/utils/workspace-init.ts
  • apps/desktop/src/lib/trpc/routers/workspaces/workspaces.ts
🧰 Additional context used
📓 Path-based instructions (4)
apps/desktop/**/*.{ts,tsx}

📄 CodeRabbit inference engine (apps/desktop/AGENTS.md)

apps/desktop/**/*.{ts,tsx}: For Electron interprocess communication, ALWAYS use tRPC as defined in src/lib/trpc
Use alias as defined in tsconfig.json when possible
Prefer zustand for state management if it makes sense. Do not use effect unless absolutely necessary.
For tRPC subscriptions with trpc-electron, ALWAYS use the observable pattern from @trpc/server/observable instead of async generators, as the library explicitly checks isObservable(result) and throws an error otherwise

Files:

  • apps/desktop/src/lib/trpc/routers/workspaces/procedures/status.ts
  • apps/desktop/src/lib/trpc/routers/workspaces/procedures/branch.ts
  • apps/desktop/src/lib/trpc/routers/workspaces/procedures/git-status.ts
  • apps/desktop/src/lib/trpc/routers/workspaces/procedures/query.ts
  • apps/desktop/src/lib/trpc/routers/workspaces/utils/workspace-init.ts
  • apps/desktop/src/lib/trpc/index.ts
  • apps/desktop/src/lib/trpc/routers/workspaces/procedures/create.ts
  • apps/desktop/src/lib/trpc/routers/workspaces/workspaces.ts
  • apps/desktop/src/lib/trpc/routers/workspaces/procedures/delete.ts
  • apps/desktop/src/lib/trpc/routers/workspaces/procedures/init.ts
  • apps/desktop/src/lib/trpc/routers/workspaces/utils/db-helpers.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx}: Use object parameters for functions with 2+ parameters instead of positional arguments
Functions with 2+ parameters should accept a single params object with named properties for self-documentation and extensibility
Use prefixed console logging with context pattern: [domain/operation] message
Extract magic numbers and hardcoded values to named constants at module top
Use lookup objects/maps instead of repeated if (type === ...) conditionals
Avoid using any type - maintain type safety in TypeScript code
Never swallow errors silently - at minimum log them with context
Import from concrete files directly when possible - avoid barrel file abuse that creates circular dependencies
Avoid deep nesting (4+ levels) - use early returns, extract functions, and invert conditions
Use named properties in options objects instead of boolean parameters to avoid boolean blindness

Files:

  • apps/desktop/src/lib/trpc/routers/workspaces/procedures/status.ts
  • apps/desktop/src/lib/trpc/routers/workspaces/procedures/branch.ts
  • apps/desktop/src/lib/trpc/routers/workspaces/procedures/git-status.ts
  • apps/desktop/src/lib/trpc/routers/workspaces/procedures/query.ts
  • apps/desktop/src/lib/trpc/routers/workspaces/utils/workspace-init.ts
  • apps/desktop/src/lib/trpc/index.ts
  • apps/desktop/src/lib/trpc/routers/workspaces/procedures/create.ts
  • apps/desktop/src/lib/trpc/routers/workspaces/workspaces.ts
  • apps/desktop/src/lib/trpc/routers/workspaces/procedures/delete.ts
  • apps/desktop/src/lib/trpc/routers/workspaces/procedures/init.ts
  • apps/desktop/src/lib/trpc/routers/workspaces/utils/db-helpers.ts
apps/**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Use Drizzle ORM for all database operations - never use raw SQL

Files:

  • apps/desktop/src/lib/trpc/routers/workspaces/procedures/status.ts
  • apps/desktop/src/lib/trpc/routers/workspaces/procedures/branch.ts
  • apps/desktop/src/lib/trpc/routers/workspaces/procedures/git-status.ts
  • apps/desktop/src/lib/trpc/routers/workspaces/procedures/query.ts
  • apps/desktop/src/lib/trpc/routers/workspaces/utils/workspace-init.ts
  • apps/desktop/src/lib/trpc/index.ts
  • apps/desktop/src/lib/trpc/routers/workspaces/procedures/create.ts
  • apps/desktop/src/lib/trpc/routers/workspaces/workspaces.ts
  • apps/desktop/src/lib/trpc/routers/workspaces/procedures/delete.ts
  • apps/desktop/src/lib/trpc/routers/workspaces/procedures/init.ts
  • apps/desktop/src/lib/trpc/routers/workspaces/utils/db-helpers.ts
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (AGENTS.md)

Use Biome for formatting and linting - run at root level with bun run lint:fix or biome check --write

Files:

  • apps/desktop/src/lib/trpc/routers/workspaces/procedures/status.ts
  • apps/desktop/src/lib/trpc/routers/workspaces/procedures/branch.ts
  • apps/desktop/src/lib/trpc/routers/workspaces/procedures/git-status.ts
  • apps/desktop/src/lib/trpc/routers/workspaces/procedures/query.ts
  • apps/desktop/src/lib/trpc/routers/workspaces/utils/workspace-init.ts
  • apps/desktop/src/lib/trpc/index.ts
  • apps/desktop/src/lib/trpc/routers/workspaces/procedures/create.ts
  • apps/desktop/src/lib/trpc/routers/workspaces/workspaces.ts
  • apps/desktop/src/lib/trpc/routers/workspaces/procedures/delete.ts
  • apps/desktop/src/lib/trpc/routers/workspaces/procedures/init.ts
  • apps/desktop/src/lib/trpc/routers/workspaces/utils/db-helpers.ts
🧠 Learnings (7)
📓 Common learnings
Learnt from: CR
Repo: superset-sh/superset PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-02T06:50:28.671Z
Learning: Applies to apps/api/**/*.{ts,tsx} : Extract business logic from tRPC procedures into utility functions when logic exceeds ~50 lines, is used by multiple procedures, or needs independent testing
Learnt from: CR
Repo: superset-sh/superset PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-02T06:50:28.671Z
Learning: Separate by ownership and lifecycle - keep transport (routes, API handlers), orchestration (tRPC procedures), and domain rules in distinct layers
Learnt from: CR
Repo: superset-sh/superset PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-02T06:50:28.671Z
Learning: Applies to apps/api/**/*.{ts,tsx} : tRPC procedures and API route handlers should validate and delegate; keep orchestrators thin
📚 Learning: 2026-01-02T06:50:28.671Z
Learnt from: CR
Repo: superset-sh/superset PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-02T06:50:28.671Z
Learning: Applies to apps/api/**/*.{ts,tsx} : Extract business logic from tRPC procedures into utility functions when logic exceeds ~50 lines, is used by multiple procedures, or needs independent testing

Applied to files:

  • apps/desktop/src/lib/trpc/routers/workspaces/procedures/status.ts
  • apps/desktop/src/lib/trpc/routers/workspaces/procedures/branch.ts
  • apps/desktop/src/lib/trpc/routers/workspaces/procedures/git-status.ts
  • apps/desktop/src/lib/trpc/routers/workspaces/procedures/query.ts
  • apps/desktop/src/lib/trpc/index.ts
  • apps/desktop/src/lib/trpc/routers/workspaces/procedures/create.ts
  • apps/desktop/src/lib/trpc/routers/workspaces/workspaces.ts
  • apps/desktop/src/lib/trpc/routers/workspaces/procedures/delete.ts
  • apps/desktop/src/lib/trpc/routers/workspaces/procedures/init.ts
📚 Learning: 2026-01-02T06:50:28.671Z
Learnt from: CR
Repo: superset-sh/superset PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-02T06:50:28.671Z
Learning: Applies to apps/api/**/*.{ts,tsx} : tRPC procedures and API route handlers should validate and delegate; keep orchestrators thin

Applied to files:

  • apps/desktop/src/lib/trpc/routers/workspaces/procedures/status.ts
  • apps/desktop/src/lib/trpc/routers/workspaces/procedures/branch.ts
  • apps/desktop/src/lib/trpc/routers/workspaces/procedures/query.ts
  • apps/desktop/src/lib/trpc/index.ts
  • apps/desktop/src/lib/trpc/routers/workspaces/procedures/create.ts
  • apps/desktop/src/lib/trpc/routers/workspaces/workspaces.ts
  • apps/desktop/src/lib/trpc/routers/workspaces/procedures/delete.ts
  • apps/desktop/src/lib/trpc/routers/workspaces/procedures/init.ts
📚 Learning: 2025-12-21T04:39:28.543Z
Learnt from: CR
Repo: superset-sh/superset PR: 0
File: apps/desktop/AGENTS.md:0-0
Timestamp: 2025-12-21T04:39:28.543Z
Learning: Applies to apps/desktop/**/*.{ts,tsx} : For Electron interprocess communication, ALWAYS use tRPC as defined in `src/lib/trpc`

Applied to files:

  • apps/desktop/src/lib/trpc/index.ts
📚 Learning: 2026-01-02T06:50:28.671Z
Learnt from: CR
Repo: superset-sh/superset PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-02T06:50:28.671Z
Learning: Applies to apps/api/**/*.{ts,tsx} : Use TRPCError with appropriate error codes: NOT_FOUND, UNAUTHORIZED, FORBIDDEN, BAD_REQUEST, INTERNAL_SERVER_ERROR, NOT_IMPLEMENTED

Applied to files:

  • apps/desktop/src/lib/trpc/index.ts
📚 Learning: 2026-01-02T06:50:28.671Z
Learnt from: CR
Repo: superset-sh/superset PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-02T06:50:28.671Z
Learning: Applies to apps/desktop/src/lib/*.ts : Never import Node.js modules in shared code like electron-router-dom.ts - it runs in both main and renderer processes

Applied to files:

  • apps/desktop/src/lib/trpc/index.ts
📚 Learning: 2026-01-02T06:50:28.671Z
Learnt from: CR
Repo: superset-sh/superset PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-02T06:50:28.671Z
Learning: Applies to apps/desktop/src/lib/electron-router-dom.ts : Do not import Node.js modules like node:path or dotenv in electron-router-dom.ts and similar shared files - they run in both main and renderer processes

Applied to files:

  • apps/desktop/src/lib/trpc/index.ts
🧬 Code graph analysis (7)
apps/desktop/src/lib/trpc/routers/workspaces/procedures/status.ts (4)
apps/desktop/src/lib/trpc/index.ts (2)
  • router (47-47)
  • publicProcedure (49-49)
apps/desktop/src/lib/trpc/routers/workspaces/utils/db-helpers.ts (3)
  • getWorkspace (134-140)
  • touchWorkspace (189-207)
  • setLastActiveWorkspace (17-26)
apps/desktop/src/main/lib/local-db/index.ts (1)
  • localDb (82-82)
packages/local-db/src/schema/schema.ts (1)
  • workspaces (80-118)
apps/desktop/src/lib/trpc/routers/workspaces/procedures/branch.ts (5)
apps/desktop/src/lib/trpc/index.ts (2)
  • router (47-47)
  • publicProcedure (49-49)
apps/desktop/src/main/lib/local-db/index.ts (1)
  • localDb (82-82)
packages/local-db/src/schema/schema.ts (2)
  • projects (16-42)
  • workspaces (80-118)
apps/desktop/src/lib/trpc/routers/workspaces/utils/git.ts (2)
  • listBranches (649-675)
  • safeCheckoutBranch (848-874)
apps/desktop/src/lib/trpc/routers/workspaces/utils/db-helpers.ts (4)
  • getBranchWorkspace (228-238)
  • touchWorkspace (189-207)
  • setLastActiveWorkspace (17-26)
  • getWorkspace (134-140)
apps/desktop/src/lib/trpc/routers/workspaces/procedures/query.ts (6)
apps/desktop/src/lib/trpc/index.ts (2)
  • router (47-47)
  • publicProcedure (49-49)
apps/desktop/src/lib/trpc/routers/workspaces/utils/db-helpers.ts (1)
  • getWorkspace (134-140)
apps/desktop/src/main/lib/local-db/index.ts (1)
  • localDb (82-82)
packages/local-db/src/schema/schema.ts (3)
  • workspaces (80-118)
  • projects (16-42)
  • worktrees (50-72)
apps/desktop/src/lib/trpc/routers/workspaces/utils/worktree.ts (1)
  • getWorkspacePath (22-43)
apps/desktop/src/lib/trpc/routers/workspaces/utils/git.ts (2)
  • hasOriginRemote (261-269)
  • detectBaseBranch (595-641)
apps/desktop/src/lib/trpc/routers/workspaces/procedures/create.ts (6)
apps/desktop/src/main/lib/local-db/index.ts (1)
  • localDb (82-82)
apps/desktop/src/lib/trpc/routers/workspaces/utils/db-helpers.ts (7)
  • getMaxWorkspaceTabOrder (32-41)
  • setLastActiveWorkspace (17-26)
  • activateProject (63-74)
  • getBranchWorkspace (228-238)
  • touchWorkspace (189-207)
  • getWorktree (156-162)
  • getProject (145-151)
apps/desktop/src/main/lib/workspace-init-manager.ts (1)
  • workspaceInitManager (302-302)
apps/desktop/src/lib/trpc/routers/workspaces/utils/workspace-init.ts (1)
  • initializeWorkspaceWorktree (36-312)
apps/desktop/src/lib/trpc/routers/workspaces/utils/setup.ts (1)
  • loadSetupConfig (30-56)
apps/desktop/src/lib/trpc/routers/workspaces/utils/git.ts (3)
  • getCurrentBranch (682-694)
  • safeCheckoutBranch (848-874)
  • worktreeExists (244-259)
apps/desktop/src/lib/trpc/routers/workspaces/procedures/delete.ts (7)
apps/desktop/src/lib/trpc/index.ts (2)
  • router (47-47)
  • publicProcedure (49-49)
apps/desktop/src/lib/trpc/routers/workspaces/utils/db-helpers.ts (7)
  • getWorkspace (134-140)
  • getWorktree (156-162)
  • getProject (145-151)
  • deleteWorkspace (212-214)
  • deleteWorktreeRecord (219-221)
  • hideProjectIfNoWorkspaces (92-101)
  • updateActiveWorkspaceIfRemoved (121-129)
apps/desktop/src/lib/trpc/routers/workspaces/utils/git.ts (4)
  • worktreeExists (244-259)
  • hasUncommittedChanges (401-407)
  • hasUnpushedCommits (409-434)
  • removeWorktree (213-232)
apps/desktop/src/main/lib/workspace-init-manager.ts (1)
  • workspaceInitManager (302-302)
packages/local-db/src/schema/schema.ts (1)
  • SelectWorktree (75-75)
apps/desktop/src/lib/trpc/routers/workspaces/utils/teardown.ts (1)
  • runTeardown (41-80)
apps/desktop/src/main/lib/analytics/index.ts (1)
  • track (27-46)
apps/desktop/src/lib/trpc/routers/workspaces/procedures/init.ts (6)
apps/desktop/src/lib/trpc/index.ts (2)
  • router (47-47)
  • publicProcedure (49-49)
apps/desktop/src/shared/types/workspace-init.ts (1)
  • WorkspaceInitProgress (17-23)
apps/desktop/src/main/lib/workspace-init-manager.ts (1)
  • workspaceInitManager (302-302)
apps/desktop/src/lib/trpc/routers/workspaces/utils/db-helpers.ts (2)
  • getWorkspaceWithRelations (168-184)
  • getProject (145-151)
apps/desktop/src/lib/trpc/routers/workspaces/utils/workspace-init.ts (1)
  • initializeWorkspaceWorktree (36-312)
apps/desktop/src/lib/trpc/routers/workspaces/utils/setup.ts (1)
  • loadSetupConfig (30-56)
apps/desktop/src/lib/trpc/routers/workspaces/utils/db-helpers.ts (2)
apps/desktop/src/main/lib/local-db/index.ts (1)
  • localDb (82-82)
packages/local-db/src/schema/schema.ts (7)
  • settings (123-139)
  • workspaces (80-118)
  • projects (16-42)
  • SelectProject (45-45)
  • SelectWorkspace (121-121)
  • SelectWorktree (75-75)
  • worktrees (50-72)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Build
🔇 Additional comments (12)
apps/desktop/src/lib/trpc/index.ts (1)

47-50: mergeRouters export is a clean public API improvement.
This makes the new modular router composition pattern more discoverable/consistent.

apps/desktop/src/lib/trpc/routers/workspaces/procedures/status.ts (2)

14-30: setActive flow looks solid (clears unread + persists last active).
Nice use of helpers to keep the procedure short.


73-93: Verify timestamp semantics: renaming shouldn’t necessarily bump lastOpenedAt; unread toggle likely should bump updatedAt.
touchWorkspace always updates lastOpenedAt (Line 88-90 usage), which may skew MRU behavior. setUnread doesn’t update updatedAt at all. Consider splitting helpers (e.g., updateWorkspaceMetadata vs markWorkspaceOpened).

Also applies to: 95-110

apps/desktop/src/lib/trpc/routers/workspaces/procedures/init.ts (1)

16-49: Subscription implementation matches trpc-electron observable requirements.
Good: sends current state, filters by ids, unsubscribes cleanly.

Based on coding guidelines, subscriptions should use @trpc/server/observable.

apps/desktop/src/lib/trpc/routers/workspaces/procedures/branch.ts (1)

58-103: switchBranchWorkspace flow is cohesive and well-factored.
Safe checkout + terminal refresh + DB updates are clear and sequenced correctly.

apps/desktop/src/lib/trpc/routers/workspaces/utils/workspace-init.ts (1)

307-311: The lock semantics are safe and do not require changes. acquireProjectLock is properly awaited (line 50) and does not throw—it waits in a loop. releaseProjectLock is synchronous and includes a guard (if (resolve)) making it a safe no-op if the lock was never acquired. No issue with the finally block.

Likely an incorrect or invalid review comment.

apps/desktop/src/lib/trpc/routers/workspaces/workspaces.ts (1)

1-34: Clean modular composition pattern.

The refactor successfully transforms a monolithic router into a thin orchestrator that merges domain-specific procedure modules. This aligns well with the project learnings to keep orchestrators thin and delegate to utility functions. The JSDoc documentation clearly maps procedures to their logical groups.

apps/desktop/src/lib/trpc/routers/workspaces/procedures/git-status.ts (1)

22-84: LGTM - refreshGitStatus mutation is well-structured.

The procedure correctly validates all required entities, handles default branch synchronization with remote, and persists the git status. The logic is at the ~60-line threshold but remains cohesive and readable as a single unit.

apps/desktop/src/lib/trpc/routers/workspaces/procedures/create.ts (2)

132-260: Race condition handling is well-implemented.

The createBranchWorkspace procedure correctly handles concurrent calls using onConflictDoNothing() combined with the unique partial index. The "insert-first, then shift" pattern prevents double-shifts from race conditions. Based on learnings, this ~130-line procedure could eventually be extracted into a utility function for independent testing, but the current inline approach is acceptable for now.


108-119: Error handling is already implemented internally.

The initializeWorkspaceWorktree function has comprehensive error handling: the entire function body is wrapped in a try-catch-finally block that catches errors, logs them with context ([workspace-init]), performs cleanup, updates progress state, and finalizes the job. No additional .catch() handler is needed at the call site.

apps/desktop/src/lib/trpc/routers/workspaces/procedures/delete.ts (2)

174-220: Project lock coordination is correctly implemented.

The lock acquisition in a try/finally block ensures proper release even if worktree removal fails. The cancellation coordination with workspaceInitManager properly prevents racing with concurrent init operations.


251-282: Close procedure is clean and consistent.

The close procedure correctly handles the simpler case of closing a workspace while preserving the worktree. The cleanup sequence (terminals → workspace record → project visibility → active workspace) is appropriate.

Comment thread apps/desktop/src/lib/trpc/routers/workspaces/procedures/branch.ts
Comment thread apps/desktop/src/lib/trpc/routers/workspaces/procedures/delete.ts
Comment on lines +30 to +100
getAllGrouped: publicProcedure.query(() => {
const activeProjects = localDb
.select()
.from(projects)
.where(isNotNull(projects.tabOrder))
.all();

const groupsMap = new Map<
string,
{
project: {
id: string;
name: string;
color: string;
tabOrder: number;
githubOwner: string | null;
mainRepoPath: string;
};
workspaces: Array<{
id: string;
projectId: string;
worktreeId: string | null;
worktreePath: string;
type: "worktree" | "branch";
branch: string;
name: string;
tabOrder: number;
createdAt: number;
updatedAt: number;
lastOpenedAt: number;
isUnread: boolean;
}>;
}
>();

for (const project of activeProjects) {
groupsMap.set(project.id, {
project: {
id: project.id,
name: project.name,
color: project.color,
// biome-ignore lint/style/noNonNullAssertion: filter guarantees tabOrder is not null
tabOrder: project.tabOrder!,
githubOwner: project.githubOwner ?? null,
mainRepoPath: project.mainRepoPath,
},
workspaces: [],
});
}

const allWorkspaces = localDb
.select()
.from(workspaces)
.all()
.sort((a, b) => a.tabOrder - b.tabOrder);

for (const workspace of allWorkspaces) {
if (groupsMap.has(workspace.projectId)) {
groupsMap.get(workspace.projectId)?.workspaces.push({
...workspace,
type: workspace.type as "worktree" | "branch",
worktreePath: getWorkspacePath(workspace) ?? "",
isUnread: workspace.isUnread ?? false,
});
}
}

return Array.from(groupsMap.values()).sort(
(a, b) => a.project.tabOrder - b.project.tabOrder,
);
}),
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

🧩 Analysis chain

🏁 Script executed:

# Find definition of getWorkspacePath
rg -n "function getWorkspacePath|const getWorkspacePath|export.*getWorkspacePath" --type=ts -A 5

Repository: superset-sh/superset

Length of output: 679


🏁 Script executed:

# Find all call sites of getWorkspacePath to understand usage patterns
rg -n "getWorkspacePath\(" --type=ts -B 2 -A 2

Repository: superset-sh/superset

Length of output: 2026


🏁 Script executed:

# Check the file structure to understand where getWorkspacePath is defined
fd "getWorkspacePath|workspace" --type=f --extension=ts --extension=tsx apps/desktop/src/lib

Repository: superset-sh/superset

Length of output: 191


🏁 Script executed:

# Read the full getWorkspacePath function implementation
cat -n apps/desktop/src/lib/trpc/routers/workspaces/utils/worktree.ts

Repository: superset-sh/superset

Length of output: 1481


getAllGrouped queries DB inside loop—eliminate N+1 reads.
getWorkspacePath() executes DB lookups per workspace in the loop (lines 91, 178). For branch workspaces, this re-queries projects that are already loaded in groupsMap; for worktree workspaces, it queries worktrees without preloading. Preload all worktrees once and pass project data directly to avoid repeated DB calls.

🤖 Prompt for AI Agents
In @apps/desktop/src/lib/trpc/routers/workspaces/procedures/query.ts around
lines 30 - 100, getAllGrouped currently calls getWorkspacePath per workspace
causing N+1 DB reads; instead preload related worktree rows once and use project
data already in groupsMap. Query all worktrees in one select (e.g.,
localDb.select().from(worktrees).all()) and build a map by worktree id, then in
the allWorkspaces loop avoid calling getWorkspacePath: for type === "worktree"
read the path from the preloaded worktreesMap, for type === "branch" use the
project's mainRepoPath from groupsMap.get(workspace.projectId).project (or
fallback ""), and remove any per-workspace localDb calls to getWorkspacePath or
projects to eliminate repeated DB access.

Comment on lines +134 to +173
// Detect and persist base branch for existing worktrees that don't have it
// We use undefined to mean "not yet attempted" and null to mean "attempted but not found"
let baseBranch = worktree?.baseBranch;
if (worktree && baseBranch === undefined && project) {
// Only attempt detection if there's a remote origin
const hasRemote = await hasOriginRemote(project.mainRepoPath);
if (hasRemote) {
try {
const defaultBranch = project.defaultBranch || "main";
const detected = await detectBaseBranch(
worktree.path,
worktree.branch,
defaultBranch,
);
if (detected) {
baseBranch = detected;
}
// Persist the result (detected branch or null sentinel)
localDb
.update(worktrees)
.set({ baseBranch: detected ?? null })
.where(eq(worktrees.id, worktree.id))
.run();
} catch {
// Detection failed, persist null to avoid retrying
localDb
.update(worktrees)
.set({ baseBranch: null })
.where(eq(worktrees.id, worktree.id))
.run();
}
} else {
// No remote - persist null to avoid retrying
localDb
.update(worktrees)
.set({ baseBranch: null })
.where(eq(worktrees.id, worktree.id))
.run();
}
}
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

🧩 Analysis chain

🏁 Script executed:

# Find the local-db schema definition for worktrees
find . -type f -name "*schema*" -o -name "*worktree*" | grep -E "\.(ts|tsx)$" | head -20

Repository: superset-sh/superset

Length of output: 339


🏁 Script executed:

# Search for worktrees table schema definition in the packages/local-db directory
fd -e ts -e tsx . packages/local-db/src | head -20

Repository: superset-sh/superset

Length of output: 249


🏁 Script executed:

# Look for the worktrees schema definition
rg -n "worktrees.*=" packages/local-db/src --type=ts -A 10 | head -50

Repository: superset-sh/superset

Length of output: 1657


🏁 Script executed:

# Read the full worktrees table schema definition
sed -n '50,100p' packages/local-db/src/schema/schema.ts

Repository: superset-sh/superset

Length of output: 1699


🏁 Script executed:

# Read the context around the code in question to see where worktree comes from
sed -n '120,175p' apps/desktop/src/lib/trpc/routers/workspaces/procedures/query.ts

Repository: superset-sh/superset

Length of output: 1673


Condition baseBranch === undefined is logically impossible and will never trigger detection for legacy rows.

The worktree's baseBranch column is nullable (text("base_branch") without .notNull()), which means SelectWorktree["baseBranch"] is inferred as string | null. When you query from the database, it can only be null or a string—never undefined.

Given the optional chaining worktree?.baseBranch, if worktree exists (truthy), then baseBranch can only be null or a string value. The condition worktree && baseBranch === undefined requires both worktree to be defined AND baseBranch to be undefined simultaneously, which is impossible.

This breaks the intended logic: detection never runs for legacy rows with null baseBranch (existing worktrees), only for completely new worktrees (where worktree doesn't exist yet). To fix this, change the check to baseBranch === null instead of undefined, or add a dedicated sentinel column (like baseBranchDetectionAttemptedAt) to distinguish "not yet attempted" from "attempted but not found."

🤖 Prompt for AI Agents
In @apps/desktop/src/lib/trpc/routers/workspaces/procedures/query.ts around
lines 134 - 173, The condition checking for "not yet attempted" uses baseBranch
=== undefined which is impossible because SelectWorktree["baseBranch"] is string
| null; change the logic in the block around worktree/baseBranch so that you
treat null as "not yet attempted" (i.e., check baseBranch === null) or introduce
a separate sentinel (e.g., baseBranchDetectionAttemptedAt) to distinguish
states; update the branch where detection runs (the code calling
hasOriginRemote(project.mainRepoPath), detectBaseBranch(worktree.path,
worktree.branch, defaultBranch), and the localDb.update(worktrees).set({
baseBranch: ... })) to use the new condition and persist either the detected
branch or null/appropriate sentinel consistently.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Jan 9, 2026

🧹 Preview Cleanup Complete

The following preview resources have been cleaned up:

  • ✅ Neon database branch
  • ✅ Electric Fly.io app

Thank you for your contribution! 🎉

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

🤖 Fix all issues with AI agents
In @apps/desktop/src/lib/trpc/routers/workspaces/procedures/delete.ts:
- Around line 230-233: The delete procedure currently calls
hideProjectIfNoWorkspaces(workspace.projectId) only when project is truthy,
causing inconsistency with close which calls it unconditionally; change the
delete flow (and the similar block around lines referenced 266-268) to call
hideProjectIfNoWorkspaces(workspace.projectId) unconditionally after workspace
deletion so the project is hidden regardless of a stale getProject result,
keeping the same call-site (hideProjectIfNoWorkspaces) and parameter.
- Around line 153-216: The log lines inside the delete procedure are
inconsistent: only the initial console.log has a “[workspace/delete]” prefix
while console.error and console.warn (e.g., the teardown failure log, "Failed to
remove worktree" log, and "Worktree ... not found" warn) lack it; also you
should handle terminalManager.killByWorkspaceId failures before proceeding.
Update all console.* calls in this flow to include the same "[workspace/delete]"
prefix (look for usages around workspaceInitManager,
terminalManager.killByWorkspaceId, runTeardown, removeWorktree and their related
error/warn logs) and add a check after terminalResult = await
terminalManager.killByWorkspaceId(input.id) to detect a failed kill (use the
terminalResult shape returned by killByWorkspaceId), log a prefixed error and
return { success: false, error: <message> } early so you don’t continue to
worktree teardown when terminals weren’t closed.
- Around line 141-248: The delete procedure returns { success: false, error: ...
} on failures while the close procedure throws tRPC errors, causing inconsistent
client handling; update the delete mutation (the publicProcedure named "delete"
in this router) to throw TRPCError instances instead of returning error objects
for cases like workspace not found, git/remove failures, and terminal-kill
failures (use TRPCError with appropriate codes such as NOT_FOUND or CONFLICT),
and ensure cleanup paths still call workspaceInitManager.clearJob and other
teardown logic inside finally blocks where needed so errors propagate as throws
rather than returned { success: false } objects.
- Around line 26-139: The canDelete procedure currently returns inconsistent
shapes (some branches omit warning) and computes activeTerminalCount but never
uses it; update the canDelete publicProcedure in delete.ts to always include the
warning field in every returned object (use null or a descriptive string) and
decide on terminal gating: if you want active terminals to block deletion, set
canDelete to false when terminalManager.getSessionCountByWorkspaceId(input.id) >
0 and return a reason like "Active terminals present" (ensure
activeTerminalCount is still returned); otherwise, explicitly document and
return warning:null and keep canDelete true — apply the chosen behavior across
all return paths including the workspace-not-found and catch(error) branches so
the response shape is uniform.
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f56b400 and 5beaa70.

📒 Files selected for processing (1)
  • apps/desktop/src/lib/trpc/routers/workspaces/procedures/delete.ts
🧰 Additional context used
📓 Path-based instructions (4)
apps/desktop/**/*.{ts,tsx}

📄 CodeRabbit inference engine (apps/desktop/AGENTS.md)

apps/desktop/**/*.{ts,tsx}: For Electron interprocess communication, ALWAYS use tRPC as defined in src/lib/trpc
Use alias as defined in tsconfig.json when possible
Prefer zustand for state management if it makes sense. Do not use effect unless absolutely necessary.
For tRPC subscriptions with trpc-electron, ALWAYS use the observable pattern from @trpc/server/observable instead of async generators, as the library explicitly checks isObservable(result) and throws an error otherwise

Files:

  • apps/desktop/src/lib/trpc/routers/workspaces/procedures/delete.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx}: Use object parameters for functions with 2+ parameters instead of positional arguments
Functions with 2+ parameters should accept a single params object with named properties for self-documentation and extensibility
Use prefixed console logging with context pattern: [domain/operation] message
Extract magic numbers and hardcoded values to named constants at module top
Use lookup objects/maps instead of repeated if (type === ...) conditionals
Avoid using any type - maintain type safety in TypeScript code
Never swallow errors silently - at minimum log them with context
Import from concrete files directly when possible - avoid barrel file abuse that creates circular dependencies
Avoid deep nesting (4+ levels) - use early returns, extract functions, and invert conditions
Use named properties in options objects instead of boolean parameters to avoid boolean blindness

Files:

  • apps/desktop/src/lib/trpc/routers/workspaces/procedures/delete.ts
apps/**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Use Drizzle ORM for all database operations - never use raw SQL

Files:

  • apps/desktop/src/lib/trpc/routers/workspaces/procedures/delete.ts
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (AGENTS.md)

Use Biome for formatting and linting - run at root level with bun run lint:fix or biome check --write

Files:

  • apps/desktop/src/lib/trpc/routers/workspaces/procedures/delete.ts
🧠 Learnings (2)
📓 Common learnings
Learnt from: CR
Repo: superset-sh/superset PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-02T06:50:28.671Z
Learning: Applies to apps/api/**/*.{ts,tsx} : Extract business logic from tRPC procedures into utility functions when logic exceeds ~50 lines, is used by multiple procedures, or needs independent testing
Learnt from: CR
Repo: superset-sh/superset PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-02T06:50:28.671Z
Learning: Co-locate code by lifecycle - feature-specific code lives together, not split by type (e.g., all task-related code in router/task/)
📚 Learning: 2026-01-02T06:50:28.671Z
Learnt from: CR
Repo: superset-sh/superset PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-02T06:50:28.671Z
Learning: Applies to apps/api/**/*.{ts,tsx} : Extract business logic from tRPC procedures into utility functions when logic exceeds ~50 lines, is used by multiple procedures, or needs independent testing

Applied to files:

  • apps/desktop/src/lib/trpc/routers/workspaces/procedures/delete.ts
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Build

Comment on lines +26 to +139
canDelete: publicProcedure
.input(
z.object({
id: z.string(),
// Skip expensive git checks (status, unpushed) during polling - only check terminal count
skipGitChecks: z.boolean().optional(),
}),
)
.query(async ({ input }) => {
const workspace = getWorkspace(input.id);

if (!workspace) {
return {
canDelete: false,
reason: "Workspace not found",
workspace: null,
activeTerminalCount: 0,
hasChanges: false,
hasUnpushedCommits: false,
};
}

const activeTerminalCount =
terminalManager.getSessionCountByWorkspaceId(input.id);

// Branch workspaces are non-destructive to close - no git checks needed
if (workspace.type === "branch") {
return {
canDelete: true,
reason: null,
workspace,
warning: null,
activeTerminalCount,
hasChanges: false,
hasUnpushedCommits: false,
};
}

// If skipping git checks, return early with just terminal count
// This is used during polling to avoid expensive git operations
if (input.skipGitChecks) {
return {
canDelete: true,
reason: null,
workspace,
warning: null,
activeTerminalCount,
hasChanges: false,
hasUnpushedCommits: false,
};
}

const worktree = workspace.worktreeId
? getWorktree(workspace.worktreeId)
: null;
const project = getProject(workspace.projectId);

if (worktree && project) {
try {
const exists = await worktreeExists(
project.mainRepoPath,
worktree.path,
);

if (!exists) {
return {
canDelete: true,
reason: null,
workspace,
warning:
"Worktree not found in git (may have been manually removed)",
activeTerminalCount,
hasChanges: false,
hasUnpushedCommits: false,
};
}

// Check for uncommitted changes and unpushed commits in parallel
const [hasChanges, unpushedCommits] = await Promise.all([
hasUncommittedChanges(worktree.path),
hasUnpushedCommits(worktree.path),
]);

return {
canDelete: true,
reason: null,
workspace,
warning: null,
activeTerminalCount,
hasChanges,
hasUnpushedCommits: unpushedCommits,
};
} catch (error) {
return {
canDelete: false,
reason: `Failed to check worktree status: ${error instanceof Error ? error.message : String(error)}`,
workspace,
activeTerminalCount,
hasChanges: false,
hasUnpushedCommits: false,
};
}
}

return {
canDelete: true,
reason: null,
workspace,
warning: "No associated worktree found",
activeTerminalCount,
hasChanges: false,
hasUnpushedCommits: false,
};
}),
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 canDelete response shape consistent (always include warning) and decide whether active terminals should block deletion.

Right now some branches omit warning (e.g., workspace-not-found and catch), and activeTerminalCount is computed but never affects canDelete, which can confuse polling/UI logic.

Proposed diff (shape consistency + optional terminal gating)
 			.query(async ({ input }) => {
 				const workspace = getWorkspace(input.id);

 				if (!workspace) {
 					return {
 						canDelete: false,
 						reason: "Workspace not found",
 						workspace: null,
+						warning: null,
 						activeTerminalCount: 0,
 						hasChanges: false,
 						hasUnpushedCommits: false,
 					};
 				}

 				const activeTerminalCount =
 					terminalManager.getSessionCountByWorkspaceId(input.id);

+				// Optional: if the intended UX is "don't allow delete/close while terminals are active"
+				// gate here so polling can disable the action.
+				// If you prefer to allow delete and just warn, return canDelete: true with warning instead.
+				if (activeTerminalCount > 0) {
+					return {
+						canDelete: false,
+						reason: "Close running terminals before deleting this workspace",
+						workspace,
+						warning: null,
+						activeTerminalCount,
+						hasChanges: false,
+						hasUnpushedCommits: false,
+					};
+				}

 				// Branch workspaces are non-destructive to close - no git checks needed
 				if (workspace.type === "branch") {
 					return {
 						canDelete: true,
 						reason: null,
 						workspace,
 						warning: null,
 						activeTerminalCount,
 						hasChanges: false,
 						hasUnpushedCommits: false,
 					};
 				}
 ...
 					} catch (error) {
 						return {
 							canDelete: false,
 							reason: `Failed to check worktree status: ${error instanceof Error ? error.message : String(error)}`,
 							workspace,
+							warning: null,
 							activeTerminalCount,
 							hasChanges: false,
 							hasUnpushedCommits: false,
 						};
 					}
🤖 Prompt for AI Agents
In @apps/desktop/src/lib/trpc/routers/workspaces/procedures/delete.ts around
lines 26 - 139, The canDelete procedure currently returns inconsistent shapes
(some branches omit warning) and computes activeTerminalCount but never uses it;
update the canDelete publicProcedure in delete.ts to always include the warning
field in every returned object (use null or a descriptive string) and decide on
terminal gating: if you want active terminals to block deletion, set canDelete
to false when terminalManager.getSessionCountByWorkspaceId(input.id) > 0 and
return a reason like "Active terminals present" (ensure activeTerminalCount is
still returned); otherwise, explicitly document and return warning:null and keep
canDelete true — apply the chosen behavior across all return paths including the
workspace-not-found and catch(error) branches so the response shape is uniform.

Comment on lines +141 to +248
delete: publicProcedure
.input(z.object({ id: z.string() }))
.mutation(async ({ input }) => {
const workspace = getWorkspace(input.id);

if (!workspace) {
return { success: false, error: "Workspace not found" };
}

// Cancel any ongoing initialization and wait for it to complete
// This ensures we don't race with init's git operations
if (workspaceInitManager.isInitializing(input.id)) {
console.log(
`[workspace/delete] Cancelling init for ${input.id}, waiting for completion...`,
);
workspaceInitManager.cancel(input.id);
// Wait for init to finish (up to 30s) - it will see cancellation and exit
await workspaceInitManager.waitForInit(input.id, 30000);
}

// Kill all terminal processes in this workspace first
const terminalResult = await terminalManager.killByWorkspaceId(
input.id,
);

const project = getProject(workspace.projectId);

let worktree: SelectWorktree | undefined;

// Branch workspaces don't have worktrees - skip worktree operations
if (workspace.type === "worktree" && workspace.worktreeId) {
worktree = getWorktree(workspace.worktreeId);

if (worktree && project) {
// Acquire project lock before any git operations
// This prevents racing with any concurrent init operations
await workspaceInitManager.acquireProjectLock(project.id);

try {
// Run teardown scripts before removing worktree
const exists = await worktreeExists(
project.mainRepoPath,
worktree.path,
);

if (exists) {
const teardownResult = await runTeardown(
project.mainRepoPath,
worktree.path,
workspace.name,
);
if (!teardownResult.success) {
console.error(
`Teardown failed for workspace ${workspace.name}:`,
teardownResult.error,
);
}
}

try {
if (exists) {
await removeWorktree(project.mainRepoPath, worktree.path);
} else {
console.warn(
`Worktree ${worktree.path} not found in git, skipping removal`,
);
}
} catch (error) {
const errorMessage =
error instanceof Error ? error.message : String(error);
console.error("Failed to remove worktree:", errorMessage);
return {
success: false,
error: `Failed to remove worktree: ${errorMessage}`,
};
}
} finally {
workspaceInitManager.releaseProjectLock(project.id);
}
}
}

// Proceed with DB cleanup
deleteWorkspace(input.id);

if (worktree) {
deleteWorktreeRecord(worktree.id);
}

if (project) {
hideProjectIfNoWorkspaces(workspace.projectId);
}

updateActiveWorkspaceIfRemoved(input.id);

const terminalWarning =
terminalResult.failed > 0
? `${terminalResult.failed} terminal process(es) may still be running`
: undefined;

track("workspace_deleted", { workspace_id: input.id });

// Clear init job state only after all cleanup is complete
// This ensures cancellation signals remain visible during cleanup
workspaceInitManager.clearJob(input.id);

return { success: true, terminalWarning };
}),
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

Unify error semantics between delete and close (throw tRPC errors vs { success: false }).

delete returns { success: false, error: ... } while close throws. Mixing these patterns within the same router tends to cause inconsistent client handling.

Proposed diff (minimal consistency: throw on not found)
 		delete: publicProcedure
 			.input(z.object({ id: z.string() }))
 			.mutation(async ({ input }) => {
 				const workspace = getWorkspace(input.id);

 				if (!workspace) {
-					return { success: false, error: "Workspace not found" };
+					throw new Error("Workspace not found");
 				}

If you already use TRPCError elsewhere, it may be better to throw that (e.g., NOT_FOUND, CONFLICT) for better client UX.

Also applies to: 250-280

🤖 Prompt for AI Agents
In @apps/desktop/src/lib/trpc/routers/workspaces/procedures/delete.ts around
lines 141 - 248, The delete procedure returns { success: false, error: ... } on
failures while the close procedure throws tRPC errors, causing inconsistent
client handling; update the delete mutation (the publicProcedure named "delete"
in this router) to throw TRPCError instances instead of returning error objects
for cases like workspace not found, git/remove failures, and terminal-kill
failures (use TRPCError with appropriate codes such as NOT_FOUND or CONFLICT),
and ensure cleanup paths still call workspaceInitManager.clearJob and other
teardown logic inside finally blocks where needed so errors propagate as throws
rather than returned { success: false } objects.

Comment on lines +150 to +160
// Cancel any ongoing initialization and wait for it to complete
// This ensures we don't race with init's git operations
if (workspaceInitManager.isInitializing(input.id)) {
console.log(
`[workspace/delete] Cancelling init for ${input.id}, waiting for completion...`,
);
workspaceInitManager.cancel(input.id);
// Wait for init to finish (up to 30s) - it will see cancellation and exit
await workspaceInitManager.waitForInit(input.id, 30000);
}

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

Handle waitForInit timeout/failure explicitly so deletes don’t hang or fail unclearly.

If waitForInit(input.id, 30000) rejects (timeout/other), the mutation will throw without a clear “what to do next” error, and without returning terminalWarning etc.

Proposed diff (explicit error message)
 				if (workspaceInitManager.isInitializing(input.id)) {
 					console.log(
 						`[workspace/delete] Cancelling init for ${input.id}, waiting for completion...`,
 					);
 					workspaceInitManager.cancel(input.id);
-					// Wait for init to finish (up to 30s) - it will see cancellation and exit
-					await workspaceInitManager.waitForInit(input.id, 30000);
+					try {
+						// Wait for init to finish (up to 30s) - it will see cancellation and exit
+						await workspaceInitManager.waitForInit(input.id, 30000);
+					} catch (error) {
+						throw new Error(
+							`Timed out waiting for workspace init to stop: ${
+								error instanceof Error ? error.message : String(error)
+							}`,
+						);
+					}
 				}
📝 Committable suggestion

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

Suggested change
// Cancel any ongoing initialization and wait for it to complete
// This ensures we don't race with init's git operations
if (workspaceInitManager.isInitializing(input.id)) {
console.log(
`[workspace/delete] Cancelling init for ${input.id}, waiting for completion...`,
);
workspaceInitManager.cancel(input.id);
// Wait for init to finish (up to 30s) - it will see cancellation and exit
await workspaceInitManager.waitForInit(input.id, 30000);
}
// Cancel any ongoing initialization and wait for it to complete
// This ensures we don't race with init's git operations
if (workspaceInitManager.isInitializing(input.id)) {
console.log(
`[workspace/delete] Cancelling init for ${input.id}, waiting for completion...`,
);
workspaceInitManager.cancel(input.id);
try {
// Wait for init to finish (up to 30s) - it will see cancellation and exit
await workspaceInitManager.waitForInit(input.id, 30000);
} catch (error) {
throw new Error(
`Timed out waiting for workspace init to stop: ${
error instanceof Error ? error.message : String(error)
}`,
);
}
}

Comment on lines +153 to +216
console.log(
`[workspace/delete] Cancelling init for ${input.id}, waiting for completion...`,
);
workspaceInitManager.cancel(input.id);
// Wait for init to finish (up to 30s) - it will see cancellation and exit
await workspaceInitManager.waitForInit(input.id, 30000);
}

// Kill all terminal processes in this workspace first
const terminalResult = await terminalManager.killByWorkspaceId(
input.id,
);

const project = getProject(workspace.projectId);

let worktree: SelectWorktree | undefined;

// Branch workspaces don't have worktrees - skip worktree operations
if (workspace.type === "worktree" && workspace.worktreeId) {
worktree = getWorktree(workspace.worktreeId);

if (worktree && project) {
// Acquire project lock before any git operations
// This prevents racing with any concurrent init operations
await workspaceInitManager.acquireProjectLock(project.id);

try {
// Run teardown scripts before removing worktree
const exists = await worktreeExists(
project.mainRepoPath,
worktree.path,
);

if (exists) {
const teardownResult = await runTeardown(
project.mainRepoPath,
worktree.path,
workspace.name,
);
if (!teardownResult.success) {
console.error(
`Teardown failed for workspace ${workspace.name}:`,
teardownResult.error,
);
}
}

try {
if (exists) {
await removeWorktree(project.mainRepoPath, worktree.path);
} else {
console.warn(
`Worktree ${worktree.path} not found in git, skipping removal`,
);
}
} catch (error) {
const errorMessage =
error instanceof Error ? error.message : String(error);
console.error("Failed to remove worktree:", errorMessage);
return {
success: false,
error: `Failed to remove worktree: ${errorMessage}`,
};
}
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

Prefix all console logs with context (and consider an early return when terminal kill fails).

Only the console.log has a [workspace/delete] prefix; the other console.error / console.warn lines don’t. Also, if terminal kill failures are common, attempting worktree removal right after may be noisy (file locks) and could benefit from an early “close terminals first” return.

🤖 Prompt for AI Agents
In @apps/desktop/src/lib/trpc/routers/workspaces/procedures/delete.ts around
lines 153 - 216, The log lines inside the delete procedure are inconsistent:
only the initial console.log has a “[workspace/delete]” prefix while
console.error and console.warn (e.g., the teardown failure log, "Failed to
remove worktree" log, and "Worktree ... not found" warn) lack it; also you
should handle terminalManager.killByWorkspaceId failures before proceeding.
Update all console.* calls in this flow to include the same "[workspace/delete]"
prefix (look for usages around workspaceInitManager,
terminalManager.killByWorkspaceId, runTeardown, removeWorktree and their related
error/warn logs) and add a check after terminalResult = await
terminalManager.killByWorkspaceId(input.id) to detect a failed kill (use the
terminalResult shape returned by killByWorkspaceId), log a prefixed error and
return { success: false, error: <message> } early so you don’t continue to
worktree teardown when terminals weren’t closed.

Comment on lines +230 to +233
if (project) {
hideProjectIfNoWorkspaces(workspace.projectId);
}

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

Call hideProjectIfNoWorkspaces consistently (likely no need to guard by project).

close calls hideProjectIfNoWorkspaces(workspace.projectId) unconditionally, but delete only calls it when project is truthy—despite passing only the ID. If getProject is briefly stale, delete may leave the project visible while close would hide it.

Proposed diff
-				if (project) {
-					hideProjectIfNoWorkspaces(workspace.projectId);
-				}
+				hideProjectIfNoWorkspaces(workspace.projectId);

Also applies to: 266-268

🤖 Prompt for AI Agents
In @apps/desktop/src/lib/trpc/routers/workspaces/procedures/delete.ts around
lines 230 - 233, The delete procedure currently calls
hideProjectIfNoWorkspaces(workspace.projectId) only when project is truthy,
causing inconsistency with close which calls it unconditionally; change the
delete flow (and the similar block around lines referenced 266-268) to call
hideProjectIfNoWorkspaces(workspace.projectId) unconditionally after workspace
deletion so the project is hidden regardless of a stale getProject result,
keeping the same call-site (hideProjectIfNoWorkspaces) and parameter.

@Kitenite Kitenite merged commit 6d5dcf0 into main Jan 9, 2026
5 checks passed
@Kitenite Kitenite deleted the refactor-workspace-file branch January 9, 2026 04:12
saddlepaddle pushed a commit that referenced this pull request Jan 10, 2026
**refactor(desktop): modularize workspaces router**

  Split the large workspaces.ts router (~1560 lines) into focused modules:
  - Extract db helpers (getWorkspace, touchWorkspace, activateProject, etc.)
  - Organize procedures by domain (create, delete, query, branch, git-status, status, init)
  - Fix potential race condition in teardown script
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.

1 participant