Skip to content

feat(desktop): replace lowdb with SQLite for local database#477

Merged
saddlepaddle merged 5 commits intomainfrom
homely-quail-98f31a
Dec 23, 2025
Merged

feat(desktop): replace lowdb with SQLite for local database#477
saddlepaddle merged 5 commits intomainfrom
homely-quail-98f31a

Conversation

@saddlepaddle
Copy link
Copy Markdown
Collaborator

@saddlepaddle saddlepaddle commented Dec 22, 2025

Summary

  • Adds @superset/local-db package with Drizzle ORM schema for SQLite
  • Migrates all database operations from lowdb (db.json) to SQLite using better-sqlite3
  • Updates all tRPC routers to use Drizzle queries instead of lowdb
  • Removes lowdb dependency
  • Adds UUID defaults for all ID fields (prep for local-first architecture)

Changes

  • New package: packages/local-db/ - Drizzle schema, relations, and Zod validators
  • Desktop app: Updated all routers (projects, workspaces, terminal, settings, external, config, branches) to use SQLite
  • Native modules: Added better-sqlite3 support for Electron builds
  • Cleanup: Removed old src/main/lib/db/ folder and lowdb dependency

Test plan

  • Typecheck passes
  • App starts and creates SQLite database
  • Projects, workspaces, and worktrees persist correctly
  • Settings (ringtones, terminal presets) work
  • Notifications look up workspace names correctly

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Desktop app now uses a local SQLite-backed database with automatic migration from the legacy JSON store.
    • Packaging updated to include native DB binaries and to bundle database migrations into app resources.
  • Chores

    • App-wide data layer migrated to a structured ORM-backed local-db package and updated to use new schemas.
    • Legacy in-file DB and old schema exports removed; build and type tooling for local-db added.

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

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Dec 22, 2025

Warning

Rate limit exceeded

@saddlepaddle has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 12 minutes and 49 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

📥 Commits

Reviewing files that changed from the base of the PR and between 236dfff and aa81186.

⛔ Files ignored due to path filters (1)
  • bun.lock is excluded by !**/*.lock
📒 Files selected for processing (44)
  • apps/admin/package.json
  • apps/admin/src/proxy.ts
  • apps/desktop/electron-builder.ts
  • apps/desktop/electron.vite.config.ts
  • apps/desktop/package.json
  • apps/desktop/scripts/copy-native-modules.ts
  • apps/desktop/src/lib/trpc/routers/changes/branches.ts
  • apps/desktop/src/lib/trpc/routers/config/config.ts
  • apps/desktop/src/lib/trpc/routers/external/helpers.ts
  • apps/desktop/src/lib/trpc/routers/external/index.ts
  • apps/desktop/src/lib/trpc/routers/projects/projects.ts
  • apps/desktop/src/lib/trpc/routers/settings/index.ts
  • apps/desktop/src/lib/trpc/routers/terminal/terminal.ts
  • apps/desktop/src/lib/trpc/routers/workspaces/utils/github/github.ts
  • apps/desktop/src/lib/trpc/routers/workspaces/utils/worktree.ts
  • apps/desktop/src/lib/trpc/routers/workspaces/workspaces.test.ts
  • apps/desktop/src/lib/trpc/routers/workspaces/workspaces.ts
  • apps/desktop/src/main/index.ts
  • apps/desktop/src/main/lib/db/index.ts
  • apps/desktop/src/main/lib/db/schemas.ts
  • apps/desktop/src/main/lib/local-db/index.ts
  • apps/desktop/src/main/lib/notification-sound.ts
  • apps/desktop/src/main/windows/main.ts
  • apps/desktop/src/renderer/components/OpenInButton/OpenInButton.tsx
  • apps/desktop/src/renderer/screens/main/components/SettingsView/PresetsSettings/types.ts
  • apps/desktop/src/renderer/screens/main/components/TopBar/WorkspaceTabs/WorkspaceHoverCard/components/ChecksList/ChecksList.tsx
  • apps/desktop/src/renderer/screens/main/components/TopBar/WorkspaceTabs/WorkspaceHoverCard/components/ChecksList/components/CheckItemRow/CheckItemRow.tsx
  • apps/desktop/src/renderer/screens/main/components/TopBar/WorkspaceTabs/WorkspaceHoverCard/components/ChecksSummary/ChecksSummary.tsx
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/Sidebar/TabsView/TabsCommandDialog/index.tsx
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/Sidebar/TabsView/index.tsx
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/WorkspaceFooter/components/WorkspaceFooterRight/WorkspaceFooterRight.tsx
  • apps/desktop/test-setup.ts
  • packages/db/src/index.ts
  • packages/local-db/drizzle.config.ts
  • packages/local-db/drizzle/0000_initial_schema.sql
  • packages/local-db/drizzle/meta/0000_snapshot.json
  • packages/local-db/drizzle/meta/_journal.json
  • packages/local-db/package.json
  • packages/local-db/src/index.ts
  • packages/local-db/src/schema/index.ts
  • packages/local-db/src/schema/relations.ts
  • packages/local-db/src/schema/schema.ts
  • packages/local-db/src/schema/zod.ts
  • packages/local-db/tsconfig.json

Walkthrough

Replaces legacy lowdb JSON persistence with a SQLite-backed local DB (better-sqlite3 + Drizzle) via a new @superset/local-db package (schema, relations, zod, migrations); migrates legacy data at startup; updates desktop routers, main process, renderer type imports, build, and tests to use the new localDb API.

Changes

Cohort / File(s) Summary
Package & deps
apps/admin/package.json, apps/desktop/package.json, packages/local-db/package.json
Add drizzle-orm, better-sqlite3, @superset/local-db; add @types/better-sqlite3; remove lowdb; add workspace link for @superset/local-db.
Build & packaging
apps/desktop/electron-builder.ts, apps/desktop/electron.vite.config.ts, apps/desktop/scripts/copy-native-modules.ts
Mark better-sqlite3 as external/unpacked; add copy rules for migrations and native module; adjust sounds copy logic and destination cleanup.
New local-db package (schema & tooling)
packages/local-db/src/schema/*, packages/local-db/drizzle.config.ts, packages/local-db/src/index.ts, packages/local-db/tsconfig.json, packages/local-db/package.json
New Drizzle schema (projects, worktrees, workspaces, settings), Zod types (GitStatus, CheckItem, TerminalPreset, ExternalApp, etc.), relations, Drizzle config, exports and TS config.
Migrations / Drizzle metadata
packages/local-db/drizzle/0000_initial_schema.sql, packages/local-db/drizzle/meta/*
Add initial SQL migration, schema snapshot, and journal metadata.
Local DB init & migration
apps/desktop/src/main/lib/local-db/index.ts, apps/desktop/src/main/index.ts
New module initializing SQLite (better-sqlite3) + Drizzle, running migrations, exposing localDb, and migrating legacy lowdb db.json into SQLite on startup; initDb removed and startup now imports localDb.
Removed legacy DB layer
apps/desktop/src/main/lib/db/index.ts, apps/desktop/src/main/lib/db/schemas.ts
Delete lowdb-based DB init/proxy and all in-file JSON schema/types; consumers migrated to @superset/local-db.
Router rewrites to Drizzle/localDb
apps/desktop/src/lib/trpc/routers/... (projects, workspaces, settings, terminal, external, config, changes/branches, etc.)
Replace direct in-memory/lowdb reads/writes with localDb Drizzle queries/inserts/updates/deletes; import eq, desc, isNotNull where needed; preserve router signatures.
Main process & notifications
apps/desktop/src/main/windows/main.ts, apps/desktop/src/main/lib/notification-sound.ts
Switch workspace/worktree/settings lookups to localDb selects and update imports.
Renderer type import updates
apps/desktop/src/renderer/... (OpenInButton, PresetsSettings, ChecksList, CheckItemRow, ChecksSummary, TabsCommandDialog, TabsView, WorkspaceFooterRight, etc.)
Update type-only imports (ExternalApp, TerminalPreset, CheckItem) to @superset/local-db.
Admin proxy import change
apps/admin/src/proxy.ts
Move eq import to drizzle-orm and adjust client DB import path.
DB operator exports cleanup
packages/db/src/index.ts
Remove re-exports of several Drizzle operators (and, eq, gt, gte, inArray, lt, lte, ne, not, or).
Tests & test setup
apps/desktop/src/lib/trpc/routers/workspaces/workspaces.test.ts (deleted), apps/desktop/test-setup.ts
Delete comprehensive workspaces test file; extend test setup with Electron mocks and add a main/lib/local-db mock implementing chainable Drizzle-like methods for tests.

Sequence Diagram(s)

sequenceDiagram
    autonumber
    participant App as App Startup
    participant LocalDbMod as `@superset/local-db` (module init)
    participant LegacyJson as legacy db.json
    participant Sqlite as SQLite (better-sqlite3)
    participant Routers as tRPC Routers
    participant Renderer as Renderer/UI

    App->>LocalDbMod: import localDb (module top-level)
    LocalDbMod->>Sqlite: open connection (better-sqlite3, WAL)
    LocalDbMod->>Sqlite: run Drizzle migrations
    alt legacy db.json exists && sqlite empty
        LocalDbMod->>LegacyJson: read legacy data (projects/worktrees/workspaces/settings)
        LocalDbMod->>Sqlite: insert migrated rows into tables
        LocalDbMod->>LegacyJson: backup legacy file (db.json.backup)
    end
    LocalDbMod-->>App: localDb ready
    Routers->>LocalDbMod: query/mutate via localDb.select/insert/update/delete
    LocalDbMod->>Sqlite: execute SQL statements
    Routers-->>Renderer: return data/results
    Renderer->>Renderer: render UI
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main change: replacing lowdb with SQLite for local database persistence, which is the primary objective of the PR.
Description check ✅ Passed The description covers the main changes, related package updates, and includes a test plan with checkmarks, though it lacks explicit related issues links and detailed screenshots.
Docstring Coverage ✅ Passed Docstring coverage is 80.00% which is sufficient. The required threshold is 80.00%.

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.

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

🧹 Nitpick comments (8)
apps/desktop/src/lib/trpc/routers/workspaces/utils/worktree.ts (1)

26-46: Standardize null/undefined handling in path helper functions.

The return type inconsistency between getWorktreePath (returns string | undefined) and getWorkspacePath (returns string | null) can cause confusion for callers. Standardize both functions to return either null or undefined consistently.

SelectWorkspace correctly includes all required properties (type, projectId, worktreeId), with worktreeId appropriately optional since it's only set for worktree-type workspaces.

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

64-73: Consider verifying update affected a row.

The dismissConfigToast mutation always returns { success: true } without verifying if the update actually modified a row. If the projectId doesn't exist, the operation silently succeeds.

This may be intentional (fire-and-forget semantics), but if you need confirmation:

🔎 Optional: Verify update result
 		dismissConfigToast: publicProcedure
 			.input(z.object({ projectId: z.string() }))
 			.mutation(({ input }) => {
-				localDb
+				const result = localDb
 					.update(projects)
 					.set({ configToastDismissed: true })
 					.where(eq(projects.id, input.projectId))
 					.run();
-				return { success: true };
+				return { success: result.changes > 0 };
 			}),
apps/desktop/src/main/lib/local-db/index.ts (2)

1-12: Organize imports as flagged by the linter.

The pipeline indicates imports need sorting. Additionally, consider grouping external dependencies separately from local imports.

🔎 Suggested import organization
 import * as schema from "@superset/local-db";
 import { projects, settings, workspaces, worktrees } from "@superset/local-db";
 import Database from "better-sqlite3";
 import { count } from "drizzle-orm";
 import { drizzle } from "drizzle-orm/better-sqlite3";
 import { migrate } from "drizzle-orm/better-sqlite3/migrator";
 import { app } from "electron";
 import { existsSync, readFileSync, renameSync } from "node:fs";
 import { join } from "node:path";
 
+import { env } from "../../env.main";
 import { DB_PATH as LEGACY_DB_PATH, SUPERSET_HOME_DIR } from "../app-environment";
-import { env } from "../../env.main";

72-82: Consider checking all tables before declaring "empty".

The migration only checks if the projects table is empty. If a partial migration occurred previously (e.g., projects migrated but settings failed), running again would skip migration entirely, potentially leaving settings unmigrated.

🔎 More thorough emptiness check
 function migrateFromLegacyDb(): void {
 	if (!existsSync(LEGACY_DB_PATH)) {
 		return;
 	}
 
-	// Check if SQLite is empty
+	// Check if SQLite is empty (check projects as primary indicator)
 	const projectCount = localDb.select({ count: count() }).from(projects).get();
 	if (projectCount && projectCount.count > 0) {
 		console.log("[local-db] SQLite already has data, skipping legacy migration");
 		return;
 	}
+
+	// Also skip if legacy file was already backed up
+	if (existsSync(`${LEGACY_DB_PATH}.backup`)) {
+		console.log("[local-db] Legacy backup exists, skipping migration");
+		return;
+	}
apps/desktop/src/lib/trpc/routers/projects/projects.ts (2)

1-14: Organize imports as flagged by the linter.


520-546: Simplify query and consider transaction for atomicity.

  1. The WHERE clause eq(projects.tabOrder, projects.tabOrder) is a tautology (always true for non-null) and doesn't filter at the DB level. The filtering happens in JS (line 525). Just remove it:

  2. The loop of individual updates could leave tabOrder in an inconsistent state if interrupted. Wrap in a transaction for atomicity.

🔎 Suggested improvements
 .mutation(({ input }) => {
 	const { fromIndex, toIndex } = input;
 
 	const activeProjects = localDb
 		.select()
 		.from(projects)
-		.where(eq(projects.tabOrder, projects.tabOrder)) // Just get all with non-null tabOrder
 		.all()
 		.filter((p) => p.tabOrder !== null)
 		.sort((a, b) => (a.tabOrder ?? 0) - (b.tabOrder ?? 0));
 
 	if (
 		fromIndex < 0 ||
 		fromIndex >= activeProjects.length ||
 		toIndex < 0 ||
 		toIndex >= activeProjects.length
 	) {
 		throw new Error("Invalid fromIndex or toIndex");
 	}
 
 	const [removed] = activeProjects.splice(fromIndex, 1);
 	activeProjects.splice(toIndex, 0, removed);
 
+	// Update all tabOrders atomically
+	localDb.transaction((tx) => {
 		for (let i = 0; i < activeProjects.length; i++) {
-			localDb
+			tx
 				.update(projects)
 				.set({ tabOrder: i })
 				.where(eq(projects.id, activeProjects[i].id))
 				.run();
 		}
+	});
 
 	return { success: true };
 }),
apps/desktop/src/lib/trpc/routers/settings/index.ts (1)

39-59: Consider using direct update since getSettings() guarantees the row exists.

Since getSettings() creates the row if it doesn't exist, the subsequent insert().onConflictDoUpdate() will always hit the conflict case. A direct update() would be cleaner and more efficient, though the current approach is functionally correct.

🔎 Optional: Use direct update
 .mutation(({ input }) => {
 	const preset: TerminalPreset = {
 		id: crypto.randomUUID(),
 		...input,
 	};
 
 	const row = getSettings();
 	const presets = row.terminalPresets ?? [];
 	presets.push(preset);
 
-	localDb
-		.insert(settings)
-		.values({ id: 1, terminalPresets: presets })
-		.onConflictDoUpdate({
-			target: settings.id,
-			set: { terminalPresets: presets },
-		})
-		.run();
+	localDb
+		.update(settings)
+		.set({ terminalPresets: presets })
+		.where(eq(settings.id, 1))
+		.run();
 
 	return preset;
 }),

Note: This would require importing eq from drizzle-orm.

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

139-147: Consider optimizing tab order calculation.

The current approach fetches all workspaces to calculate the maximum tab order. For better performance with many workspaces, consider using Drizzle's max() aggregation function.

🔎 Proposed optimization
+import { max } from "drizzle-orm";

 // Get max tab order for this project's workspaces
-const projectWorkspaces = localDb
+const result = localDb
 	.select()
+	.select({ maxTabOrder: max(workspaces.tabOrder) })
 	.from(workspaces)
 	.where(eq(workspaces.projectId, input.projectId))
-	.all();
-const maxTabOrder =
-	projectWorkspaces.length > 0
-		? Math.max(...projectWorkspaces.map((w) => w.tabOrder))
-		: -1;
+	.get();
+const maxTabOrder = result?.maxTabOrder ?? -1;

Note: This same pattern is used in multiple places (lines 222-230, 292-296, 328-337, etc.) and could benefit from extraction into a helper function.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7bffcc6 and b377b9f.

⛔ Files ignored due to path filters (1)
  • bun.lock is excluded by !**/*.lock
📒 Files selected for processing (42)
  • apps/admin/package.json
  • apps/admin/src/proxy.ts
  • apps/desktop/electron-builder.ts
  • apps/desktop/electron.vite.config.ts
  • apps/desktop/package.json
  • apps/desktop/scripts/copy-native-modules.ts
  • apps/desktop/src/lib/trpc/routers/changes/branches.ts
  • apps/desktop/src/lib/trpc/routers/config/config.ts
  • apps/desktop/src/lib/trpc/routers/external/helpers.ts
  • apps/desktop/src/lib/trpc/routers/external/index.ts
  • apps/desktop/src/lib/trpc/routers/projects/projects.ts
  • apps/desktop/src/lib/trpc/routers/settings/index.ts
  • apps/desktop/src/lib/trpc/routers/terminal/terminal.ts
  • apps/desktop/src/lib/trpc/routers/workspaces/utils/github/github.ts
  • apps/desktop/src/lib/trpc/routers/workspaces/utils/worktree.ts
  • apps/desktop/src/lib/trpc/routers/workspaces/workspaces.ts
  • apps/desktop/src/main/index.ts
  • apps/desktop/src/main/lib/db/index.ts
  • apps/desktop/src/main/lib/db/schemas.ts
  • apps/desktop/src/main/lib/local-db/index.ts
  • apps/desktop/src/main/lib/notification-sound.ts
  • apps/desktop/src/main/windows/main.ts
  • apps/desktop/src/renderer/components/OpenInButton/OpenInButton.tsx
  • apps/desktop/src/renderer/screens/main/components/SettingsView/PresetsSettings/types.ts
  • apps/desktop/src/renderer/screens/main/components/TopBar/WorkspaceTabs/WorkspaceHoverCard/components/ChecksList/ChecksList.tsx
  • apps/desktop/src/renderer/screens/main/components/TopBar/WorkspaceTabs/WorkspaceHoverCard/components/ChecksList/components/CheckItemRow/CheckItemRow.tsx
  • apps/desktop/src/renderer/screens/main/components/TopBar/WorkspaceTabs/WorkspaceHoverCard/components/ChecksSummary/ChecksSummary.tsx
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/Sidebar/TabsView/TabsCommandDialog/index.tsx
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/Sidebar/TabsView/index.tsx
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/WorkspaceFooter/components/WorkspaceFooterRight/WorkspaceFooterRight.tsx
  • packages/db/src/index.ts
  • packages/local-db/drizzle.config.ts
  • packages/local-db/drizzle/0000_initial_schema.sql
  • packages/local-db/drizzle/meta/0000_snapshot.json
  • packages/local-db/drizzle/meta/_journal.json
  • packages/local-db/package.json
  • packages/local-db/src/index.ts
  • packages/local-db/src/schema/index.ts
  • packages/local-db/src/schema/relations.ts
  • packages/local-db/src/schema/schema.ts
  • packages/local-db/src/schema/zod.ts
  • packages/local-db/tsconfig.json
💤 Files with no reviewable changes (3)
  • apps/desktop/src/main/lib/db/schemas.ts
  • apps/desktop/src/main/lib/db/index.ts
  • packages/db/src/index.ts
🧰 Additional context used
📓 Path-based instructions (11)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Avoid using any type in TypeScript - maintain type safety unless absolutely necessary

Files:

  • apps/desktop/src/lib/trpc/routers/workspaces/utils/github/github.ts
  • packages/local-db/src/index.ts
  • apps/desktop/src/renderer/screens/main/components/TopBar/WorkspaceTabs/WorkspaceHoverCard/components/ChecksList/components/CheckItemRow/CheckItemRow.tsx
  • apps/desktop/src/renderer/screens/main/components/TopBar/WorkspaceTabs/WorkspaceHoverCard/components/ChecksSummary/ChecksSummary.tsx
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/Sidebar/TabsView/index.tsx
  • apps/desktop/src/renderer/screens/main/components/SettingsView/PresetsSettings/types.ts
  • apps/desktop/src/main/lib/local-db/index.ts
  • apps/desktop/src/main/lib/notification-sound.ts
  • apps/desktop/scripts/copy-native-modules.ts
  • apps/desktop/src/main/windows/main.ts
  • apps/desktop/src/renderer/components/OpenInButton/OpenInButton.tsx
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/Sidebar/TabsView/TabsCommandDialog/index.tsx
  • apps/desktop/src/lib/trpc/routers/workspaces/utils/worktree.ts
  • apps/desktop/electron.vite.config.ts
  • packages/local-db/src/schema/relations.ts
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/WorkspaceFooter/components/WorkspaceFooterRight/WorkspaceFooterRight.tsx
  • apps/desktop/src/lib/trpc/routers/terminal/terminal.ts
  • apps/desktop/electron-builder.ts
  • packages/local-db/src/schema/index.ts
  • apps/desktop/src/renderer/screens/main/components/TopBar/WorkspaceTabs/WorkspaceHoverCard/components/ChecksList/ChecksList.tsx
  • packages/local-db/drizzle.config.ts
  • apps/desktop/src/main/index.ts
  • packages/local-db/src/schema/schema.ts
  • apps/desktop/src/lib/trpc/routers/external/index.ts
  • apps/desktop/src/lib/trpc/routers/settings/index.ts
  • apps/desktop/src/lib/trpc/routers/external/helpers.ts
  • apps/admin/src/proxy.ts
  • apps/desktop/src/lib/trpc/routers/changes/branches.ts
  • packages/local-db/src/schema/zod.ts
  • apps/desktop/src/lib/trpc/routers/config/config.ts
  • apps/desktop/src/lib/trpc/routers/workspaces/workspaces.ts
  • apps/desktop/src/lib/trpc/routers/projects/projects.ts
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (AGENTS.md)

Run Biome for formatting, linting, import organization, and safe fixes at the root level using bun run lint:fix

Files:

  • apps/desktop/src/lib/trpc/routers/workspaces/utils/github/github.ts
  • packages/local-db/src/index.ts
  • apps/desktop/src/renderer/screens/main/components/TopBar/WorkspaceTabs/WorkspaceHoverCard/components/ChecksList/components/CheckItemRow/CheckItemRow.tsx
  • apps/desktop/src/renderer/screens/main/components/TopBar/WorkspaceTabs/WorkspaceHoverCard/components/ChecksSummary/ChecksSummary.tsx
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/Sidebar/TabsView/index.tsx
  • apps/desktop/src/renderer/screens/main/components/SettingsView/PresetsSettings/types.ts
  • apps/desktop/src/main/lib/local-db/index.ts
  • apps/desktop/src/main/lib/notification-sound.ts
  • apps/desktop/scripts/copy-native-modules.ts
  • apps/desktop/src/main/windows/main.ts
  • apps/desktop/src/renderer/components/OpenInButton/OpenInButton.tsx
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/Sidebar/TabsView/TabsCommandDialog/index.tsx
  • apps/desktop/src/lib/trpc/routers/workspaces/utils/worktree.ts
  • apps/desktop/electron.vite.config.ts
  • packages/local-db/src/schema/relations.ts
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/WorkspaceFooter/components/WorkspaceFooterRight/WorkspaceFooterRight.tsx
  • apps/desktop/src/lib/trpc/routers/terminal/terminal.ts
  • apps/desktop/electron-builder.ts
  • packages/local-db/src/schema/index.ts
  • apps/desktop/src/renderer/screens/main/components/TopBar/WorkspaceTabs/WorkspaceHoverCard/components/ChecksList/ChecksList.tsx
  • packages/local-db/drizzle.config.ts
  • apps/desktop/src/main/index.ts
  • packages/local-db/src/schema/schema.ts
  • apps/desktop/src/lib/trpc/routers/external/index.ts
  • apps/desktop/src/lib/trpc/routers/settings/index.ts
  • apps/desktop/src/lib/trpc/routers/external/helpers.ts
  • apps/admin/src/proxy.ts
  • apps/desktop/src/lib/trpc/routers/changes/branches.ts
  • packages/local-db/src/schema/zod.ts
  • apps/desktop/src/lib/trpc/routers/config/config.ts
  • apps/desktop/src/lib/trpc/routers/workspaces/workspaces.ts
  • apps/desktop/src/lib/trpc/routers/projects/projects.ts
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/utils/github/github.ts
  • apps/desktop/src/renderer/screens/main/components/TopBar/WorkspaceTabs/WorkspaceHoverCard/components/ChecksList/components/CheckItemRow/CheckItemRow.tsx
  • apps/desktop/src/renderer/screens/main/components/TopBar/WorkspaceTabs/WorkspaceHoverCard/components/ChecksSummary/ChecksSummary.tsx
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/Sidebar/TabsView/index.tsx
  • apps/desktop/src/renderer/screens/main/components/SettingsView/PresetsSettings/types.ts
  • apps/desktop/src/main/lib/local-db/index.ts
  • apps/desktop/src/main/lib/notification-sound.ts
  • apps/desktop/scripts/copy-native-modules.ts
  • apps/desktop/src/main/windows/main.ts
  • apps/desktop/src/renderer/components/OpenInButton/OpenInButton.tsx
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/Sidebar/TabsView/TabsCommandDialog/index.tsx
  • apps/desktop/src/lib/trpc/routers/workspaces/utils/worktree.ts
  • apps/desktop/electron.vite.config.ts
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/WorkspaceFooter/components/WorkspaceFooterRight/WorkspaceFooterRight.tsx
  • apps/desktop/src/lib/trpc/routers/terminal/terminal.ts
  • apps/desktop/electron-builder.ts
  • apps/desktop/src/renderer/screens/main/components/TopBar/WorkspaceTabs/WorkspaceHoverCard/components/ChecksList/ChecksList.tsx
  • apps/desktop/src/main/index.ts
  • apps/desktop/src/lib/trpc/routers/external/index.ts
  • apps/desktop/src/lib/trpc/routers/settings/index.ts
  • apps/desktop/src/lib/trpc/routers/external/helpers.ts
  • apps/desktop/src/lib/trpc/routers/changes/branches.ts
  • apps/desktop/src/lib/trpc/routers/config/config.ts
  • apps/desktop/src/lib/trpc/routers/workspaces/workspaces.ts
  • apps/desktop/src/lib/trpc/routers/projects/projects.ts
**/{components,features}/**/[!.]*.tsx

📄 CodeRabbit inference engine (AGENTS.md)

Organize project structure with one folder per component: ComponentName/ComponentName.tsx with index.ts barrel export

Files:

  • apps/desktop/src/renderer/screens/main/components/TopBar/WorkspaceTabs/WorkspaceHoverCard/components/ChecksList/components/CheckItemRow/CheckItemRow.tsx
  • apps/desktop/src/renderer/screens/main/components/TopBar/WorkspaceTabs/WorkspaceHoverCard/components/ChecksSummary/ChecksSummary.tsx
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/Sidebar/TabsView/index.tsx
  • apps/desktop/src/renderer/components/OpenInButton/OpenInButton.tsx
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/Sidebar/TabsView/TabsCommandDialog/index.tsx
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/WorkspaceFooter/components/WorkspaceFooterRight/WorkspaceFooterRight.tsx
  • apps/desktop/src/renderer/screens/main/components/TopBar/WorkspaceTabs/WorkspaceHoverCard/components/ChecksList/ChecksList.tsx
**/{components,features}/**/*.{ts,tsx,test.ts,test.tsx,stories.tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Co-locate component dependencies (utils, hooks, constants, config, tests, stories) next to the file using them

Files:

  • apps/desktop/src/renderer/screens/main/components/TopBar/WorkspaceTabs/WorkspaceHoverCard/components/ChecksList/components/CheckItemRow/CheckItemRow.tsx
  • apps/desktop/src/renderer/screens/main/components/TopBar/WorkspaceTabs/WorkspaceHoverCard/components/ChecksSummary/ChecksSummary.tsx
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/Sidebar/TabsView/index.tsx
  • apps/desktop/src/renderer/screens/main/components/SettingsView/PresetsSettings/types.ts
  • apps/desktop/src/renderer/components/OpenInButton/OpenInButton.tsx
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/Sidebar/TabsView/TabsCommandDialog/index.tsx
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/WorkspaceFooter/components/WorkspaceFooterRight/WorkspaceFooterRight.tsx
  • apps/desktop/src/renderer/screens/main/components/TopBar/WorkspaceTabs/WorkspaceHoverCard/components/ChecksList/ChecksList.tsx
apps/desktop/src/renderer/**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Never import Node.js modules in renderer process or shared code - use only in main process (src/main/)

Files:

  • apps/desktop/src/renderer/screens/main/components/TopBar/WorkspaceTabs/WorkspaceHoverCard/components/ChecksList/components/CheckItemRow/CheckItemRow.tsx
  • apps/desktop/src/renderer/screens/main/components/TopBar/WorkspaceTabs/WorkspaceHoverCard/components/ChecksSummary/ChecksSummary.tsx
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/Sidebar/TabsView/index.tsx
  • apps/desktop/src/renderer/screens/main/components/SettingsView/PresetsSettings/types.ts
  • apps/desktop/src/renderer/components/OpenInButton/OpenInButton.tsx
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/Sidebar/TabsView/TabsCommandDialog/index.tsx
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/WorkspaceFooter/components/WorkspaceFooterRight/WorkspaceFooterRight.tsx
  • apps/desktop/src/renderer/screens/main/components/TopBar/WorkspaceTabs/WorkspaceHoverCard/components/ChecksList/ChecksList.tsx
apps/desktop/src/{main,renderer,preload}/**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Use type-safe IPC communication - define channel types in apps/desktop/src/shared/ipc-channels.ts before implementing handlers

Files:

  • apps/desktop/src/renderer/screens/main/components/TopBar/WorkspaceTabs/WorkspaceHoverCard/components/ChecksList/components/CheckItemRow/CheckItemRow.tsx
  • apps/desktop/src/renderer/screens/main/components/TopBar/WorkspaceTabs/WorkspaceHoverCard/components/ChecksSummary/ChecksSummary.tsx
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/Sidebar/TabsView/index.tsx
  • apps/desktop/src/renderer/screens/main/components/SettingsView/PresetsSettings/types.ts
  • apps/desktop/src/main/lib/local-db/index.ts
  • apps/desktop/src/main/lib/notification-sound.ts
  • apps/desktop/src/main/windows/main.ts
  • apps/desktop/src/renderer/components/OpenInButton/OpenInButton.tsx
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/Sidebar/TabsView/TabsCommandDialog/index.tsx
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/WorkspaceFooter/components/WorkspaceFooterRight/WorkspaceFooterRight.tsx
  • apps/desktop/src/renderer/screens/main/components/TopBar/WorkspaceTabs/WorkspaceHoverCard/components/ChecksList/ChecksList.tsx
  • apps/desktop/src/main/index.ts
**/*.{tsx,css}

📄 CodeRabbit inference engine (AGENTS.md)

Use React + TailwindCSS v4 + shadcn/ui for UI development

Files:

  • apps/desktop/src/renderer/screens/main/components/TopBar/WorkspaceTabs/WorkspaceHoverCard/components/ChecksList/components/CheckItemRow/CheckItemRow.tsx
  • apps/desktop/src/renderer/screens/main/components/TopBar/WorkspaceTabs/WorkspaceHoverCard/components/ChecksSummary/ChecksSummary.tsx
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/Sidebar/TabsView/index.tsx
  • apps/desktop/src/renderer/components/OpenInButton/OpenInButton.tsx
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/Sidebar/TabsView/TabsCommandDialog/index.tsx
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/WorkspaceFooter/components/WorkspaceFooterRight/WorkspaceFooterRight.tsx
  • apps/desktop/src/renderer/screens/main/components/TopBar/WorkspaceTabs/WorkspaceHoverCard/components/ChecksList/ChecksList.tsx
**/{components,features}/**/*.tsx

📄 CodeRabbit inference engine (AGENTS.md)

**/{components,features}/**/*.tsx: Nest components in parent's components/ folder if used only once, promote to highest shared parent's components/ if used 2+ times
Use one component per file - do not combine multiple components in a single file

Files:

  • apps/desktop/src/renderer/screens/main/components/TopBar/WorkspaceTabs/WorkspaceHoverCard/components/ChecksList/components/CheckItemRow/CheckItemRow.tsx
  • apps/desktop/src/renderer/screens/main/components/TopBar/WorkspaceTabs/WorkspaceHoverCard/components/ChecksSummary/ChecksSummary.tsx
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/Sidebar/TabsView/index.tsx
  • apps/desktop/src/renderer/components/OpenInButton/OpenInButton.tsx
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/Sidebar/TabsView/TabsCommandDialog/index.tsx
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/WorkspaceFooter/components/WorkspaceFooterRight/WorkspaceFooterRight.tsx
  • apps/desktop/src/renderer/screens/main/components/TopBar/WorkspaceTabs/WorkspaceHoverCard/components/ChecksList/ChecksList.tsx
apps/desktop/src/main/**/*.ts

📄 CodeRabbit inference engine (AGENTS.md)

Accept object parameters in IPC handlers - do not use positional parameters in ipcMain.handle()

Files:

  • apps/desktop/src/main/lib/local-db/index.ts
  • apps/desktop/src/main/lib/notification-sound.ts
  • apps/desktop/src/main/windows/main.ts
  • apps/desktop/src/main/index.ts
apps/desktop/src/main/index.ts

📄 CodeRabbit inference engine (AGENTS.md)

Load environment variables in src/main/index.ts and electron.vite.config.ts with override: true before other imports

Files:

  • apps/desktop/src/main/index.ts
🧠 Learnings (12)
📓 Common learnings
Learnt from: CR
Repo: superset-sh/superset PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-18T23:19:10.415Z
Learning: Applies to packages/db/src/**/*.{ts,tsx} : Store database schema in packages/db/src/ and use Drizzle ORM for schema management
Learnt from: CR
Repo: superset-sh/superset PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-18T23:19:10.415Z
Learning: Applies to apps/api/src/**/*.{ts,tsx} : Use Drizzle ORM for all database operations
Learnt from: CR
Repo: superset-sh/superset PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-18T23:19:10.415Z
Learning: Create database migrations by modifying Drizzle schema in packages/db/src/schema and running drizzle-kit generate with snake_case migration names
📚 Learning: 2025-12-18T23:19:10.415Z
Learnt from: CR
Repo: superset-sh/superset PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-18T23:19:10.415Z
Learning: Applies to packages/db/src/**/*.{ts,tsx} : Store database schema in packages/db/src/ and use Drizzle ORM for schema management

Applied to files:

  • packages/local-db/src/index.ts
  • packages/local-db/package.json
  • apps/desktop/src/main/lib/local-db/index.ts
  • apps/desktop/src/main/windows/main.ts
  • apps/admin/package.json
  • apps/desktop/src/lib/trpc/routers/workspaces/utils/worktree.ts
  • packages/local-db/src/schema/relations.ts
  • packages/local-db/tsconfig.json
  • apps/desktop/src/lib/trpc/routers/terminal/terminal.ts
  • packages/local-db/src/schema/index.ts
  • packages/local-db/drizzle/0000_initial_schema.sql
  • packages/local-db/drizzle.config.ts
  • apps/desktop/src/main/index.ts
  • packages/local-db/src/schema/schema.ts
  • apps/desktop/src/lib/trpc/routers/external/helpers.ts
  • apps/admin/src/proxy.ts
  • apps/desktop/src/lib/trpc/routers/changes/branches.ts
  • packages/local-db/drizzle/meta/0000_snapshot.json
  • apps/desktop/src/lib/trpc/routers/config/config.ts
  • apps/desktop/src/lib/trpc/routers/workspaces/workspaces.ts
  • apps/desktop/src/lib/trpc/routers/projects/projects.ts
📚 Learning: 2025-12-18T23:19:10.415Z
Learnt from: CR
Repo: superset-sh/superset PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-18T23:19:10.415Z
Learning: Applies to src/components/{ui,ai-elements,react-flow}/**/*.tsx : Use kebab-case single files for shadcn/ui components in src/components/ui/, src/components/ai-elements, and src/components/react-flow/ directories

Applied to files:

  • apps/desktop/src/renderer/screens/main/components/TopBar/WorkspaceTabs/WorkspaceHoverCard/components/ChecksList/components/CheckItemRow/CheckItemRow.tsx
📚 Learning: 2025-12-18T23:19:10.415Z
Learnt from: CR
Repo: superset-sh/superset PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-18T23:19:10.415Z
Learning: Applies to apps/api/src/**/*.{ts,tsx} : Use Drizzle ORM for all database operations

Applied to files:

  • packages/local-db/package.json
  • apps/desktop/src/main/lib/local-db/index.ts
  • apps/desktop/src/main/windows/main.ts
  • apps/admin/package.json
  • packages/local-db/src/schema/relations.ts
  • packages/local-db/tsconfig.json
  • apps/desktop/src/lib/trpc/routers/terminal/terminal.ts
  • packages/local-db/drizzle.config.ts
  • apps/desktop/src/main/index.ts
  • packages/local-db/src/schema/schema.ts
  • apps/admin/src/proxy.ts
  • apps/desktop/src/lib/trpc/routers/changes/branches.ts
  • apps/desktop/src/lib/trpc/routers/config/config.ts
  • apps/desktop/src/lib/trpc/routers/workspaces/workspaces.ts
  • apps/desktop/src/lib/trpc/routers/projects/projects.ts
📚 Learning: 2025-12-18T23:19:10.415Z
Learnt from: CR
Repo: superset-sh/superset PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-18T23:19:10.415Z
Learning: Applies to **/*.{tsx,css} : Use React + TailwindCSS v4 + shadcn/ui for UI development

Applied to files:

  • apps/desktop/package.json
📚 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} : Use alias as defined in `tsconfig.json` when possible

Applied to files:

  • apps/desktop/src/renderer/screens/main/components/SettingsView/PresetsSettings/types.ts
  • apps/desktop/src/renderer/components/OpenInButton/OpenInButton.tsx
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/Sidebar/TabsView/TabsCommandDialog/index.tsx
  • packages/local-db/tsconfig.json
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/WorkspaceFooter/components/WorkspaceFooterRight/WorkspaceFooterRight.tsx
📚 Learning: 2025-12-18T23:19:10.415Z
Learnt from: CR
Repo: superset-sh/superset PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-18T23:19:10.415Z
Learning: Applies to apps/desktop/src/renderer/**/*.{ts,tsx} : Never import Node.js modules in renderer process or shared code - use only in main process (src/main/)

Applied to files:

  • apps/desktop/scripts/copy-native-modules.ts
  • apps/desktop/src/renderer/components/OpenInButton/OpenInButton.tsx
  • apps/desktop/electron.vite.config.ts
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/WorkspaceFooter/components/WorkspaceFooterRight/WorkspaceFooterRight.tsx
  • apps/desktop/src/lib/trpc/routers/external/helpers.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/main/windows/main.ts
  • apps/desktop/src/lib/trpc/routers/terminal/terminal.ts
  • apps/desktop/src/lib/trpc/routers/external/index.ts
  • apps/desktop/src/lib/trpc/routers/external/helpers.ts
📚 Learning: 2025-12-18T23:19:10.415Z
Learnt from: CR
Repo: superset-sh/superset PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-18T23:19:10.415Z
Learning: Applies to apps/desktop/src/{main,renderer,preload}/**/*.{ts,tsx} : Use type-safe IPC communication - define channel types in apps/desktop/src/shared/ipc-channels.ts before implementing handlers

Applied to files:

  • apps/desktop/src/renderer/components/OpenInButton/OpenInButton.tsx
📚 Learning: 2025-12-18T23:19:10.415Z
Learnt from: CR
Repo: superset-sh/superset PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-18T23:19:10.415Z
Learning: Applies to apps/desktop/src/main/index.ts : Load environment variables in src/main/index.ts and electron.vite.config.ts with override: true before other imports

Applied to files:

  • apps/desktop/electron.vite.config.ts
📚 Learning: 2025-12-18T23:19:10.415Z
Learnt from: CR
Repo: superset-sh/superset PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-18T23:19:10.415Z
Learning: Applies to **/{components,features}/**/[!.]*.tsx : Organize project structure with one folder per component: ComponentName/ComponentName.tsx with index.ts barrel export

Applied to files:

  • packages/local-db/src/schema/index.ts
📚 Learning: 2025-12-18T23:19:10.415Z
Learnt from: CR
Repo: superset-sh/superset PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-18T23:19:10.415Z
Learning: Create database migrations by modifying Drizzle schema in packages/db/src/schema and running drizzle-kit generate with snake_case migration names

Applied to files:

  • packages/local-db/drizzle/0000_initial_schema.sql
  • packages/local-db/drizzle.config.ts
  • packages/local-db/drizzle/meta/0000_snapshot.json
🧬 Code graph analysis (10)
apps/desktop/src/main/lib/local-db/index.ts (2)
apps/desktop/src/main/lib/app-environment.ts (2)
  • DB_PATH (8-8)
  • SUPERSET_HOME_DIR (5-5)
packages/local-db/src/schema/schema.ts (4)
  • projects (14-31)
  • worktrees (39-57)
  • workspaces (65-88)
  • settings (96-103)
apps/desktop/src/lib/trpc/routers/workspaces/utils/worktree.ts (2)
apps/desktop/src/main/lib/local-db/index.ts (1)
  • localDb (60-60)
packages/local-db/src/schema/schema.ts (3)
  • worktrees (39-57)
  • SelectWorkspace (91-91)
  • projects (14-31)
apps/desktop/src/main/index.ts (1)
apps/desktop/src/main/lib/local-db/index.ts (1)
  • localDb (60-60)
packages/local-db/src/schema/schema.ts (3)
packages/local-db/src/schema/zod.ts (5)
  • GitStatus (12-12)
  • GitHubStatus (48-48)
  • WorkspaceType (68-68)
  • ExternalApp (97-97)
  • TerminalPreset (61-61)
apps/desktop/src/lib/trpc/routers/external/helpers.ts (1)
  • ExternalApp (97-97)
apps/desktop/src/renderer/screens/main/components/SettingsView/PresetsSettings/types.ts (1)
  • TerminalPreset (3-3)
apps/desktop/src/lib/trpc/routers/external/index.ts (2)
apps/desktop/src/main/lib/local-db/index.ts (1)
  • localDb (60-60)
packages/local-db/src/schema/schema.ts (1)
  • settings (96-103)
apps/desktop/src/lib/trpc/routers/changes/branches.ts (2)
apps/desktop/src/main/lib/local-db/index.ts (1)
  • localDb (60-60)
packages/local-db/src/schema/schema.ts (1)
  • worktrees (39-57)
packages/local-db/src/schema/zod.ts (2)
apps/desktop/src/renderer/screens/main/components/SettingsView/PresetsSettings/types.ts (1)
  • TerminalPreset (3-3)
apps/desktop/src/lib/trpc/routers/external/helpers.ts (2)
  • EXTERNAL_APPS (97-97)
  • ExternalApp (97-97)
apps/desktop/src/lib/trpc/routers/config/config.ts (2)
apps/desktop/src/main/lib/local-db/index.ts (1)
  • localDb (60-60)
packages/local-db/src/schema/schema.ts (1)
  • projects (14-31)
apps/desktop/src/lib/trpc/routers/workspaces/workspaces.ts (2)
apps/desktop/src/main/lib/local-db/index.ts (1)
  • localDb (60-60)
packages/local-db/src/schema/schema.ts (5)
  • projects (14-31)
  • worktrees (39-57)
  • workspaces (65-88)
  • settings (96-103)
  • SelectWorktree (60-60)
apps/desktop/src/lib/trpc/routers/projects/projects.ts (3)
packages/local-db/src/schema/schema.ts (2)
  • SelectProject (34-34)
  • projects (14-31)
apps/desktop/src/main/lib/local-db/index.ts (1)
  • localDb (60-60)
apps/desktop/src/lib/trpc/routers/projects/utils/colors/colors.ts (1)
  • assignRandomColor (3-7)
🪛 GitHub Actions: CI
apps/desktop/src/lib/trpc/routers/workspaces/utils/github/github.ts

[error] 1-1: Command failed: bun run build exited with exit code 1.

apps/desktop/src/renderer/screens/main/components/TopBar/WorkspaceTabs/WorkspaceHoverCard/components/ChecksList/components/CheckItemRow/CheckItemRow.tsx

[error] 1-1: Command failed: bun run build exited with exit code 1.

apps/desktop/src/renderer/screens/main/components/TopBar/WorkspaceTabs/WorkspaceHoverCard/components/ChecksSummary/ChecksSummary.tsx

[error] 1-1: Command failed: bun run build exited with exit code 1.

apps/desktop/src/renderer/screens/main/components/WorkspaceView/Sidebar/TabsView/index.tsx

[error] 1-1: Command failed: bun run build exited with exit code 1.

apps/desktop/package.json

[error] 1-1: Command failed: bun run build exited with exit code 1.

apps/desktop/src/renderer/screens/main/components/SettingsView/PresetsSettings/types.ts

[error] 1-1: Command failed: bun run build exited with exit code 1.

apps/desktop/src/main/lib/local-db/index.ts

[warning] 1-1: assist/source/organizeImports: The imports and exports are not sorted. Safe fix suggested: Organize Imports (Biome).

apps/desktop/src/main/lib/notification-sound.ts

[error] 1-1: Command failed: bun run build exited with exit code 1.

apps/desktop/scripts/copy-native-modules.ts

[error] 1-1: Command failed: bun run build exited with exit code 1.

apps/desktop/src/main/windows/main.ts

[error] 1-1: Command failed: bun run build exited with exit code 1.

apps/desktop/src/renderer/components/OpenInButton/OpenInButton.tsx

[error] 1-1: Command failed: bun run build exited with exit code 1.

apps/desktop/src/renderer/screens/main/components/WorkspaceView/Sidebar/TabsView/TabsCommandDialog/index.tsx

[error] 1-1: Command failed: bun run build exited with exit code 1.

apps/desktop/src/lib/trpc/routers/workspaces/utils/worktree.ts

[warning] 1-1: assist/source/organizeImports: The imports and exports are not sorted. Safe fix suggested: Organize Imports (Biome).

apps/desktop/electron.vite.config.ts

[warning] 49-51: Formatter would have printed: reorganize migrationsSrc path. Fix formatting with the formatter.

apps/desktop/src/renderer/screens/main/components/WorkspaceView/WorkspaceFooter/components/WorkspaceFooterRight/WorkspaceFooterRight.tsx

[error] 1-1: Command failed: bun run build exited with exit code 1.

apps/desktop/src/lib/trpc/routers/terminal/terminal.ts

[error] 1-1: Command failed: bun run build exited with exit code 1.

apps/desktop/electron-builder.ts

[error] 1-1: Command failed: bun run build exited with exit code 1.

packages/local-db/src/schema/index.ts

[warning] 1-1: assist/source/organizeImports: The imports and exports are not sorted. Safe fix suggested: Organize Imports (Biome).

apps/desktop/src/renderer/screens/main/components/TopBar/WorkspaceTabs/WorkspaceHoverCard/components/ChecksList/ChecksList.tsx

[error] 1-1: Command failed: bun run build exited with exit code 1.

apps/desktop/src/main/index.ts

[error] 1-1: Command failed: bun run build exited with exit code 1.

packages/local-db/src/schema/schema.ts

[warning] 17-32: Formatter would rearrange field definitions for IDs and defaults. Apply formatting fixes.

apps/desktop/src/lib/trpc/routers/external/index.ts

[error] 1-1: Command failed: bun run build exited with exit code 1.

apps/desktop/src/lib/trpc/routers/settings/index.ts

[error] 1-1: Command failed: bun run build exited with exit code 1.

apps/desktop/src/lib/trpc/routers/external/helpers.ts

[error] 1-1: Command failed: bun run build exited with exit code 1.

apps/admin/src/proxy.ts

[warning] 1-1: assist/source/organizeImports: Imports are not sorted. Safe fix suggested: Organize Imports (Biome).

apps/desktop/src/lib/trpc/routers/changes/branches.ts

[error] 1-1: Command failed: bun run build exited with exit code 1.

apps/desktop/src/lib/trpc/routers/config/config.ts

[error] 1-1: Command failed: bun run build exited with exit code 1.

apps/desktop/src/lib/trpc/routers/workspaces/workspaces.ts

[warning] 8-8: lint/correctness/noUnusedImports: Unused import 'SelectWorkspace' detected. This import is fixable.


[warning] 3-11: Formatter would have rearranged imports for proper ordering. Apply Biome organize-imports fixes.

apps/desktop/src/lib/trpc/routers/projects/projects.ts

[warning] 1-1: assist/source/organizeImports: The imports and exports are not sorted. Safe fix suggested: Organize Imports (Biome).

🪛 GitHub Check: Test
apps/desktop/src/main/lib/local-db/index.ts

[failure] 43-43: TypeError: app.getAppPath is not a function. (In 'app.getAppPath()'

  at getMigrationsDirectory (/home/runner/work/superset/superset/apps/desktop/src/main/lib/local-db/index.ts:43:27)
  at /home/runner/work/superset/superset/apps/desktop/src/main/lib/local-db/index.ts:52:26
  at loadAndEvaluateModule (2:1)
⏰ 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). (5)
  • GitHub Check: Deploy Marketing
  • GitHub Check: Deploy Web
  • GitHub Check: Deploy API
  • GitHub Check: Deploy Docs
  • GitHub Check: Deploy Admin

Comment thread apps/desktop/electron.vite.config.ts Outdated
Comment thread apps/desktop/src/lib/trpc/routers/workspaces/utils/worktree.ts Outdated
Comment thread apps/desktop/src/lib/trpc/routers/workspaces/workspaces.ts
Comment thread apps/desktop/src/lib/trpc/routers/workspaces/workspaces.ts Outdated
Comment on lines +24 to +62
function getMigrationsDirectory(): string {
if (app.isPackaged) {
return join(process.resourcesPath, "resources/migrations");
}

const isDev = env.NODE_ENV === "development";

if (isDev) {
// Development: source files in monorepo
return join(app.getAppPath(), "../../packages/local-db/drizzle");
}

// Preview mode: __dirname is dist/main, so go up one level to dist/resources/migrations
const previewPath = join(__dirname, "../resources/migrations");
if (existsSync(previewPath)) {
return previewPath;
}

// Fallback: try monorepo path
const srcPath = join(app.getAppPath(), "../../packages/local-db/drizzle");
if (existsSync(srcPath)) {
return srcPath;
}

console.warn(`[local-db] Migrations directory not found at: ${previewPath}`);
return previewPath;
}

const migrationsFolder = getMigrationsDirectory();

const sqlite = new Database(DB_PATH);
sqlite.pragma("journal_mode = WAL");

console.log(`[local-db] Database initialized at: ${DB_PATH}`);
console.log(`[local-db] Running migrations from: ${migrationsFolder}`);

export const localDb = drizzle(sqlite, { schema });

migrate(localDb, { migrationsFolder });
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

Test failure: app.getAppPath is not a function in test environment.

The module executes getMigrationsDirectory() and database initialization at import time (lines 52-64). When tests import modules that transitively import this file, Electron's app module isn't available, causing the test failure.

Consider either:

  1. Lazy initialization: Export an initLocalDb() function called explicitly during app startup instead of at module load
  2. Guard for test environment: Check if running in a test context before calling Electron APIs
🔎 Option 1: Lazy initialization pattern
-const migrationsFolder = getMigrationsDirectory();
-
-const sqlite = new Database(DB_PATH);
-sqlite.pragma("journal_mode = WAL");
-
-console.log(`[local-db] Database initialized at: ${DB_PATH}`);
-console.log(`[local-db] Running migrations from: ${migrationsFolder}`);
-
-export const localDb = drizzle(sqlite, { schema });
-
-migrate(localDb, { migrationsFolder });
-
-console.log("[local-db] Migrations complete");
+let _localDb: ReturnType<typeof drizzle<typeof schema>> | null = null;
+
+export function initLocalDb() {
+	if (_localDb) return _localDb;
+	
+	const migrationsFolder = getMigrationsDirectory();
+	const sqlite = new Database(DB_PATH);
+	sqlite.pragma("journal_mode = WAL");
+	
+	console.log(`[local-db] Database initialized at: ${DB_PATH}`);
+	console.log(`[local-db] Running migrations from: ${migrationsFolder}`);
+	
+	_localDb = drizzle(sqlite, { schema });
+	migrate(_localDb, { migrationsFolder });
+	
+	console.log("[local-db] Migrations complete");
+	migrateFromLegacyDb();
+	
+	return _localDb;
+}
+
+export const localDb = new Proxy({} as ReturnType<typeof drizzle<typeof schema>>, {
+	get(_, prop) {
+		if (!_localDb) {
+			throw new Error("localDb not initialized. Call initLocalDb() first.");
+		}
+		return Reflect.get(_localDb, prop);
+	},
+});

Committable suggestion skipped: line range outside the PR's diff.

🧰 Tools
🪛 GitHub Check: Test

[failure] 43-43: TypeError: app.getAppPath is not a function. (In 'app.getAppPath()'

  at getMigrationsDirectory (/home/runner/work/superset/superset/apps/desktop/src/main/lib/local-db/index.ts:43:27)
  at /home/runner/work/superset/superset/apps/desktop/src/main/lib/local-db/index.ts:52:26
  at loadAndEvaluateModule (2:1)
🤖 Prompt for AI Agents
In apps/desktop/src/main/lib/local-db/index.ts around lines 24-62, the file runs
getMigrationsDirectory() and initializes the Database at module import time
causing test failures because app.getAppPath/app isn't available in test env;
refactor to lazy initialization: remove top-level creation of
sqlite/drizzle/migrate and migrationsFolder, export an async
initLocalDb(options?) function that calls getMigrationsDirectory(), constructs
the Database, sets PRAGMA, logs, initializes drizzle and runs migrate, and
returns (or sets) the localDb instance; ensure getMigrationsDirectory() is safe
in tests by only calling it inside initLocalDb and add a simple guard (if
(process.env.NODE_ENV === 'test') return a noop or throw a clear error) if you
need explicit behavior in tests; update callers to invoke await initLocalDb()
during app startup.

Comment on lines +86 to +176
try {
const legacyData = JSON.parse(readFileSync(LEGACY_DB_PATH, "utf-8"));

// Migrate projects
if (legacyData.projects?.length > 0) {
for (const p of legacyData.projects) {
localDb.insert(projects).values({
id: p.id,
mainRepoPath: p.mainRepoPath,
name: p.name,
color: p.color,
tabOrder: p.tabOrder,
lastOpenedAt: p.lastOpenedAt,
createdAt: p.createdAt,
configToastDismissed: p.configToastDismissed,
defaultBranch: p.defaultBranch,
}).run();
}
console.log(`[local-db] Migrated ${legacyData.projects.length} projects`);
}

// Migrate worktrees
if (legacyData.worktrees?.length > 0) {
for (const w of legacyData.worktrees) {
localDb.insert(worktrees).values({
id: w.id,
projectId: w.projectId,
path: w.path,
branch: w.branch,
baseBranch: w.baseBranch,
createdAt: w.createdAt,
gitStatus: w.gitStatus,
githubStatus: w.githubStatus,
}).run();
}
console.log(`[local-db] Migrated ${legacyData.worktrees.length} worktrees`);
}

// Migrate workspaces
if (legacyData.workspaces?.length > 0) {
for (const ws of legacyData.workspaces) {
// Get branch from worktree if not set on workspace
let branch = ws.branch;
if (!branch && ws.worktreeId) {
const worktree = legacyData.worktrees?.find(
(wt: { id: string }) => wt.id === ws.worktreeId
);
branch = worktree?.branch ?? "unknown";
}

localDb.insert(workspaces).values({
id: ws.id,
projectId: ws.projectId,
worktreeId: ws.worktreeId,
type: ws.type ?? "worktree", // Default to worktree for legacy data
branch: branch ?? "unknown",
name: ws.name,
tabOrder: ws.tabOrder,
createdAt: ws.createdAt,
updatedAt: ws.updatedAt,
lastOpenedAt: ws.lastOpenedAt,
}).run();
}
console.log(`[local-db] Migrated ${legacyData.workspaces.length} workspaces`);
}

// Migrate settings
if (legacyData.settings) {
const s = legacyData.settings;
localDb.insert(settings).values({
id: 1,
lastActiveWorkspaceId: s.lastActiveWorkspaceId,
lastUsedApp: s.lastUsedApp,
terminalPresets: s.terminalPresets,
terminalPresetsInitialized: s.terminalPresetsInitialized,
selectedRingtoneId: s.selectedRingtoneId,
}).run();
console.log("[local-db] Migrated settings");
}

// Backup the legacy db.json
const backupPath = `${LEGACY_DB_PATH}.backup`;
renameSync(LEGACY_DB_PATH, backupPath);
console.log(`[local-db] Legacy db.json backed up to ${backupPath}`);

console.log("[local-db] Legacy migration complete!");
} catch (error) {
console.error("[local-db] Failed to migrate legacy data:", error);
// Don't throw - app can continue with empty db
}
}
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.

🛠️ Refactor suggestion | 🟠 Major

Wrap legacy migration in a transaction for atomicity.

If the migration fails partway through (e.g., after inserting some projects but before worktrees), the database will be left in an inconsistent state. The backup won't be created, but the partial data will remain, and subsequent runs will skip migration because projectCount > 0.

🔎 Wrap in transaction
 	try {
 		const legacyData = JSON.parse(readFileSync(LEGACY_DB_PATH, "utf-8"));
 
+		// Use a transaction for atomic migration
+		localDb.transaction((tx) => {
 		// Migrate projects
 		if (legacyData.projects?.length > 0) {
 			for (const p of legacyData.projects) {
-				localDb.insert(projects).values({
+				tx.insert(projects).values({
 					// ... values
 				}).run();
 			}
 			// ... rest of migration using tx instead of localDb
 		}
+		});
 
 		// Backup the legacy db.json
 		const backupPath = `${LEGACY_DB_PATH}.backup`;

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In apps/desktop/src/main/lib/local-db/index.ts around lines 86-176, the legacy
migration is performed without a DB transaction so partial inserts can leave the
DB inconsistent if an error occurs; wrap the entire migration (all inserts for
projects, worktrees, workspaces, settings and the backup/rename) inside a single
database transaction so it either fully commits or rolls back on error, and move
the rename/backup to occur only after a successful commit; ensure errors inside
the transaction cause a rollback and preserve the original legacy file so
subsequent runs can retry the migration.

Comment thread packages/local-db/src/schema/index.ts Outdated
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Dec 23, 2025

🧹 Preview Cleanup Complete

The following preview resources have been cleaned up:

Service Status
Neon Database (Neon)

Thank you for your contribution! 🎉


Preview resources have been processed for cleanup

Copy link
Copy Markdown
Contributor

@charliecreates charliecreates Bot left a comment

Choose a reason for hiding this comment

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

The biggest risks are runtime correctness in production and data integrity: getMigrationsDirectory() likely builds an incorrect packaged path (process.resourcesPath + "resources/..."), and DB migrations/legacy migration run at import-time without lifecycle control. Several routers introduce non-atomic multi-step updates (reorder loops, JSON blob settings updates) that can corrupt ordering/state on crashes or concurrent calls. Finally, there’s a clear bug in projects.reorder using eq(projects.tabOrder, projects.tabOrder) as a pseudo-filter.

Additional notes (1)
  • Readability | apps/desktop/src/lib/trpc/routers/projects/projects.ts:38-57
    In upsertProject, Date.now() is computed multiple times. That can lead to returning a lastOpenedAt that doesn’t match what’s persisted (tiny difference, but avoidable) and makes testing harder.

This also affects other mutations where timestamps are used in multiple operations.

Summary of changes

What changed

Local DB migration (lowdb ➜ SQLite)

  • Introduced a new Electron-local SQLite database layer using better-sqlite3 + Drizzle (apps/desktop/src/main/lib/local-db/index.ts).
  • Updated multiple desktop tRPC routers (projects/workspaces/settings/terminal/external/branches/config, etc.) to query/update via drizzle-orm instead of lowdb’s db.data.
  • Removed the legacy lowdb implementation (apps/desktop/src/main/lib/db/*) and dependency (lowdb removed from apps/desktop/package.json).

New package: @superset/local-db

  • Added packages/local-db/ containing Drizzle SQLite schema, relations, and Zod types/validators.
  • Added initial SQL migration drizzle/0000_initial_schema.sql + Drizzle meta journal/snapshot.

Build & packaging updates

  • Added better-sqlite3 native module handling:
    • Added to asarUnpack and files in apps/desktop/electron-builder.ts.
    • Marked as Rollup external in apps/desktop/electron.vite.config.ts.
    • Added to copy-native-modules.ts.
  • Added copying of DB migrations into dist/resources/migrations for preview/packaging.

Admin app change

  • Updated admin proxy to import db from @superset/db/client and import eq from drizzle-orm.
  • Added drizzle-orm dependency to apps/admin/package.json.

Misc

  • packages/db/src/index.ts no longer re-exports Drizzle operators (e.g. eq, and).

Comment on lines +52 to +179
const migrationsFolder = getMigrationsDirectory();

const sqlite = new Database(DB_PATH);
sqlite.pragma("journal_mode = WAL");

console.log(`[local-db] Database initialized at: ${DB_PATH}`);
console.log(`[local-db] Running migrations from: ${migrationsFolder}`);

export const localDb = drizzle(sqlite, { schema });

migrate(localDb, { migrationsFolder });

console.log("[local-db] Migrations complete");

/**
* Migrate data from legacy db.json (lowdb) to SQLite.
* Only runs if:
* 1. db.json exists
* 2. SQLite projects table is empty (first run after migration)
*/
function migrateFromLegacyDb(): void {
if (!existsSync(LEGACY_DB_PATH)) {
return;
}

// Check if SQLite is empty
const projectCount = localDb.select({ count: count() }).from(projects).get();
if (projectCount && projectCount.count > 0) {
console.log("[local-db] SQLite already has data, skipping legacy migration");
return;
}

console.log("[local-db] Migrating data from legacy db.json...");

try {
const legacyData = JSON.parse(readFileSync(LEGACY_DB_PATH, "utf-8"));

// Migrate projects
if (legacyData.projects?.length > 0) {
for (const p of legacyData.projects) {
localDb.insert(projects).values({
id: p.id,
mainRepoPath: p.mainRepoPath,
name: p.name,
color: p.color,
tabOrder: p.tabOrder,
lastOpenedAt: p.lastOpenedAt,
createdAt: p.createdAt,
configToastDismissed: p.configToastDismissed,
defaultBranch: p.defaultBranch,
}).run();
}
console.log(`[local-db] Migrated ${legacyData.projects.length} projects`);
}

// Migrate worktrees
if (legacyData.worktrees?.length > 0) {
for (const w of legacyData.worktrees) {
localDb.insert(worktrees).values({
id: w.id,
projectId: w.projectId,
path: w.path,
branch: w.branch,
baseBranch: w.baseBranch,
createdAt: w.createdAt,
gitStatus: w.gitStatus,
githubStatus: w.githubStatus,
}).run();
}
console.log(`[local-db] Migrated ${legacyData.worktrees.length} worktrees`);
}

// Migrate workspaces
if (legacyData.workspaces?.length > 0) {
for (const ws of legacyData.workspaces) {
// Get branch from worktree if not set on workspace
let branch = ws.branch;
if (!branch && ws.worktreeId) {
const worktree = legacyData.worktrees?.find(
(wt: { id: string }) => wt.id === ws.worktreeId
);
branch = worktree?.branch ?? "unknown";
}

localDb.insert(workspaces).values({
id: ws.id,
projectId: ws.projectId,
worktreeId: ws.worktreeId,
type: ws.type ?? "worktree", // Default to worktree for legacy data
branch: branch ?? "unknown",
name: ws.name,
tabOrder: ws.tabOrder,
createdAt: ws.createdAt,
updatedAt: ws.updatedAt,
lastOpenedAt: ws.lastOpenedAt,
}).run();
}
console.log(`[local-db] Migrated ${legacyData.workspaces.length} workspaces`);
}

// Migrate settings
if (legacyData.settings) {
const s = legacyData.settings;
localDb.insert(settings).values({
id: 1,
lastActiveWorkspaceId: s.lastActiveWorkspaceId,
lastUsedApp: s.lastUsedApp,
terminalPresets: s.terminalPresets,
terminalPresetsInitialized: s.terminalPresetsInitialized,
selectedRingtoneId: s.selectedRingtoneId,
}).run();
console.log("[local-db] Migrated settings");
}

// Backup the legacy db.json
const backupPath = `${LEGACY_DB_PATH}.backup`;
renameSync(LEGACY_DB_PATH, backupPath);
console.log(`[local-db] Legacy db.json backed up to ${backupPath}`);

console.log("[local-db] Legacy migration complete!");
} catch (error) {
console.error("[local-db] Failed to migrate legacy data:", error);
// Don't throw - app can continue with empty db
}
}

migrateFromLegacyDb();

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.

migrate(localDb, { migrationsFolder }) and migrateFromLegacyDb() run at import time. That makes module evaluation have side effects (disk IO + schema changes) and can happen earlier than Electron’s app.whenReady() lifecycle. It also makes tests / secondary processes (e.g. preload) unexpectedly touch the DB.

This should be an explicit initialization step (e.g. initLocalDb()), ideally called once from main after app.whenReady() or after SUPERSET_HOME_DIR is guaranteed to exist. If you want to preserve “import triggers init”, at least guard it so it only runs in the main process and only once.

Suggestion

Consider refactoring to an explicit initializer and making the exported handle lazily created.

Example direction:

let _localDb: ReturnType<typeof drizzle> | null = null;

export function initLocalDb(): LocalDb {
  if (_localDb) return _localDb;
  const migrationsFolder = getMigrationsDirectory();
  const sqlite = new Database(DB_PATH);
  sqlite.pragma("journal_mode = WAL");
  const db = drizzle(sqlite, { schema });
  migrate(db, { migrationsFolder });
  migrateFromLegacyDb(db);
  _localDb = db;
  return db;
}

export function getLocalDb(): LocalDb {
  if (!_localDb) throw new Error("Local DB not initialized");
  return _localDb;
}

Then in main/index.ts call initLocalDb() after await app.whenReady().

Reply with "@CharlieHelps yes please" if you'd like me to add a commit with this suggestion.

Comment on lines +24 to +27
function getMigrationsDirectory(): string {
if (app.isPackaged) {
return join(process.resourcesPath, "resources/migrations");
}
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.

The migration folder path resolution is inconsistent between build-time copy and runtime expectations:

  • electron.vite.config.ts copies from ../../packages/local-db/drizzle to dist/resources/migrations.
  • electron-builder.ts copies from dist/resources/migrations to resources/migrations.
  • But getMigrationsDirectory() (packaged) returns process.resourcesPath + "resources/migrations".

In packaged Electron apps, process.resourcesPath usually already points at .../Contents/Resources (macOS) / resources (Windows/Linux). Appending an extra resources/ often produces .../Resources/resources/migrations (double resources), which will break migrations in production.

This is a correctness issue: prod users may not get migrations, and the DB schema may never be created.

Suggestion

Fix packaged path to match where electron-builder places the folder.

If you copy to the app’s resources/migrations directory, the runtime path should typically be:

if (app.isPackaged) {
  return join(process.resourcesPath, "migrations");
}

Or, if you intentionally want resources/migrations under a nested directory, make the electron-builder copy match it and document it.

Reply with "@CharlieHelps yes please" if you'd like me to add a commit with this suggestion.

Comment on lines +66 to +175
/**
* Migrate data from legacy db.json (lowdb) to SQLite.
* Only runs if:
* 1. db.json exists
* 2. SQLite projects table is empty (first run after migration)
*/
function migrateFromLegacyDb(): void {
if (!existsSync(LEGACY_DB_PATH)) {
return;
}

// Check if SQLite is empty
const projectCount = localDb.select({ count: count() }).from(projects).get();
if (projectCount && projectCount.count > 0) {
console.log("[local-db] SQLite already has data, skipping legacy migration");
return;
}

console.log("[local-db] Migrating data from legacy db.json...");

try {
const legacyData = JSON.parse(readFileSync(LEGACY_DB_PATH, "utf-8"));

// Migrate projects
if (legacyData.projects?.length > 0) {
for (const p of legacyData.projects) {
localDb.insert(projects).values({
id: p.id,
mainRepoPath: p.mainRepoPath,
name: p.name,
color: p.color,
tabOrder: p.tabOrder,
lastOpenedAt: p.lastOpenedAt,
createdAt: p.createdAt,
configToastDismissed: p.configToastDismissed,
defaultBranch: p.defaultBranch,
}).run();
}
console.log(`[local-db] Migrated ${legacyData.projects.length} projects`);
}

// Migrate worktrees
if (legacyData.worktrees?.length > 0) {
for (const w of legacyData.worktrees) {
localDb.insert(worktrees).values({
id: w.id,
projectId: w.projectId,
path: w.path,
branch: w.branch,
baseBranch: w.baseBranch,
createdAt: w.createdAt,
gitStatus: w.gitStatus,
githubStatus: w.githubStatus,
}).run();
}
console.log(`[local-db] Migrated ${legacyData.worktrees.length} worktrees`);
}

// Migrate workspaces
if (legacyData.workspaces?.length > 0) {
for (const ws of legacyData.workspaces) {
// Get branch from worktree if not set on workspace
let branch = ws.branch;
if (!branch && ws.worktreeId) {
const worktree = legacyData.worktrees?.find(
(wt: { id: string }) => wt.id === ws.worktreeId
);
branch = worktree?.branch ?? "unknown";
}

localDb.insert(workspaces).values({
id: ws.id,
projectId: ws.projectId,
worktreeId: ws.worktreeId,
type: ws.type ?? "worktree", // Default to worktree for legacy data
branch: branch ?? "unknown",
name: ws.name,
tabOrder: ws.tabOrder,
createdAt: ws.createdAt,
updatedAt: ws.updatedAt,
lastOpenedAt: ws.lastOpenedAt,
}).run();
}
console.log(`[local-db] Migrated ${legacyData.workspaces.length} workspaces`);
}

// Migrate settings
if (legacyData.settings) {
const s = legacyData.settings;
localDb.insert(settings).values({
id: 1,
lastActiveWorkspaceId: s.lastActiveWorkspaceId,
lastUsedApp: s.lastUsedApp,
terminalPresets: s.terminalPresets,
terminalPresetsInitialized: s.terminalPresetsInitialized,
selectedRingtoneId: s.selectedRingtoneId,
}).run();
console.log("[local-db] Migrated settings");
}

// Backup the legacy db.json
const backupPath = `${LEGACY_DB_PATH}.backup`;
renameSync(LEGACY_DB_PATH, backupPath);
console.log(`[local-db] Legacy db.json backed up to ${backupPath}`);

console.log("[local-db] Legacy migration complete!");
} catch (error) {
console.error("[local-db] Failed to migrate legacy data:", error);
// Don't throw - app can continue with empty db
}
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.

migrateFromLegacyDb() parses unvalidated JSON (JSON.parse(readFileSync(...))) and then writes many rows. Two issues:

  1. There’s no transaction. If the app crashes mid-migration, you can end up with a partially migrated DB and the original db.json may still get renamed (or not), leading to hard-to-recover states.
  2. The “empty check” only looks at the projects table count. If a previous attempt partially inserted worktrees/workspaces/settings but not projects (or vice versa), you can skip/redo incorrectly.

This is a data integrity risk.

Suggestion

Wrap the migration in a SQLite transaction and use a more robust “already migrated” marker.

Minimal improvement:

sqlite.transaction(() => {
  // insert projects/worktrees/workspaces/settings
  // only rename legacy db.json *after* transaction succeeds
})();

Better: create a meta table or a settings.migratedFromLowdbAt column and set it on success.

Reply with "@CharlieHelps yes please" if you'd like me to add a commit with this suggestion.

Comment on lines +86 to +164
try {
const legacyData = JSON.parse(readFileSync(LEGACY_DB_PATH, "utf-8"));

// Migrate projects
if (legacyData.projects?.length > 0) {
for (const p of legacyData.projects) {
localDb.insert(projects).values({
id: p.id,
mainRepoPath: p.mainRepoPath,
name: p.name,
color: p.color,
tabOrder: p.tabOrder,
lastOpenedAt: p.lastOpenedAt,
createdAt: p.createdAt,
configToastDismissed: p.configToastDismissed,
defaultBranch: p.defaultBranch,
}).run();
}
console.log(`[local-db] Migrated ${legacyData.projects.length} projects`);
}

// Migrate worktrees
if (legacyData.worktrees?.length > 0) {
for (const w of legacyData.worktrees) {
localDb.insert(worktrees).values({
id: w.id,
projectId: w.projectId,
path: w.path,
branch: w.branch,
baseBranch: w.baseBranch,
createdAt: w.createdAt,
gitStatus: w.gitStatus,
githubStatus: w.githubStatus,
}).run();
}
console.log(`[local-db] Migrated ${legacyData.worktrees.length} worktrees`);
}

// Migrate workspaces
if (legacyData.workspaces?.length > 0) {
for (const ws of legacyData.workspaces) {
// Get branch from worktree if not set on workspace
let branch = ws.branch;
if (!branch && ws.worktreeId) {
const worktree = legacyData.worktrees?.find(
(wt: { id: string }) => wt.id === ws.worktreeId
);
branch = worktree?.branch ?? "unknown";
}

localDb.insert(workspaces).values({
id: ws.id,
projectId: ws.projectId,
worktreeId: ws.worktreeId,
type: ws.type ?? "worktree", // Default to worktree for legacy data
branch: branch ?? "unknown",
name: ws.name,
tabOrder: ws.tabOrder,
createdAt: ws.createdAt,
updatedAt: ws.updatedAt,
lastOpenedAt: ws.lastOpenedAt,
}).run();
}
console.log(`[local-db] Migrated ${legacyData.workspaces.length} workspaces`);
}

// Migrate settings
if (legacyData.settings) {
const s = legacyData.settings;
localDb.insert(settings).values({
id: 1,
lastActiveWorkspaceId: s.lastActiveWorkspaceId,
lastUsedApp: s.lastUsedApp,
terminalPresets: s.terminalPresets,
terminalPresetsInitialized: s.terminalPresetsInitialized,
selectedRingtoneId: s.selectedRingtoneId,
}).run();
console.log("[local-db] Migrated settings");
}
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.

Legacy migration uses JSON.parse(readFileSync(...)) on untrusted local files, and then inserts values directly into SQLite. While this is local-only, it still needs basic validation because malformed JSON or unexpected types can lead to runtime crashes or silently corrupted state.

You already have Zod schemas in @superset/local-db; none are used here.

Suggestion

Validate legacyData before inserting.

Pragmatic options:

  • Create a legacyDatabaseSchema (matching the old shape) and safeParse it.
  • Or validate each record minimally (e.g., required keys) before insertion.

Fail fast with a clear log and keep the legacy file in place if validation fails.

Reply with "@CharlieHelps yes please" if you'd like me to add a commit with this suggestion.

Comment on lines +517 to +546
.mutation(({ input }) => {
const { fromIndex, toIndex } = input;

const activeProjects = localDb
.select()
.from(projects)
.where(eq(projects.tabOrder, projects.tabOrder)) // Just get all with non-null tabOrder
.all()
.filter((p) => p.tabOrder !== null)
.sort((a, b) => (a.tabOrder ?? 0) - (b.tabOrder ?? 0));

if (
fromIndex < 0 ||
fromIndex >= activeProjects.length ||
toIndex < 0 ||
toIndex >= activeProjects.length
) {
throw new Error("Invalid fromIndex or toIndex");
}

const [removed] = activeProjects.splice(fromIndex, 1);
activeProjects.splice(toIndex, 0, removed);
const [removed] = activeProjects.splice(fromIndex, 1);
activeProjects.splice(toIndex, 0, removed);

activeProjects.forEach((project, index) => {
const p = data.projects.find((p) => p.id === project.id);
if (p) {
p.tabOrder = index;
}
});
});
for (let i = 0; i < activeProjects.length; i++) {
localDb
.update(projects)
.set({ tabOrder: i })
.where(eq(projects.id, activeProjects[i].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.

Same issue as workspace reorder: projects.reorder runs N updates without a transaction, and it also builds the ordering by fetching all rows and filtering in JS.

Additionally, because activeProjects is derived from current DB state and then you update each row individually, concurrent reorder requests (or other updates changing tabOrder) can interleave and corrupt ordering.

Suggestion

Do two improvements:

  1. Use a proper SQL filter (isNotNull(projects.tabOrder)) + orderBy(projects.tabOrder).
  2. Wrap the update loop in a transaction (or implement a single-statement CASE update).

Reply with "@CharlieHelps yes please" if you'd like me to add a commit with this suggestion.

Comment on lines +978 to +1006
.mutation(({ input }) => {
const { projectId, fromIndex, toIndex } = input;

const projectWorkspaces = localDb
.select()
.from(workspaces)
.where(eq(workspaces.projectId, projectId))
.all()
.sort((a, b) => a.tabOrder - b.tabOrder);

if (
fromIndex < 0 ||
fromIndex >= projectWorkspaces.length ||
toIndex < 0 ||
toIndex >= projectWorkspaces.length
) {
throw new Error("Invalid fromIndex or toIndex");
}

const [removed] = projectWorkspaces.splice(fromIndex, 1);
projectWorkspaces.splice(toIndex, 0, removed);
const [removed] = projectWorkspaces.splice(fromIndex, 1);
projectWorkspaces.splice(toIndex, 0, removed);

projectWorkspaces.forEach((workspace, index) => {
const ws = data.workspaces.find((w) => w.id === workspace.id);
if (ws) {
ws.tabOrder = index;
}
});
});
for (let i = 0; i < projectWorkspaces.length; i++) {
localDb
.update(workspaces)
.set({ tabOrder: i })
.where(eq(workspaces.id, projectWorkspaces[i].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.

Project/workspace reorder operations were converted to loops that execute N separate UPDATEs. That increases IO and makes the reorder non-atomic (a crash mid-loop leaves inconsistent tab orders). This is especially risky because tabOrder drives UI ordering.

For SQLite, you should either (a) wrap the loop in a transaction, or (b) perform a batch update strategy (e.g. temporary offsets, CASE expression) so the reorder is atomic.

Suggestion

At minimum, wrap the sequential updates in a transaction.

With better-sqlite3 you can do:

const tx = sqlite.transaction(() => {
  for (let i = 0; i < projectWorkspaces.length; i++) {
    localDb.update(workspaces)
      .set({ tabOrder: i })
      .where(eq(workspaces.id, projectWorkspaces[i].id))
      .run();
  }
});

tx();

You’ll need access to the underlying sqlite instance (export it or provide a runInTransaction helper from main/lib/local-db).

Reply with "@CharlieHelps yes please" if you'd like me to add a commit with this suggestion.

Comment on lines +9 to +15
import { localDb } from "./lib/local-db";
import { terminalManager } from "./lib/terminal";
import { MainWindow } from "./windows/main";

// Initialize local SQLite database (runs migrations + legacy data migration on import)
console.log("[main] Local database ready:", !!localDb);

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.

main/index.ts now relies on side effects from importing localDb (comment says migrations + legacy migration run on import). But the code also removed await initDb().

That makes the app lifecycle harder to reason about: DB setup occurs before app.whenReady() and cannot be awaited/handled if it fails. If migrations fail, you’ll still proceed into app startup, potentially causing confusing runtime errors later.

This ties into the import-time side effects in main/lib/local-db/index.ts and should be fixed together.

Suggestion

Move DB initialization into the app.whenReady() block and fail fast if migrations fail.

await app.whenReady();
initLocalDb(); // throws on fatal migration issues

Reply with "@CharlieHelps yes please" if you'd like me to add a commit with this suggestion.

Comment on lines +50 to +59
// Copy database migrations from local-db package
const migrationsSrc = resolve(__dirname, "../../packages/local-db/drizzle");
const migrationsDest = resolve(devPath, "resources/migrations");

if (existsSync(migrationsSrc)) {
if (existsSync(migrationsDest)) {
rmSync(migrationsDest, { recursive: true });
}
mkdirSync(migrationsDest, { recursive: true });
cpSync(migrationsSrc, migrationsDest, { recursive: 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.

copyResourcesPlugin() copies the entire packages/local-db/drizzle directory into resources/migrations. That directory includes meta/ and possibly other non-migration artifacts, and the migrator generally expects SQL migration files (and possibly a meta folder depending on how you run it).

This may work today but is brittle: any future generated artifacts in drizzle/ will get packaged into the app resources unnecessarily. Also, the code comment says these are “migrations”, but you’re actually copying the whole Drizzle output folder.

At minimum, consider copying only *.sql (and whatever the migrator truly requires).

Suggestion

Narrow the copy filter to expected files, e.g.:

cpSync(migrationsSrc, migrationsDest, {
  recursive: true,
  filter: (src) => src.endsWith(".sql") || src.includes("meta"),
});

Or keep the Drizzle folder structure but explicitly justify which subfolders are required.

Reply with "@CharlieHelps yes please" if you'd like me to add a commit with this suggestion.

@charliecreates charliecreates Bot removed the request for review from CharlieHelps December 23, 2025 00:03
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: 0

♻️ Duplicate comments (9)
apps/desktop/src/main/lib/local-db/index.ts (5)

27-70: Path resolution for packaged app may produce incorrect path.

On line 34, the path uses join(process.resourcesPath, "resources/migrations"). In packaged Electron apps, process.resourcesPath already points to the resources directory (e.g., .../Contents/Resources on macOS), so this could produce a double-nested path like .../Resources/resources/migrations.


72-84: Module-level side effects may cause issues in test environments.

Database initialization and migrations run at import time, which can cause test failures when Electron's app module isn't available. Consider lazy initialization via an explicit initLocalDb() function called after app.whenReady().


108-213: Wrap legacy migration in a transaction for atomicity.

If the migration fails partway through (e.g., after inserting projects but before worktrees), the database is left in an inconsistent state. The backup won't occur, but partial data remains.


109-109: Validate legacy JSON before inserting.

JSON.parse(readFileSync(...)) on untrusted local files without validation can cause runtime crashes or silently corrupted state. Consider using Zod schemas from @superset/local-db to validate the legacy data structure before migration.


216-218: Consider explicit initialization instead of side-effect execution.

migrateFromLegacyDb() runs at module load time, coupling database migration to import order. Moving this to an explicit initialization step would improve testability and control.

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

296-302: Sequential updates without transaction risk inconsistent tab ordering.

Shifting tabOrder for existing workspaces runs N separate UPDATE statements. If the process crashes mid-loop, workspaces will have inconsistent ordering. Consider wrapping in a transaction.


999-1005: Reorder loop runs N updates without a transaction.

This makes the reorder non-atomic. A crash mid-loop leaves inconsistent tabOrder values, which drives UI ordering.

apps/desktop/src/lib/trpc/routers/projects/projects.ts (2)

545-551: Reorder loop runs N updates without a transaction.

Multiple UPDATE statements without a transaction wrapper means a crash mid-loop leaves tabOrder values inconsistent.


525-531: Tautological query condition always returns all rows.

Line 528 uses eq(projects.tabOrder, projects.tabOrder) which is always true (a column equals itself), effectively returning all projects regardless of tabOrder value. The subsequent .filter((p) => p.tabOrder !== null) on line 530 does the actual filtering in JavaScript.

Use isNotNull instead for a proper SQL filter:

🔎 Proposed fix
+import { desc, eq, isNotNull } from "drizzle-orm";
 // ...
 
 			const activeProjects = localDb
 				.select()
 				.from(projects)
-				.where(eq(projects.tabOrder, projects.tabOrder)) // Just get all with non-null tabOrder
+				.where(isNotNull(projects.tabOrder))
+				.orderBy(projects.tabOrder)
 				.all()
-				.filter((p) => p.tabOrder !== null)
-				.sort((a, b) => (a.tabOrder ?? 0) - (b.tabOrder ?? 0));
+				.sort((a, b) => (a.tabOrder ?? 0) - (b.tabOrder ?? 0)); // Keep sort for safety
🧹 Nitpick comments (4)
apps/desktop/electron.vite.config.ts (1)

50-63: Consider filtering migrations to required files only.

Copying the entire packages/local-db/drizzle directory works, but may include generated artifacts beyond what the migrator needs. Drizzle's migrator typically requires .sql files and the meta/ directory.

Suggested filter (optional)
 cpSync(migrationsSrc, migrationsDest, {
   recursive: true,
+  filter: (src) => {
+    const name = src.split('/').pop() ?? '';
+    return name.endsWith('.sql') || name === 'meta' || !name.includes('.');
+  },
 });

This is low priority if the current approach works in testing.

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

8-15: Consider aligning return type with getWorkspacePath.

getWorktreePath returns string | undefined while getWorkspacePath returns string | null. For consistency within the same module, consider using the same "not found" representation.

Optional: Use null consistently
-export function getWorktreePath(worktreeId: string): string | undefined {
+export function getWorktreePath(worktreeId: string): string | null {
 	const worktree = localDb
 		.select()
 		.from(worktrees)
 		.where(eq(worktrees.id, worktreeId))
 		.get();
-	return worktree?.path;
+	return worktree?.path ?? null;
 }
apps/desktop/src/main/lib/local-db/index.ts (1)

92-104: Migration check only inspects projects table count.

If a previous migration attempt partially inserted data into other tables (worktrees, workspaces, settings) but failed before inserting projects, those orphaned records would remain and subsequent runs would re-attempt migration, potentially causing duplicates or constraint violations.

Consider using a dedicated migration marker (e.g., a meta table with migratedFromLowdbAt timestamp) for more robust detection.

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

38-68: Return value after update may have stale fields.

In upsertProject, after updating an existing project (lines 48-52), line 53 returns a manually constructed object:

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

This spreads existing (which has the old lastOpenedAt value read before the update) and overwrites only two fields. While currently correct since only those fields are updated, this pattern can lead to subtle bugs if additional fields are updated in the future but forgotten in the return.

Consider using .returning().get() on the update, or re-fetching the row:

🔎 Suggested improvement
 	if (existing) {
-		localDb
+		const updated = localDb
 			.update(projects)
 			.set({ lastOpenedAt: Date.now(), defaultBranch })
 			.where(eq(projects.id, existing.id))
+			.returning()
+			.get();
-			.run();
-		return { ...existing, lastOpenedAt: Date.now(), defaultBranch };
+		return updated;
 	}
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b377b9f and c7918b6.

📒 Files selected for processing (15)
  • apps/admin/src/proxy.ts
  • apps/desktop/electron.vite.config.ts
  • apps/desktop/src/lib/trpc/routers/projects/projects.ts
  • apps/desktop/src/lib/trpc/routers/workspaces/utils/worktree.ts
  • apps/desktop/src/lib/trpc/routers/workspaces/workspaces.test.ts
  • apps/desktop/src/lib/trpc/routers/workspaces/workspaces.ts
  • apps/desktop/src/main/lib/local-db/index.ts
  • apps/desktop/src/main/windows/main.ts
  • apps/desktop/src/renderer/components/OpenInButton/OpenInButton.tsx
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/Sidebar/TabsView/TabsCommandDialog/index.tsx
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/Sidebar/TabsView/index.tsx
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/WorkspaceFooter/components/WorkspaceFooterRight/WorkspaceFooterRight.tsx
  • apps/desktop/test-setup.ts
  • packages/local-db/src/schema/index.ts
  • packages/local-db/src/schema/schema.ts
💤 Files with no reviewable changes (1)
  • apps/desktop/src/lib/trpc/routers/workspaces/workspaces.test.ts
🚧 Files skipped from review as they are similar to previous changes (4)
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/Sidebar/TabsView/index.tsx
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/Sidebar/TabsView/TabsCommandDialog/index.tsx
  • apps/desktop/src/renderer/components/OpenInButton/OpenInButton.tsx
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/WorkspaceFooter/components/WorkspaceFooterRight/WorkspaceFooterRight.tsx
🧰 Additional context used
📓 Path-based instructions (5)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Avoid using any type in TypeScript - maintain type safety unless absolutely necessary

Files:

  • packages/local-db/src/schema/index.ts
  • apps/desktop/src/main/lib/local-db/index.ts
  • apps/desktop/test-setup.ts
  • apps/desktop/electron.vite.config.ts
  • packages/local-db/src/schema/schema.ts
  • apps/admin/src/proxy.ts
  • apps/desktop/src/lib/trpc/routers/workspaces/workspaces.ts
  • apps/desktop/src/main/windows/main.ts
  • apps/desktop/src/lib/trpc/routers/projects/projects.ts
  • apps/desktop/src/lib/trpc/routers/workspaces/utils/worktree.ts
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (AGENTS.md)

Run Biome for formatting, linting, import organization, and safe fixes at the root level using bun run lint:fix

Files:

  • packages/local-db/src/schema/index.ts
  • apps/desktop/src/main/lib/local-db/index.ts
  • apps/desktop/test-setup.ts
  • apps/desktop/electron.vite.config.ts
  • packages/local-db/src/schema/schema.ts
  • apps/admin/src/proxy.ts
  • apps/desktop/src/lib/trpc/routers/workspaces/workspaces.ts
  • apps/desktop/src/main/windows/main.ts
  • apps/desktop/src/lib/trpc/routers/projects/projects.ts
  • apps/desktop/src/lib/trpc/routers/workspaces/utils/worktree.ts
apps/desktop/src/{main,renderer,preload}/**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Use type-safe IPC communication - define channel types in apps/desktop/src/shared/ipc-channels.ts before implementing handlers

Files:

  • apps/desktop/src/main/lib/local-db/index.ts
  • apps/desktop/src/main/windows/main.ts
apps/desktop/src/main/**/*.ts

📄 CodeRabbit inference engine (AGENTS.md)

Accept object parameters in IPC handlers - do not use positional parameters in ipcMain.handle()

Files:

  • apps/desktop/src/main/lib/local-db/index.ts
  • apps/desktop/src/main/windows/main.ts
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/main/lib/local-db/index.ts
  • apps/desktop/test-setup.ts
  • apps/desktop/electron.vite.config.ts
  • apps/desktop/src/lib/trpc/routers/workspaces/workspaces.ts
  • apps/desktop/src/main/windows/main.ts
  • apps/desktop/src/lib/trpc/routers/projects/projects.ts
  • apps/desktop/src/lib/trpc/routers/workspaces/utils/worktree.ts
🧠 Learnings (12)
📓 Common learnings
Learnt from: CR
Repo: superset-sh/superset PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-18T23:19:10.415Z
Learning: Applies to packages/db/src/**/*.{ts,tsx} : Store database schema in packages/db/src/ and use Drizzle ORM for schema management
Learnt from: CR
Repo: superset-sh/superset PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-18T23:19:10.415Z
Learning: Applies to apps/api/src/**/*.{ts,tsx} : Use Drizzle ORM for all database operations
📚 Learning: 2025-12-18T23:19:10.415Z
Learnt from: CR
Repo: superset-sh/superset PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-18T23:19:10.415Z
Learning: Applies to packages/db/src/**/*.{ts,tsx} : Store database schema in packages/db/src/ and use Drizzle ORM for schema management

Applied to files:

  • packages/local-db/src/schema/index.ts
  • apps/desktop/src/main/lib/local-db/index.ts
  • apps/desktop/electron.vite.config.ts
  • packages/local-db/src/schema/schema.ts
  • apps/admin/src/proxy.ts
  • apps/desktop/src/lib/trpc/routers/workspaces/workspaces.ts
  • apps/desktop/src/main/windows/main.ts
  • apps/desktop/src/lib/trpc/routers/projects/projects.ts
  • apps/desktop/src/lib/trpc/routers/workspaces/utils/worktree.ts
📚 Learning: 2025-12-18T23:19:10.415Z
Learnt from: CR
Repo: superset-sh/superset PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-18T23:19:10.415Z
Learning: Applies to **/{components,features}/**/[!.]*.tsx : Organize project structure with one folder per component: ComponentName/ComponentName.tsx with index.ts barrel export

Applied to files:

  • packages/local-db/src/schema/index.ts
  • apps/desktop/src/lib/trpc/routers/workspaces/utils/worktree.ts
📚 Learning: 2025-12-18T23:19:10.415Z
Learnt from: CR
Repo: superset-sh/superset PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-18T23:19:10.415Z
Learning: Applies to **/*.{ts,tsx,js,jsx} : Run Biome for formatting, linting, import organization, and safe fixes at the root level using bun run lint:fix

Applied to files:

  • packages/local-db/src/schema/index.ts
  • apps/desktop/electron.vite.config.ts
  • apps/desktop/src/lib/trpc/routers/workspaces/workspaces.ts
  • apps/desktop/src/lib/trpc/routers/workspaces/utils/worktree.ts
📚 Learning: 2025-12-18T23:19:10.415Z
Learnt from: CR
Repo: superset-sh/superset PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-18T23:19:10.415Z
Learning: Applies to apps/api/src/**/*.{ts,tsx} : Use Drizzle ORM for all database operations

Applied to files:

  • apps/desktop/src/main/lib/local-db/index.ts
  • packages/local-db/src/schema/schema.ts
  • apps/admin/src/proxy.ts
  • apps/desktop/src/lib/trpc/routers/workspaces/workspaces.ts
  • apps/desktop/src/lib/trpc/routers/projects/projects.ts
📚 Learning: 2025-12-18T23:19:10.415Z
Learnt from: CR
Repo: superset-sh/superset PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-18T23:19:10.415Z
Learning: Applies to apps/desktop/src/main/index.ts : Load environment variables in src/main/index.ts and electron.vite.config.ts with override: true before other imports

Applied to files:

  • apps/desktop/electron.vite.config.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} : Use alias as defined in `tsconfig.json` when possible

Applied to files:

  • apps/desktop/electron.vite.config.ts
📚 Learning: 2025-12-18T23:19:10.415Z
Learnt from: CR
Repo: superset-sh/superset PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-18T23:19:10.415Z
Learning: Applies to apps/desktop/src/renderer/**/*.{ts,tsx} : Never import Node.js modules in renderer process or shared code - use only in main process (src/main/)

Applied to files:

  • apps/desktop/electron.vite.config.ts
  • apps/desktop/src/lib/trpc/routers/workspaces/utils/worktree.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/routers/workspaces/workspaces.ts
  • apps/desktop/src/main/windows/main.ts
  • apps/desktop/src/lib/trpc/routers/workspaces/utils/worktree.ts
📚 Learning: 2025-12-18T23:19:10.415Z
Learnt from: CR
Repo: superset-sh/superset PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-18T23:19:10.415Z
Learning: Applies to apps/desktop/src/{main,renderer,preload}/**/*.{ts,tsx} : Use type-safe IPC communication - define channel types in apps/desktop/src/shared/ipc-channels.ts before implementing handlers

Applied to files:

  • apps/desktop/src/lib/trpc/routers/workspaces/utils/worktree.ts
📚 Learning: 2025-12-18T23:19:10.415Z
Learnt from: CR
Repo: superset-sh/superset PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-18T23:19:10.415Z
Learning: Applies to **/{components,features}/**/*.{ts,tsx,test.ts,test.tsx,stories.tsx} : Co-locate component dependencies (utils, hooks, constants, config, tests, stories) next to the file using them

Applied to files:

  • apps/desktop/src/lib/trpc/routers/workspaces/utils/worktree.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 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

Applied to files:

  • apps/desktop/src/lib/trpc/routers/workspaces/utils/worktree.ts
🧬 Code graph analysis (4)
apps/desktop/src/main/lib/local-db/index.ts (2)
apps/desktop/src/main/lib/app-environment.ts (2)
  • DB_PATH (8-8)
  • SUPERSET_HOME_DIR (5-5)
packages/local-db/src/schema/schema.ts (4)
  • projects (14-39)
  • worktrees (47-69)
  • workspaces (77-108)
  • settings (116-127)
apps/desktop/src/lib/trpc/routers/workspaces/workspaces.ts (2)
apps/desktop/src/main/lib/local-db/index.ts (1)
  • localDb (80-80)
packages/local-db/src/schema/schema.ts (5)
  • projects (14-39)
  • worktrees (47-69)
  • workspaces (77-108)
  • settings (116-127)
  • SelectWorktree (72-72)
apps/desktop/src/lib/trpc/routers/projects/projects.ts (3)
packages/local-db/src/schema/schema.ts (2)
  • SelectProject (42-42)
  • projects (14-39)
apps/desktop/src/main/lib/local-db/index.ts (1)
  • localDb (80-80)
apps/desktop/src/lib/trpc/routers/projects/utils/colors/colors.ts (1)
  • assignRandomColor (3-7)
apps/desktop/src/lib/trpc/routers/workspaces/utils/worktree.ts (2)
apps/desktop/src/main/lib/local-db/index.ts (1)
  • localDb (80-80)
packages/local-db/src/schema/schema.ts (3)
  • worktrees (47-69)
  • SelectWorkspace (111-111)
  • projects (14-39)
🔇 Additional comments (27)
apps/admin/src/proxy.ts (1)

2-5: LGTM! Import refactoring follows Drizzle ORM best practices.

The import path changes are correct:

  • Importing db from @superset/db/client separates the database client from the schema
  • Importing eq directly from drizzle-orm follows the recommended pattern (operators should come from the ORM library, not re-exported)

The behavioral logic remains unchanged, and the query usage at lines 31-33 is still correct.

packages/local-db/src/schema/index.ts (1)

1-3: LGTM!

The barrel exports are correctly ordered alphabetically (relations, schema, zod), which satisfies Biome's import/export organization rules.

apps/desktop/test-setup.ts (2)

70-71: LGTM!

Good additions for the Electron app mock—getAppPath and isPackaged are commonly needed for path resolution in the main process.


97-136: Local DB mock structure looks reasonable.

The chainable mock API mirrors Drizzle ORM's query builder pattern. One consideration: the mock always returns static values (null, [], { id: "test-id" }), which works for basic smoke tests but won't help verify actual query logic.

For tests that need to verify database interactions, consider either:

  1. Using integration tests with a real in-memory SQLite database
  2. Allowing per-test mock customization via mockImplementation

The current approach is pragmatic given better-sqlite3's incompatibility with Bun.

apps/desktop/src/main/windows/main.ts (2)

2-3: LGTM!

Imports are correctly structured—schema entities from @superset/local-db, query helper from drizzle-orm, and the database instance from the local module.

Also applies to: 8-8


92-106: Clean migration to Drizzle ORM queries.

The workspace name resolution logic correctly:

  1. Queries workspace by ID
  2. Conditionally fetches the related worktree if worktreeId exists
  3. Falls back through workspace.nameworktree.branch"Workspace"

The synchronous .get() calls are appropriate here since better-sqlite3 is synchronous and this runs in the main process notification handler.

apps/desktop/electron.vite.config.ts (2)

38-48: LGTM!

Good variable renaming for clarity—soundsSrc/soundsDest makes the purpose explicit.


104-104: Correct externalization of native module.

better-sqlite3 must remain external since it's a native Node.js addon that can't be bundled by Rollup.

apps/desktop/src/lib/trpc/routers/workspaces/utils/worktree.ts (2)

1-3: LGTM!

Imports are well-organized—external packages first (@superset/local-db, drizzle-orm), then local modules.


22-43: Clean implementation of workspace path resolution.

The branching logic correctly handles both workspace types:

  • branch type → fetch project's mainRepoPath
  • worktree type → fetch worktree's path

The null coalescing (?? null) provides explicit null returns when entities aren't found.

apps/desktop/src/main/lib/local-db/index.ts (1)

1-15: LGTM!

Imports are well-organized, using the new @superset/local-db package for schema and Drizzle ORM for database operations. This aligns with the learnings to use Drizzle ORM for all database operations.

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

3-11: LGTM!

Import structure is clean and properly organized. The previous unused SelectWorkspace import has been removed, leaving only the necessary SelectWorktree type import.


50-54: LGTM!

Clean migration to Drizzle ORM query pattern. The localDb.select().from(projects).where(eq(...)).get() pattern correctly retrieves a single project record.


120-135: Good use of .returning().get() for insert.

The pattern correctly inserts and retrieves the new worktree record in a single operation, avoiding a separate query.


162-170: Good use of upsert pattern for settings.

The onConflictDoUpdate pattern correctly handles the singleton settings row, ensuring idempotent updates.


512-515: Type narrowing for workspace.type.

The cast workspace.type as "worktree" | "branch" at line 551 and similar locations (lines 637, etc.) is necessary because Drizzle infers string from the schema's .$type<WorkspaceType>(). This is acceptable but could be improved by ensuring the schema type flows through correctly.


840-850: Null coalescing to undefined is handled correctly.

Line 850 uses ?? undefined to convert the potential null from .get() to undefined for the worktree variable. This aligns with the conditional logic below that checks if (worktree && project).


891-895: Consider cascade behavior for workspace-worktree deletion.

The workspace is deleted first (line 891), then the worktree (line 894). Given the schema has onDelete: "cascade" on workspaces.worktreeId, deleting the worktree first would automatically cascade to workspaces. However, the current order works correctly since the workspace deletion doesn't trigger a cascade from worktree.

packages/local-db/src/schema/schema.ts (6)

1-9: LGTM!

Imports are well-organized, bringing in Drizzle SQLite utilities and the custom Zod types for type-safe JSON columns.


14-39: Well-structured projects table.

Good schema design with:

  • UUID primary key with auto-generation
  • Appropriate indexes on mainRepoPath and lastOpenedAt
  • Nullable tabOrder allowing projects to be "inactive" (not in sidebar)
  • Boolean mode for configToastDismissed

47-69: Worktrees table with proper foreign key cascade.

The onDelete: "cascade" on projectId ensures worktrees are automatically cleaned up when a project is deleted. JSON columns for gitStatus and githubStatus with typed generics provide type safety.


77-108: Workspaces table with dual foreign key relationships.

Good design where:

  • projectId is required with cascade delete
  • worktreeId is optional (null for branch-type workspaces) with cascade delete
  • tabOrder is required (unlike projects) since workspaces always appear in UI

116-127: Settings table uses singleton pattern.

The id.default(1) ensures a single settings row. This works well with the onConflictDoUpdate upsert pattern used in the routers.


41-42: Good type exports for Insert/Select patterns.

Exporting both InsertX and SelectX types for each table enables type-safe operations throughout the codebase.

Also applies to: 71-72, 110-111, 129-130

apps/desktop/src/lib/trpc/routers/projects/projects.ts (3)

4-8: LGTM!

Clean imports from the new @superset/local-db package and drizzle-orm operators.


70-135: Good URL parsing and validation logic.

The extractRepoName function properly handles:

  • HTTP/HTTPS URLs
  • SSH-style URLs (git@host:user/repo)
  • Query strings and hashes
  • .git suffix stripping
  • Percent-encoded characters
  • Safe filename validation

400-428: Good handling of stale project records.

The clone logic properly handles the case where a project record exists but the filesystem path is gone—it deletes the stale record and continues with the clone. This is defensive coding that prevents orphaned records.

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

♻️ Duplicate comments (3)
apps/desktop/src/lib/trpc/routers/workspaces/workspaces.ts (1)

1016-1022: Same atomicity concern as createBranchWorkspace.

This loop also executes sequential updates without transaction wrapping, risking inconsistent tabOrder values on failure.

apps/desktop/src/main/lib/local-db/index.ts (1)

108-214: Wrap legacy migration in a transaction for atomicity.

The migration inserts data across multiple tables (projects, worktrees, workspaces, settings) without transaction wrapping. If the app crashes or an error occurs mid-migration, the database will be left in an inconsistent state—some records inserted, others not—and the backup may not be created.

Additionally, the "already migrated" check only examines the projects table count (line 98-99). If a previous attempt partially inserted worktrees or workspaces but failed before projects, subsequent runs could produce duplicate or inconsistent data.

🔎 Wrap in a transaction for atomic migration
 	try {
 		const legacyData = JSON.parse(readFileSync(LEGACY_DB_PATH, "utf-8"));
 
+		// Use a transaction for atomic migration
+		sqlite.transaction(() => {
 		// Migrate projects
 		if (legacyData.projects?.length > 0) {
 			for (const p of legacyData.projects) {
 				localDb
 					.insert(projects)
 					.values({
 						// ... existing values
 					})
 					.run();
 			}
 			// ... rest of migration
 		}
+		})();
 
 		// Backup the legacy db.json (only after successful transaction)
 		const backupPath = `${LEGACY_DB_PATH}.backup`;

This requires exporting the sqlite instance or providing a runInTransaction helper from this module.

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

522-551: Transaction and query issues already flagged.

The concerns about missing transaction and the confusing query pattern on line 528 (eq(projects.tabOrder, projects.tabOrder)) were raised in the previous review.

As a quick note: the eq(projects.tabOrder, projects.tabOrder) condition is a SQL tautology that exploits NULL ≠ NULL behavior to exclude NULL rows, but it's non-obvious. Using isNotNull(projects.tabOrder) from drizzle-orm would be clearer and would also eliminate the redundant JS .filter() on line 530.

🧹 Nitpick comments (5)
apps/desktop/src/main/lib/local-db/index.ts (1)

72-84: Consider lazy initialization to avoid test environment issues.

Database initialization and migrations run at module import time (lines 72-84), which can cause issues in test environments where Electron's app module isn't available. While the isElectron guard at lines 29-31 provides some protection, the initialization still occurs eagerly.

A lazy initialization pattern (as suggested in past reviews) would allow explicit control over when the database is initialized:

🔎 Lazy initialization pattern
-const migrationsFolder = getMigrationsDirectory();
-
-const sqlite = new Database(DB_PATH);
-sqlite.pragma("journal_mode = WAL");
-
-console.log(`[local-db] Database initialized at: ${DB_PATH}`);
-console.log(`[local-db] Running migrations from: ${migrationsFolder}`);
-
-export const localDb = drizzle(sqlite, { schema });
-
-migrate(localDb, { migrationsFolder });
-
-console.log("[local-db] Migrations complete");
+let _localDb: ReturnType<typeof drizzle<typeof schema>> | null = null;
+
+export function initLocalDb() {
+  if (_localDb) return _localDb;
+  
+  const migrationsFolder = getMigrationsDirectory();
+  const sqlite = new Database(DB_PATH);
+  sqlite.pragma("journal_mode = WAL");
+  
+  console.log(`[local-db] Database initialized at: ${DB_PATH}`);
+  console.log(`[local-db] Running migrations from: ${migrationsFolder}`);
+  
+  _localDb = drizzle(sqlite, { schema });
+  migrate(_localDb, { migrationsFolder });
+  
+  console.log("[local-db] Migrations complete");
+  migrateFromLegacyDb();
+  
+  return _localDb;
+}
+
+export function getLocalDb() {
+  if (!_localDb) throw new Error("localDb not initialized. Call initLocalDb() first.");
+  return _localDb;
+}

Then call initLocalDb() after app.whenReady() in the main process.

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

906-943: Consider transaction for delete cleanup operations.

The delete mutation performs multiple DB operations (delete workspace, delete worktree, update project tabOrder, update settings) without transaction wrapping. While less critical than reordering (since external operations precede DB cleanup), wrapping these in a transaction would improve atomicity.

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

39-58: Consider extracting the repeated upsert pattern into a helper.

The same insert(...).values({id: 1, ...}).onConflictDoUpdate({target: settings.id, set: {...}}).run() pattern appears 6 times. Extracting it would reduce duplication and make future changes easier.

Example helper
function updateSettings(patch: Partial<typeof settings.$inferInsert>) {
  localDb
    .insert(settings)
    .values({ id: 1, ...patch })
    .onConflictDoUpdate({
      target: settings.id,
      set: patch,
    })
    .run();
}

Then usages simplify to:

updateSettings({ terminalPresets: presets });
updateSettings({ selectedRingtoneId: input.ringtoneId });

Also applies to: 73-96, 103-115, 138-145, 157-164

apps/desktop/src/lib/trpc/routers/projects/projects.ts (2)

38-68: Consider using .returning() on the update path for consistency.

Line 53 manually constructs the return object { ...existing, lastOpenedAt: Date.now(), defaultBranch }. If the schema adds fields with DB-side defaults or triggers, this manual construction may diverge from actual DB state.

Suggested fix
 	if (existing) {
-		localDb
+		const updated = localDb
 			.update(projects)
 			.set({ lastOpenedAt: Date.now(), defaultBranch })
 			.where(eq(projects.id, existing.id))
-			.run();
-		return { ...existing, lastOpenedAt: Date.now(), defaultBranch };
+			.returning()
+			.get();
+		return updated;
 	}

447-456: Minor duplication: this insert mirrors upsertProject.

Lines 447-456 duplicate the insert logic from upsertProject (lines 56-65). Consider reusing upsertProject(clonePath, defaultBranch) here to keep project creation logic centralized.

Suggested change
 				// Create new project
-				const name = basename(clonePath);
 				const defaultBranch = await getDefaultBranch(clonePath);
-				const project = localDb
-					.insert(projects)
-					.values({
-						mainRepoPath: clonePath,
-						name,
-						color: assignRandomColor(),
-						defaultBranch,
-					})
-					.returning()
-					.get();
+				const project = upsertProject(clonePath, defaultBranch);
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c7918b6 and dab84a3.

⛔ Files ignored due to path filters (1)
  • bun.lock is excluded by !**/*.lock
📒 Files selected for processing (44)
  • apps/admin/package.json
  • apps/admin/src/proxy.ts
  • apps/desktop/electron-builder.ts
  • apps/desktop/electron.vite.config.ts
  • apps/desktop/package.json
  • apps/desktop/scripts/copy-native-modules.ts
  • apps/desktop/src/lib/trpc/routers/changes/branches.ts
  • apps/desktop/src/lib/trpc/routers/config/config.ts
  • apps/desktop/src/lib/trpc/routers/external/helpers.ts
  • apps/desktop/src/lib/trpc/routers/external/index.ts
  • apps/desktop/src/lib/trpc/routers/projects/projects.ts
  • apps/desktop/src/lib/trpc/routers/settings/index.ts
  • apps/desktop/src/lib/trpc/routers/terminal/terminal.ts
  • apps/desktop/src/lib/trpc/routers/workspaces/utils/github/github.ts
  • apps/desktop/src/lib/trpc/routers/workspaces/utils/worktree.ts
  • apps/desktop/src/lib/trpc/routers/workspaces/workspaces.test.ts
  • apps/desktop/src/lib/trpc/routers/workspaces/workspaces.ts
  • apps/desktop/src/main/index.ts
  • apps/desktop/src/main/lib/db/index.ts
  • apps/desktop/src/main/lib/db/schemas.ts
  • apps/desktop/src/main/lib/local-db/index.ts
  • apps/desktop/src/main/lib/notification-sound.ts
  • apps/desktop/src/main/windows/main.ts
  • apps/desktop/src/renderer/components/OpenInButton/OpenInButton.tsx
  • apps/desktop/src/renderer/screens/main/components/SettingsView/PresetsSettings/types.ts
  • apps/desktop/src/renderer/screens/main/components/TopBar/WorkspaceTabs/WorkspaceHoverCard/components/ChecksList/ChecksList.tsx
  • apps/desktop/src/renderer/screens/main/components/TopBar/WorkspaceTabs/WorkspaceHoverCard/components/ChecksList/components/CheckItemRow/CheckItemRow.tsx
  • apps/desktop/src/renderer/screens/main/components/TopBar/WorkspaceTabs/WorkspaceHoverCard/components/ChecksSummary/ChecksSummary.tsx
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/Sidebar/TabsView/TabsCommandDialog/index.tsx
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/Sidebar/TabsView/index.tsx
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/WorkspaceFooter/components/WorkspaceFooterRight/WorkspaceFooterRight.tsx
  • apps/desktop/test-setup.ts
  • packages/db/src/index.ts
  • packages/local-db/drizzle.config.ts
  • packages/local-db/drizzle/0000_initial_schema.sql
  • packages/local-db/drizzle/meta/0000_snapshot.json
  • packages/local-db/drizzle/meta/_journal.json
  • packages/local-db/package.json
  • packages/local-db/src/index.ts
  • packages/local-db/src/schema/index.ts
  • packages/local-db/src/schema/relations.ts
  • packages/local-db/src/schema/schema.ts
  • packages/local-db/src/schema/zod.ts
  • packages/local-db/tsconfig.json
💤 Files with no reviewable changes (4)
  • apps/desktop/src/main/lib/db/index.ts
  • apps/desktop/src/lib/trpc/routers/workspaces/workspaces.test.ts
  • apps/desktop/src/main/lib/db/schemas.ts
  • packages/db/src/index.ts
✅ Files skipped from review due to trivial changes (1)
  • packages/local-db/drizzle/meta/_journal.json
🚧 Files skipped from review as they are similar to previous changes (24)
  • packages/local-db/src/index.ts
  • apps/desktop/scripts/copy-native-modules.ts
  • apps/desktop/src/renderer/screens/main/components/TopBar/WorkspaceTabs/WorkspaceHoverCard/components/ChecksSummary/ChecksSummary.tsx
  • apps/desktop/src/lib/trpc/routers/external/index.ts
  • packages/local-db/tsconfig.json
  • apps/desktop/src/main/lib/notification-sound.ts
  • packages/local-db/package.json
  • apps/admin/package.json
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/Sidebar/TabsView/TabsCommandDialog/index.tsx
  • apps/desktop/src/lib/trpc/routers/config/config.ts
  • apps/desktop/src/renderer/screens/main/components/SettingsView/PresetsSettings/types.ts
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/WorkspaceFooter/components/WorkspaceFooterRight/WorkspaceFooterRight.tsx
  • apps/desktop/src/renderer/screens/main/components/TopBar/WorkspaceTabs/WorkspaceHoverCard/components/ChecksList/components/CheckItemRow/CheckItemRow.tsx
  • apps/desktop/src/lib/trpc/routers/workspaces/utils/github/github.ts
  • packages/local-db/drizzle.config.ts
  • packages/local-db/drizzle/meta/0000_snapshot.json
  • apps/desktop/package.json
  • packages/local-db/src/schema/zod.ts
  • packages/local-db/src/schema/schema.ts
  • apps/desktop/src/renderer/screens/main/components/TopBar/WorkspaceTabs/WorkspaceHoverCard/components/ChecksList/ChecksList.tsx
  • apps/desktop/src/main/index.ts
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/Sidebar/TabsView/index.tsx
  • apps/desktop/electron.vite.config.ts
  • apps/desktop/src/lib/trpc/routers/external/helpers.ts
🧰 Additional context used
📓 Path-based instructions (10)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Avoid using any type in TypeScript - maintain type safety unless absolutely necessary

Files:

  • packages/local-db/src/schema/index.ts
  • apps/desktop/src/main/lib/local-db/index.ts
  • apps/admin/src/proxy.ts
  • apps/desktop/src/lib/trpc/routers/terminal/terminal.ts
  • apps/desktop/src/main/windows/main.ts
  • apps/desktop/src/lib/trpc/routers/workspaces/utils/worktree.ts
  • apps/desktop/src/lib/trpc/routers/changes/branches.ts
  • apps/desktop/electron-builder.ts
  • apps/desktop/src/lib/trpc/routers/projects/projects.ts
  • apps/desktop/src/lib/trpc/routers/settings/index.ts
  • apps/desktop/src/lib/trpc/routers/workspaces/workspaces.ts
  • apps/desktop/test-setup.ts
  • packages/local-db/src/schema/relations.ts
  • apps/desktop/src/renderer/components/OpenInButton/OpenInButton.tsx
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (AGENTS.md)

Run Biome for formatting, linting, import organization, and safe fixes at the root level using bun run lint:fix

Files:

  • packages/local-db/src/schema/index.ts
  • apps/desktop/src/main/lib/local-db/index.ts
  • apps/admin/src/proxy.ts
  • apps/desktop/src/lib/trpc/routers/terminal/terminal.ts
  • apps/desktop/src/main/windows/main.ts
  • apps/desktop/src/lib/trpc/routers/workspaces/utils/worktree.ts
  • apps/desktop/src/lib/trpc/routers/changes/branches.ts
  • apps/desktop/electron-builder.ts
  • apps/desktop/src/lib/trpc/routers/projects/projects.ts
  • apps/desktop/src/lib/trpc/routers/settings/index.ts
  • apps/desktop/src/lib/trpc/routers/workspaces/workspaces.ts
  • apps/desktop/test-setup.ts
  • packages/local-db/src/schema/relations.ts
  • apps/desktop/src/renderer/components/OpenInButton/OpenInButton.tsx
apps/desktop/src/{main,renderer,preload}/**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Use type-safe IPC communication - define channel types in apps/desktop/src/shared/ipc-channels.ts before implementing handlers

Files:

  • apps/desktop/src/main/lib/local-db/index.ts
  • apps/desktop/src/main/windows/main.ts
  • apps/desktop/src/renderer/components/OpenInButton/OpenInButton.tsx
apps/desktop/src/main/**/*.ts

📄 CodeRabbit inference engine (AGENTS.md)

Accept object parameters in IPC handlers - do not use positional parameters in ipcMain.handle()

Files:

  • apps/desktop/src/main/lib/local-db/index.ts
  • apps/desktop/src/main/windows/main.ts
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/main/lib/local-db/index.ts
  • apps/desktop/src/lib/trpc/routers/terminal/terminal.ts
  • apps/desktop/src/main/windows/main.ts
  • apps/desktop/src/lib/trpc/routers/workspaces/utils/worktree.ts
  • apps/desktop/src/lib/trpc/routers/changes/branches.ts
  • apps/desktop/electron-builder.ts
  • apps/desktop/src/lib/trpc/routers/projects/projects.ts
  • apps/desktop/src/lib/trpc/routers/settings/index.ts
  • apps/desktop/src/lib/trpc/routers/workspaces/workspaces.ts
  • apps/desktop/test-setup.ts
  • apps/desktop/src/renderer/components/OpenInButton/OpenInButton.tsx
**/{components,features}/**/[!.]*.tsx

📄 CodeRabbit inference engine (AGENTS.md)

Organize project structure with one folder per component: ComponentName/ComponentName.tsx with index.ts barrel export

Files:

  • apps/desktop/src/renderer/components/OpenInButton/OpenInButton.tsx
**/{components,features}/**/*.{ts,tsx,test.ts,test.tsx,stories.tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Co-locate component dependencies (utils, hooks, constants, config, tests, stories) next to the file using them

Files:

  • apps/desktop/src/renderer/components/OpenInButton/OpenInButton.tsx
apps/desktop/src/renderer/**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Never import Node.js modules in renderer process or shared code - use only in main process (src/main/)

Files:

  • apps/desktop/src/renderer/components/OpenInButton/OpenInButton.tsx
**/*.{tsx,css}

📄 CodeRabbit inference engine (AGENTS.md)

Use React + TailwindCSS v4 + shadcn/ui for UI development

Files:

  • apps/desktop/src/renderer/components/OpenInButton/OpenInButton.tsx
**/{components,features}/**/*.tsx

📄 CodeRabbit inference engine (AGENTS.md)

**/{components,features}/**/*.tsx: Nest components in parent's components/ folder if used only once, promote to highest shared parent's components/ if used 2+ times
Use one component per file - do not combine multiple components in a single file

Files:

  • apps/desktop/src/renderer/components/OpenInButton/OpenInButton.tsx
🧠 Learnings (10)
📓 Common learnings
Learnt from: CR
Repo: superset-sh/superset PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-18T23:19:10.415Z
Learning: Applies to packages/db/src/**/*.{ts,tsx} : Store database schema in packages/db/src/ and use Drizzle ORM for schema management
📚 Learning: 2025-12-18T23:19:10.415Z
Learnt from: CR
Repo: superset-sh/superset PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-18T23:19:10.415Z
Learning: Applies to packages/db/src/**/*.{ts,tsx} : Store database schema in packages/db/src/ and use Drizzle ORM for schema management

Applied to files:

  • packages/local-db/src/schema/index.ts
  • apps/desktop/src/main/lib/local-db/index.ts
  • apps/admin/src/proxy.ts
  • apps/desktop/src/lib/trpc/routers/terminal/terminal.ts
  • apps/desktop/src/main/windows/main.ts
  • apps/desktop/src/lib/trpc/routers/workspaces/utils/worktree.ts
  • apps/desktop/src/lib/trpc/routers/changes/branches.ts
  • apps/desktop/src/lib/trpc/routers/projects/projects.ts
  • packages/local-db/drizzle/0000_initial_schema.sql
  • apps/desktop/src/lib/trpc/routers/workspaces/workspaces.ts
  • packages/local-db/src/schema/relations.ts
📚 Learning: 2025-12-18T23:19:10.415Z
Learnt from: CR
Repo: superset-sh/superset PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-18T23:19:10.415Z
Learning: Applies to **/{components,features}/**/[!.]*.tsx : Organize project structure with one folder per component: ComponentName/ComponentName.tsx with index.ts barrel export

Applied to files:

  • packages/local-db/src/schema/index.ts
  • apps/desktop/src/lib/trpc/routers/workspaces/utils/worktree.ts
📚 Learning: 2025-12-18T23:19:10.415Z
Learnt from: CR
Repo: superset-sh/superset PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-18T23:19:10.415Z
Learning: Applies to **/*.{ts,tsx,js,jsx} : Run Biome for formatting, linting, import organization, and safe fixes at the root level using bun run lint:fix

Applied to files:

  • packages/local-db/src/schema/index.ts
  • apps/desktop/src/lib/trpc/routers/workspaces/utils/worktree.ts
  • apps/desktop/src/lib/trpc/routers/workspaces/workspaces.ts
📚 Learning: 2025-12-18T23:19:10.415Z
Learnt from: CR
Repo: superset-sh/superset PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-18T23:19:10.415Z
Learning: Applies to apps/api/src/**/*.{ts,tsx} : Use Drizzle ORM for all database operations

Applied to files:

  • apps/desktop/src/main/lib/local-db/index.ts
  • apps/admin/src/proxy.ts
  • apps/desktop/src/lib/trpc/routers/terminal/terminal.ts
  • apps/desktop/src/main/windows/main.ts
  • apps/desktop/src/lib/trpc/routers/changes/branches.ts
  • apps/desktop/src/lib/trpc/routers/projects/projects.ts
  • apps/desktop/src/lib/trpc/routers/workspaces/workspaces.ts
  • packages/local-db/src/schema/relations.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/routers/terminal/terminal.ts
  • apps/desktop/src/main/windows/main.ts
  • apps/desktop/src/lib/trpc/routers/workspaces/utils/worktree.ts
  • apps/desktop/src/lib/trpc/routers/workspaces/workspaces.ts
📚 Learning: 2025-12-18T23:19:10.415Z
Learnt from: CR
Repo: superset-sh/superset PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-18T23:19:10.415Z
Learning: Applies to apps/desktop/src/renderer/**/*.{ts,tsx} : Never import Node.js modules in renderer process or shared code - use only in main process (src/main/)

Applied to files:

  • apps/desktop/src/lib/trpc/routers/workspaces/utils/worktree.ts
  • apps/desktop/src/renderer/components/OpenInButton/OpenInButton.tsx
📚 Learning: 2025-12-18T23:19:10.415Z
Learnt from: CR
Repo: superset-sh/superset PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-18T23:19:10.415Z
Learning: Applies to apps/desktop/src/{main,renderer,preload}/**/*.{ts,tsx} : Use type-safe IPC communication - define channel types in apps/desktop/src/shared/ipc-channels.ts before implementing handlers

Applied to files:

  • apps/desktop/src/lib/trpc/routers/workspaces/utils/worktree.ts
📚 Learning: 2025-12-18T23:19:10.415Z
Learnt from: CR
Repo: superset-sh/superset PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-18T23:19:10.415Z
Learning: Create database migrations by modifying Drizzle schema in packages/db/src/schema and running drizzle-kit generate with snake_case migration names

Applied to files:

  • packages/local-db/drizzle/0000_initial_schema.sql
📚 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} : Use alias as defined in `tsconfig.json` when possible

Applied to files:

  • apps/desktop/src/renderer/components/OpenInButton/OpenInButton.tsx
🧬 Code graph analysis (8)
apps/desktop/src/main/lib/local-db/index.ts (2)
apps/desktop/src/main/lib/app-environment.ts (2)
  • DB_PATH (8-8)
  • SUPERSET_HOME_DIR (5-5)
packages/local-db/src/schema/schema.ts (4)
  • projects (14-39)
  • worktrees (47-69)
  • workspaces (77-108)
  • settings (116-127)
apps/desktop/src/main/windows/main.ts (2)
apps/desktop/src/main/lib/local-db/index.ts (1)
  • localDb (80-80)
packages/local-db/src/schema/schema.ts (2)
  • workspaces (77-108)
  • worktrees (47-69)
apps/desktop/src/lib/trpc/routers/workspaces/utils/worktree.ts (2)
apps/desktop/src/main/lib/local-db/index.ts (1)
  • localDb (80-80)
packages/local-db/src/schema/schema.ts (3)
  • worktrees (47-69)
  • SelectWorkspace (111-111)
  • projects (14-39)
apps/desktop/src/lib/trpc/routers/changes/branches.ts (2)
apps/desktop/src/main/lib/local-db/index.ts (1)
  • localDb (80-80)
packages/local-db/src/schema/schema.ts (1)
  • worktrees (47-69)
apps/desktop/src/lib/trpc/routers/projects/projects.ts (3)
packages/local-db/src/schema/schema.ts (2)
  • SelectProject (42-42)
  • projects (14-39)
apps/desktop/src/main/lib/local-db/index.ts (1)
  • localDb (80-80)
apps/desktop/src/lib/trpc/routers/projects/utils/colors/colors.ts (1)
  • assignRandomColor (3-7)
apps/desktop/src/lib/trpc/routers/settings/index.ts (4)
apps/desktop/src/shared/ringtones.ts (2)
  • RINGTONES (21-129)
  • DEFAULT_RINGTONE_ID (131-131)
apps/desktop/src/main/lib/local-db/index.ts (1)
  • localDb (80-80)
packages/local-db/src/schema/schema.ts (1)
  • settings (116-127)
packages/local-db/src/schema/zod.ts (1)
  • TerminalPreset (61-61)
apps/desktop/src/lib/trpc/routers/workspaces/workspaces.ts (3)
apps/desktop/src/main/lib/local-db/index.ts (1)
  • localDb (80-80)
packages/local-db/src/schema/schema.ts (5)
  • projects (14-39)
  • worktrees (47-69)
  • workspaces (77-108)
  • settings (116-127)
  • SelectWorktree (72-72)
apps/desktop/src/main/lib/analytics/index.ts (1)
  • track (45-66)
packages/local-db/src/schema/relations.ts (1)
packages/local-db/src/schema/schema.ts (3)
  • projects (14-39)
  • worktrees (47-69)
  • workspaces (77-108)
⏰ 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). (7)
  • GitHub Check: Deploy Docs
  • GitHub Check: Deploy API
  • GitHub Check: Deploy Marketing
  • GitHub Check: Deploy Admin
  • GitHub Check: Deploy Web
  • GitHub Check: Build
  • GitHub Check: Lint
🔇 Additional comments (28)
apps/desktop/electron-builder.ts (3)

36-40: LGTM! Correct handling of native module.

The addition of better-sqlite3 to asarUnpack is necessary and correct. Native modules with binary dependencies must be unpacked from the ASAR archive to be accessible at runtime. This follows the same pattern as the existing node-pty configuration.


59-63: LGTM! Consistent native module handling.

The better-sqlite3 copy rule is correctly configured and follows the same pattern as node-pty. The dual inclusion strategy (in files array + asarUnpack) is necessary for native modules:

  • The files entry ensures it's included in the ASAR
  • The asarUnpack entry (line 36) ensures it's extracted and accessible at runtime

50-55: Migration copy chain is properly configured.

The migration copy rule correctly makes Drizzle migrations accessible at runtime. Verified that electron.vite.config.ts is configured to copy migrations from packages/local-db/drizzle to dist/resources/migrations during the Vite build, and electron-builder.ts properly copies them from dist/resources/migrations to the packaged app's resources/migrations directory.

apps/admin/src/proxy.ts (1)

2-5: LGTM!

The import refactoring correctly sources db from the more specific @superset/db/client path and eq directly from drizzle-orm. This aligns with the PR's broader migration pattern and improves import clarity.

apps/desktop/test-setup.ts (2)

70-71: LGTM!

Good additions to the Electron app mock. getAppPath and isPackaged are commonly needed Electron APIs that the code may depend on.


110-148: The mock adequately covers the Drizzle ORM builder APIs actually used in the codebase. Since localDb.query is not referenced anywhere in the code, adding relational query support to this mock is unnecessary.

Likely an incorrect or invalid review comment.

apps/desktop/src/lib/trpc/routers/changes/branches.ts (2)

1-3: LGTM!

Clean imports following the new local-db pattern. Using eq from drizzle-orm and worktrees schema from @superset/local-db is consistent with the project's migration approach.


64-87: LGTM!

The migration from in-memory db to Drizzle ORM is well-implemented:

  • Synchronous .get() is appropriate for better-sqlite3
  • The gitStatus update correctly preserves existing status while updating the branch field
  • Using .run() for the update is correct since no return value is needed
packages/local-db/drizzle/0000_initial_schema.sql (2)

1-55: Good schema design with appropriate indexes.

The schema includes:

  • Proper cascade deletes for referential integrity
  • Indexes on foreign keys and frequently queried columns (last_opened_at, created_at, branch)
  • Sensible column types with text for UUIDs and integer for timestamps

24-37: The foreign key ordering is not problematic for this initial schema. PRAGMA foreign_keys setting is not required for creating foreign keys, but it is required for enforcing foreign keys. Since this migration creates all tables in a single transaction before any data insertion, the workspaces table can safely reference worktrees before it's created—SQLite only validates foreign key constraints during data modification operations, not during table creation.

packages/local-db/src/schema/index.ts (1)

1-3: LGTM!

Clean barrel export with alphabetically sorted exports, satisfying Biome linting requirements.

apps/desktop/src/renderer/components/OpenInButton/OpenInButton.tsx (1)

1-1: LGTM!

The import type syntax ensures only TypeScript types are imported without pulling in any Node.js runtime code from @superset/local-db. This is safe for the renderer process as per the coding guidelines.

apps/desktop/src/main/windows/main.ts (2)

2-3: LGTM!

Clean import additions following the local-db migration pattern. Separating schema imports (@superset/local-db) from the database instance (main/lib/local-db) is a good practice.

Also applies to: 8-8


92-106: LGTM!

Well-structured migration to the new local-db layer:

  • Two-step fetch pattern correctly handles the workspace → worktree relationship
  • Synchronous .get() calls are appropriate for better-sqlite3
  • Fallback chain (workspace?.name || worktree?.branch || "Workspace") provides sensible defaults
  • Error handling preserves resilience with try/catch and console logging
apps/desktop/src/lib/trpc/routers/terminal/terminal.ts (3)

3-11: LGTM! Imports are correctly organized for the local-db migration.

The imports properly bring in the necessary entities (projects, workspaces, worktrees) from @superset/local-db, the eq operator from drizzle-orm, and the localDb instance. This aligns with the Drizzle ORM pattern used throughout the PR.


53-70: LGTM! Workspace and project lookups correctly migrated to localDb.

The queries use the correct Drizzle ORM pattern with synchronous .get() for single-record lookups, which is appropriate for better-sqlite3. The conditional project lookup properly handles the case when workspace is undefined.


178-197: LGTM! Improved null-safety for worktree lookups.

The explicit guard for !workspace.worktreeId (lines 188-190) is a good addition that handles workspaces without associated worktrees (e.g., branch workspaces). This aligns with the schema where worktreeId is nullable.

apps/desktop/src/lib/trpc/routers/workspaces/utils/worktree.ts (2)

1-3: LGTM! Imports correctly updated for local-db migration.

The imports properly bring in SelectWorkspace type and table references from @superset/local-db, along with the eq operator and localDb instance.


8-43: LGTM! Clean implementation of workspace path resolution.

Both functions correctly implement the local-db migration:

  • getWorktreePath provides a simple worktree lookup by ID
  • getWorkspacePath properly handles both workspace types: returning mainRepoPath for branch workspaces and worktree.path for worktree workspaces

The consistent use of ?? null ensures a predictable return type.

packages/local-db/src/schema/relations.ts (1)

1-26: LGTM! Relations correctly define the entity relationships.

The relation definitions properly mirror the foreign key constraints in the schema:

  • projectsRelations: One project has many worktrees and workspaces
  • worktreesRelations: Each worktree belongs to one project and can have many workspaces
  • workspacesRelations: Each workspace belongs to one project and optionally one worktree

The field/reference mappings are accurate and follow Drizzle ORM conventions. Based on learnings, this correctly uses Drizzle ORM for schema management in the packages/ layer.

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

3-16: LGTM! Imports correctly organized for local-db migration.

The imports properly bring in table entities (projects, settings, workspaces, worktrees) and the SelectWorktree type from @superset/local-db, along with Drizzle ORM operators (and, desc, eq, isNotNull).


121-171: LGTM! Worktree and workspace creation correctly uses localDb.

The insert operations properly use:

  • .returning().get() to retrieve the inserted record with generated ID
  • onConflictDoUpdate for settings upsert (appropriate for the singleton settings row)
  • Correct field mappings matching the schema

528-540: The type annotation correctly reflects Drizzle's behavior: nullable columns return string | null, not string | undefined. However, this is not a breaking change in practice. All consumer code in the renderer uses truthiness checks (if (workspace.worktreeId)) or ternary operators, which safely handle both null and undefined. No code attempts to distinguish between them with strict equality checks like === undefined. The existing usage patterns are compatible with the nullable type.

apps/desktop/src/lib/trpc/routers/settings/index.ts (2)

9-18: LGTM!

The getSettings() helper ensures a settings row exists before reads, preventing null-pointer issues across all consumers. Good pattern for a singleton settings row.


120-147: Good self-healing pattern for stale ringtone IDs.

The fallback logic that detects invalid stored IDs, logs a warning, and persists the default is a solid defensive approach that prevents UI issues from orphaned configuration.

apps/desktop/src/lib/trpc/routers/projects/projects.ts (3)

78-135: LGTM!

The extractRepoName function handles HTTP/HTTPS URLs, SSH-style URLs, query strings, .git suffixes, and percent-encoding robustly. The SAFE_REPO_NAME_REGEX validation prevents path traversal attacks.


151-157: LGTM!

Clean migration to Drizzle ORM. The desc(projects.lastOpenedAt) ordering is correctly applied at the SQL level.


400-428: LGTM!

Good stale-entry handling: verifies filesystem presence, updates lastOpenedAt on hit, and cleans up orphaned DB records when the directory no longer exists before proceeding with a fresh clone.

Comment on lines +298 to +310
// Shift existing workspaces to make room at front
const projectWorkspaces = localDb
.select()
.from(workspaces)
.where(eq(workspaces.projectId, input.projectId))
.all();
for (const ws of projectWorkspaces) {
localDb
.update(workspaces)
.set({ tabOrder: ws.tabOrder + 1 })
.where(eq(workspaces.id, ws.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

Wrap tab reordering in a transaction for atomicity.

The loop at lines 304-310 executes N separate UPDATE statements to shift workspace tab orders. If the app crashes mid-loop, the database will have inconsistent tabOrder values, causing incorrect UI ordering.

The same issue exists in the reorder mutation (lines 1016-1022).

🔎 Wrap in transaction

You'll need access to the underlying sqlite instance. Consider exporting a transaction helper from main/lib/local-db:

// In local-db/index.ts
export function runInTransaction<T>(fn: () => T): T {
  return sqlite.transaction(fn)();
}

Then use it:

+import { runInTransaction } from "main/lib/local-db";
+
 // Shift existing workspaces to make room at front
 const projectWorkspaces = localDb
   .select()
   .from(workspaces)
   .where(eq(workspaces.projectId, input.projectId))
   .all();
-for (const ws of projectWorkspaces) {
-  localDb
-    .update(workspaces)
-    .set({ tabOrder: ws.tabOrder + 1 })
-    .where(eq(workspaces.id, ws.id))
-    .run();
-}
+runInTransaction(() => {
+  for (const ws of projectWorkspaces) {
+    localDb
+      .update(workspaces)
+      .set({ tabOrder: ws.tabOrder + 1 })
+      .where(eq(workspaces.id, ws.id))
+      .run();
+  }
+});

Committable suggestion skipped: line range outside the PR's diff.

Comment thread apps/desktop/src/main/lib/local-db/index.ts
Replace crypto.randomUUID() with uuid v4 to avoid node:crypto module
resolution issues in CI environments.

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@saddlepaddle saddlepaddle merged commit 33dbe99 into main Dec 23, 2025
12 checks passed
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