Skip to content

feat(desktop): add Linux build pipeline and stabilize dev auth callba…#1477

Merged
AviPeltz merged 4 commits into
mainfrom
feat/desktop-linux-build-and-auth-hardening
Feb 14, 2026
Merged

feat(desktop): add Linux build pipeline and stabilize dev auth callba…#1477
AviPeltz merged 4 commits into
mainfrom
feat/desktop-linux-build-and-auth-hardening

Conversation

@AviPeltz
Copy link
Copy Markdown
Collaborator

@AviPeltz AviPeltz commented Feb 14, 2026

…ck flow

Enable Linux AppImage build/release artifacts and Linux updater support, and make desktop OAuth callback reliable in Linux development by using a localhost callback fallback with matching CORS/trusted-origin updates.

Description

  • add Linux desktop build/release support for AppImage artifacts and Linux updater manifests in CI/release workflows
  • enable Linux updater runtime path in desktop app and document Linux packaging/testing steps
  • harden desktop OAuth in development by introducing a localhost callback fallback and aligning API/auth CORS trusted origins for desktop dev renderer
  • polish Linux window controls interaction and styling in the desktop top bar

Test plan

  • psql pooled and unpooled connectivity verified after env fix
  • desktop OAuth callback verified via localhost fallback path in Linux dev flow
  • CORS issue for http://localhost:5173 -> /api/auth/get-session resolved after allowlist updates
  • run end-to-end CI workflow validation for Linux AppImage artifact + *-linux.yml manifest upload
  • run canary release job once to validate Linux release asset naming (AppImage, canary-linux.yml, latest-linux.yml)
    EOF
    )"

Related Issues

Type of Change

  • Bug fix
  • New feature
  • Documentation
  • Refactor
  • Other (please describe):

Testing

Screenshots (if applicable)

Additional Notes

Summary by CodeRabbit

  • New Features

    • Linux desktop AppImage builds and Linux auto-update support
    • Desktop automation tooling: remote automation, DOM inspection, screenshots, click/type/navigation, and console-log access
  • Improvements

    • Optional local callback in desktop auth flow and clearer post-sign-in redirects
    • Auto-update messaging/checks now include Linux
    • Refined window control styling and workspace-aware dock/window titles (dev)
    • Local-dev CORS/origin support
  • Documentation

    • Updated build/release docs with Linux packaging and verification steps
  • Chores

    • CI/CD and release script enhancements for cross-platform builds

…ck flow

Enable Linux AppImage build/release artifacts and Linux updater support, and make desktop OAuth callback reliable in Linux development by using a localhost callback fallback with matching CORS/trusted-origin updates.

Co-authored-by: Cursor <cursoragent@cursor.com>
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Feb 14, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

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

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds Linux desktop build/release support and artifact stabilization; introduces a desktop automation MCP package with CDP/puppeteer tools; expands desktop OAuth local-callback plumbing and CORS dev origins; adds workspace branding (dock icon/title), port mapping and static-ports subscription, migrations, and various desktop packaging, docs, and UI tweaks.

Changes

Cohort / File(s) Summary
CI / Release Workflows
\.github/workflows/build-desktop.yml, \.github/workflows/release-desktop.yml, \.github/workflows/release-desktop-canary.yml
Add Linux build-linux job; create stable/canary AppImage copies and stable linux manifests; list artifacts for releases/canary.
Release tooling & scripts
apps/desktop/create-release.sh, apps/desktop/BUILDING.md, apps/desktop/RELEASE.md
Switch workflow lookup to tag SHA, surface AppImage URLs, update local build/release docs and bun command usage.
Electron packaging
apps/desktop/electron-builder.ts, apps/desktop/electron-builder.canary.ts
Conditional platform/canary icon paths using existsSync; restrict Linux target to AppImage; apply conditional icon assignment per platform.
Auto-update & updater manifests
apps/desktop/src/main/lib/auto-updater.ts
Introduce IS_AUTO_UPDATE_PLATFORM (macOS
Desktop OAuth & Notifications
apps/api/src/app/api/auth/desktop/connect/route.ts, apps/desktop/src/main/lib/notifications/server.ts, apps/web/src/app/auth/desktop/success/**, apps/desktop/src/lib/trpc/routers/auth/index.ts
Propagate and validate desktop_local_callback through connect flow; add /auth/callback handler in notifications server to accept token/expiresAt/state, call handleAuthCallback, and focus/restore window; web success page and redirect accept optional local callback; desktop client appends local callback param when opening browser.
CORS / Trusted origins
apps/api/src/proxy.ts, packages/auth/src/server.ts
Add DESKTOP_VITE_PORT-derived localhost/127.0.0.1 dev origins to allowed/trusted origins when NODE_ENV is development.
Desktop automation (new package)
packages/desktop-mcp/* (package.json, src/{mcp,tools,connection,console-capture,focus-lock,dom-inspector,zod.ts,bin.ts,tsconfig})
New @superset/desktop-mcp package: MCP server, ConnectionManager (CDP), ConsoleCapture, FocusLock, DOM inspector, multiple automation tools (click, navigate, type-text, send-keys, screenshots, get-window-info, get-console-logs, evaluate-js, inspect-dom), zod schemas, and CLI entry.
Desktop app integrations
apps/desktop/src/lib/electron-app/factories/app/setup.ts, apps/desktop/src/shared/env.shared.ts, apps/desktop/src/main/lib/dock-icon.ts, apps/desktop/src/main/index.ts, apps/desktop/src/main/windows/main.ts
Dev-only CDP flags (DESKTOP_AUTOMATION_PORT), env schema entry, workspace-determined dock icon rendering, set workspace name in dev, and workspace-aware window title in dev.
Static ports / ports subscription
\.superset/setup.sh, .superset/teardown.sh, .mcp.json, apps/desktop/src/lib/trpc/routers/ports/ports.ts, apps/desktop/src/renderer/.../usePortsData.ts
Generate .superset/ports.json; add teardown step to kill terminal daemons; add .mcp.json server entry; add subscribeAllStatic TRPC subscription and switch client to global subscription.
Workspaces & branch sync
apps/desktop/src/lib/trpc/routers/workspaces/procedures/status.ts, apps/desktop/src/lib/trpc/routers/changes/status.ts, apps/desktop/src/renderer/screens/main/hooks/useBranchSyncInvalidation/**
Add syncBranch mutation to update workspace/worktree branch; remove prior auto-sync call from changes/status; implement targeted branch-sync mutation with in-flight dedupe and localized cache updates.
Local DB / migrations
apps/desktop/src/main/lib/local-db/index.ts, packages/db/drizzle/0018_sandbox_and_slack_users.sql, packages/db/drizzle/meta/0018_snapshot.json, packages/db/drizzle/0015_slack_integration.sql, packages/db/drizzle/meta/_journal.json, packages/local-db/drizzle/meta/0025_snapshot.json, packages/local-db/drizzle/meta/_journal.json
Add users__slack_users table and snapshot updates; remove one migration line (0015); consolidate journal entries; simplify migration error handling (log and continue) in local DB startup.
Dev tooling & packaging changes
apps/desktop/scripts/patch-dev-protocol.ts, apps/desktop/package.json
Enhanced patch-dev-protocol to validate/rename Electron.app, update Info.plist URL types and path.txt; bump desktop package version and add dependency on @superset/desktop-mcp.
Renderer UI & tests
apps/desktop/src/renderer/.../WindowControls/WindowControls.tsx, apps/desktop/src/renderer/react-query/{projects,useReorderWorkspaces}.ts, apps/desktop/src/main/terminal-host/session-lifecycle.test.ts
Window control styling/aria changes; reorder invalidations extended to previous/next workspace queries; mark two PTY tests as skipped due to CI flakiness.
Misc / small edits
apps/streams/src/routes/chunks.ts, .mcp.json
zod import style changed to namespace import; added desktop-automation entry to .mcp.json.

Sequence Diagram(s)

sequenceDiagram
    participant DA as Desktop App
    participant API as API Server
    participant WEB as Web App
    participant NS as Notifications Server
    participant BW as Browser Window

    DA->>API: GET /auth/desktop/connect?local_callback=<url>
    API->>API: validate local_callback (scheme=http, host=localhost|127.0.0.1, path=/auth/callback)
    API-->>DA: redirect to Web success URL with desktop_local_callback param
    WEB->>WEB: parse desktop_local_callback → localCallbackUrl
    WEB->>NS: GET localCallbackUrl (browser hits local notifications server)
    NS->>NS: handle /auth/callback (validate token/expiresAt/state, call handleAuthCallback)
    NS->>BW: focus/restore main window
    NS-->>WEB: 200 OK (success page)
    WEB->>DA: navigate to localCallbackUrl (or fallback deep-link if failure)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • CharlieHelps

Poem

🐇 I hopped from mac to Linux, packing each AppImage tight,
I added tiny callbacks so localhost sleeps right,
I whispered ports and icons, and taught the MCP to click,
Releases hum, tests nap, and docs now do the trick,
A rabbit cheers: build, connect, and launch—what a night! 🎉

🚥 Pre-merge checks | ✅ 2 | ❌ 2
❌ Failed checks (2 warnings)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 25.64% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Merge Conflict Detection ⚠️ Warning ❌ Merge conflicts detected (25 files):

⚔️ .github/workflows/build-desktop.yml (content)
⚔️ .github/workflows/release-desktop-canary.yml (content)
⚔️ .github/workflows/release-desktop.yml (content)
⚔️ .superset/setup.sh (content)
⚔️ apps/api/src/app/api/auth/desktop/connect/route.ts (content)
⚔️ apps/api/src/proxy.ts (content)
⚔️ apps/desktop/BUILDING.md (content)
⚔️ apps/desktop/RELEASE.md (content)
⚔️ apps/desktop/create-release.sh (content)
⚔️ apps/desktop/electron-builder.canary.ts (content)
⚔️ apps/desktop/electron-builder.ts (content)
⚔️ apps/desktop/package.json (content)
⚔️ apps/desktop/src/lib/trpc/routers/ai-chat/utils/session-manager/agent-execution.ts (content)
⚔️ apps/desktop/src/lib/trpc/routers/auth/index.ts (content)
⚔️ apps/desktop/src/main/lib/auto-updater.ts (content)
⚔️ apps/desktop/src/main/lib/notifications/server.ts (content)
⚔️ apps/desktop/src/main/terminal-host/session-lifecycle.test.ts (content)
⚔️ apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/components/WindowControls/WindowControls.tsx (content)
⚔️ apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/PortsList/hooks/usePortsData.ts (content)
⚔️ apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/PortsList/utils/merge-ports.ts (content)
⚔️ apps/streams/src/routes/chunks.ts (content)
⚔️ apps/web/src/app/auth/desktop/success/components/DesktopRedirect/DesktopRedirect.tsx (content)
⚔️ apps/web/src/app/auth/desktop/success/page.tsx (content)
⚔️ bun.lock (content)
⚔️ packages/auth/src/server.ts (content)

These conflicts must be resolved before merging into main.
Resolve conflicts locally and push changes to this branch.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main changes: adding Linux build pipeline for desktop (AppImage/manifests) and stabilizing OAuth callback flow in development.
Description check ✅ Passed The PR description adequately covers objectives, includes testing evidence, and is mostly aligned with the template structure despite some formatting inconsistencies in the Description section.

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

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/desktop-linux-build-and-auth-hardening
⚔️ Resolve merge conflicts (beta)
  • Auto-commit resolved conflicts to branch feat/desktop-linux-build-and-auth-hardening
  • Create stacked PR with resolved conflicts
  • Post resolved changes as copyable diffs in a comment

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.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Feb 14, 2026

🧹 Preview Cleanup Complete

The following preview resources have been cleaned up:

  • ✅ Neon database branch
  • ⚠️ Electric Fly.io app
  • ⚠️ Streams Fly.io app

Thank you for your contribution! 🎉

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@apps/web/src/app/auth/desktop/success/page.tsx`:
- Around line 83-85: localCallbackUrl is built by concatenating strings which
breaks when localCallbackBase already contains query params; instead, construct
the URL with the URL API: if localCallbackBase is defined, create const url =
new URL(localCallbackBase) (or new URL(localCallbackBase,
window.location.origin) if needed), then use url.searchParams.set('token',
token), url.searchParams.set('expiresAt', expiresAt.toISOString()), and
url.searchParams.set('state', state), and finally set localCallbackUrl =
url.toString(); keep the undefined fallback when localCallbackBase is falsy.
This updates the logic around the localCallbackBase/localCallbackUrl variables
to robustly handle existing query parameters.
🧹 Nitpick comments (6)
apps/desktop/src/lib/trpc/routers/auth/index.ts (1)

91-94: The local_callback is sent unconditionally — is this intentional for production?

This appends the localhost callback URL to every OAuth flow, including production builds. If the intent is a fallback only for Linux/dev environments where custom URI handlers are unreliable, consider gating this behind a condition (e.g., platform check or dev-mode flag) to avoid leaking internal infrastructure details in the query string for macOS/Windows users where deep links work reliably.

Also, the /auth/callback path is duplicated here and in notifications/server.ts — consider extracting it to a shared constant.

apps/desktop/src/main/lib/notifications/server.ts (2)

181-211: Add prefixed console logging for the auth callback route.

This is a significant operation (OAuth callback completion) with no logging. Per coding guidelines, entry/exit of significant operations and error conditions should have [domain/operation] message logging. This will be valuable for debugging auth issues in Linux/dev environments.

♻️ Suggested logging additions
 app.get("/auth/callback", async (req, res) => {
 	const token = req.query.token;
 	const expiresAt = req.query.expiresAt;
 	const state = req.query.state;
 
 	if (
 		typeof token !== "string" ||
 		typeof expiresAt !== "string" ||
 		typeof state !== "string"
 	) {
+		console.warn("[auth/callback] Missing or invalid auth params");
 		return res.status(400).json({ success: false, error: "Missing auth params" });
 	}
 
 	const result = await handleAuthCallback({ token, expiresAt, state });
 	if (!result.success) {
+		console.warn("[auth/callback] Auth callback failed:", result.error);
 		return res.status(400).json(result);
 	}
 
+	console.log("[auth/callback] Auth callback succeeded, focusing main window");
 	const mainWindow = BrowserWindow.getAllWindows()[0];

As per coding guidelines: "Use prefixed console logging with consistent context pattern: [domain/operation] message for entry/exit of significant operations, external API calls, and error conditions."


196-198: Unhandled rejection if handleAuthCallback throws unexpectedly.

While Express 5 propagates async errors, handleAuthCallback could theoretically throw (e.g., if saveToken fails with a filesystem error). The error would surface as a generic 500 with no context. Consider wrapping in try/catch for a more informative error response.

🛡️ Defensive error handling
-	const result = await handleAuthCallback({ token, expiresAt, state });
-	if (!result.success) {
-		return res.status(400).json(result);
-	}
+	let result: { success: boolean; error?: string };
+	try {
+		result = await handleAuthCallback({ token, expiresAt, state });
+	} catch (err) {
+		console.error("[auth/callback] Unexpected error:", err);
+		return res.status(500).json({ success: false, error: "Internal auth error" });
+	}
+	if (!result.success) {
+		return res.status(400).json(result);
+	}
apps/web/src/app/auth/desktop/success/components/DesktopRedirect/DesktopRedirect.tsx (1)

19-36: Extract retry constants to named values.

The retry count (6) and delay (250) are magic numbers. Extracting them improves readability and makes tuning easier.

♻️ Suggested refactor
+"use client";
+
+import Image from "next/image";
+import Link from "next/link";
+import { useEffect, useState } from "react";
+
+const LOCAL_CALLBACK_MAX_ATTEMPTS = 6;
+const LOCAL_CALLBACK_RETRY_DELAY_MS = 250;
+
 export function DesktopRedirect({

Then reference the constants in the loop:

-			for (let attempt = 0; attempt < 6; attempt++) {
+			for (let attempt = 0; attempt < LOCAL_CALLBACK_MAX_ATTEMPTS; attempt++) {
 				...
-				await new Promise((resolve) => window.setTimeout(resolve, 250));
+				await new Promise((resolve) => window.setTimeout(resolve, LOCAL_CALLBACK_RETRY_DELAY_MS));

As per coding guidelines, "Extract hardcoded magic numbers, strings, and enums to named constants at module top instead of leaving them inline in logic".

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

5-18: Dev-only CORS origins are properly gated — consider extracting the shared definition.

The development guard is correct and the dual localhost/127.0.0.1 coverage is appropriate. However, desktopDevPort and desktopDevOrigins are duplicated in packages/auth/src/server.ts (per the summary). If the port or allowed hostnames ever change, both files must be updated in lockstep.

Consider extracting the shared origin list to a small utility in packages/constants or similar, so both consumers reference the same source of truth.

.github/workflows/build-desktop.yml (1)

144-237: Consider extracting shared steps to a composite action to reduce duplication.

The steps for checkout, Bun setup, caching, dependency install, version suffix, clean, and compile are identical between build-macos and build-linux. If a third platform is added or these steps change, both jobs must be updated. A composite action could reduce this surface. This is a common trade-off in GHA and acceptable to defer.

Comment thread apps/web/src/app/auth/desktop/success/page.tsx
Add release-script checks for jq and robust workflow run lookup by tag SHA, print Linux AppImage download URL, and include lint formatting cleanups in updated desktop auth callback files.

Co-authored-by: Cursor <cursoragent@cursor.com>
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@apps/desktop/src/main/lib/notifications/server.ts`:
- Around line 183-213: The async Express route handler for
app.get("/auth/callback") currently awaits handleAuthCallback without error
handling; wrap the entire handler body in a try/catch, call await
handleAuthCallback(...) inside the try, and in the catch log the error with
contextual info (including the state/token/expiresAt if safe) and return a 500
JSON response (e.g., { success: false, error: "Internal server error" }); ensure
you still perform the BrowserWindow restore/show/focus only on success, and do
not let rejections from handleAuthCallback or underlying functions like
saveToken bubble unhandled.
🧹 Nitpick comments (3)
apps/desktop/create-release.sh (1)

182-185: Install hint is Linux-specific.

The error message only suggests sudo apt install jq, but macOS developers (likely the majority for a desktop app) would need brew install jq. Consider a more generic hint.

Suggested improvement
-    error "jq is required but not installed.\nInstall it with your package manager (e.g. sudo apt install jq)"
+    error "jq is required but not installed.\nInstall it with your package manager (e.g. brew install jq or sudo apt install jq)"
apps/desktop/src/lib/trpc/routers/auth/index.ts (1)

91-94: Extract the callback path to a shared constant to avoid duplication.

The path /auth/callback is hardcoded here and also in apps/desktop/src/main/lib/notifications/server.ts (Line 183). Consider extracting it to a constant in shared/constants so both usages stay in sync.

apps/web/src/app/auth/desktop/success/components/DesktopRedirect/DesktopRedirect.tsx (1)

18-56: Consider aborting in-flight fetch requests on cleanup.

The isCancelled flag prevents state updates after unmount, but any in-flight fetch call will still complete. Using an AbortController would properly cancel pending network requests.

♻️ Suggested improvement
 	useEffect(() => {
 		let isCancelled = false;
+		const controller = new AbortController();
 
 		const runRedirect = async () => {
 			if (localCallbackUrl) {
 				for (let attempt = 0; attempt < 6; attempt++) {
 					try {
 						const response = await fetch(localCallbackUrl, {
 							method: "GET",
 							credentials: "omit",
+							signal: controller.signal,
 						});
 						if (response.ok && !isCancelled) {
 							setStatus("connected");
 							return;
 						}
 					} catch {
 						// Retry shortly in case desktop server is still initializing.
 					}
 					await new Promise((resolve) => window.setTimeout(resolve, 250));
 					if (isCancelled) return;
 				}
 // ...
 		return () => {
 			isCancelled = true;
+			controller.abort();
 		};
 	}, [url, localCallbackUrl]);

Comment thread apps/desktop/src/main/lib/notifications/server.ts
Switch to a namespace zod import so the chunks route schema initializes reliably under the Vitest SSR runtime, unblocking the streams test suite in CI.

Co-authored-by: Cursor <cursoragent@cursor.com>
Replace the fetch()-based localhost callback with a full browser redirect.
Browsers block mixed-content fetch (HTTPS->HTTP) but allow full-page
navigations, fixing auth on Linux production builds where the success
page is served from https://app.superset.sh.

Co-authored-by: Cursor <cursoragent@cursor.com>
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 12

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/desktop/scripts/patch-dev-protocol.ts (1)

130-167: ⚠️ Potential issue | 🟠 Major

Unsanitized DISPLAY_NAME interpolated into shell commands — breaks on special characters.

DISPLAY_NAME (and transitively workspaceName) is injected directly into PlistBuddy command strings without escaping. A workspace name containing a single quote, double quote, backtick, or $ will break these execSync calls or produce corrupt plist values. For example, a worktree directory named feature/it's-done would produce Superset (it's-done) inside single quotes, breaking the shell parse.

Consider sanitizing or constraining workspaceName early in the script (e.g., reject or strip non-alphanumeric/dash/underscore characters), or switch to an array-based execFileSync invocation that avoids shell interpretation entirely.

Proposed fix — use execFileSync to avoid shell
+import { execFileSync } from "node:child_process";
+
+function plistBuddy(...cmds: string[]): string {
+	const args: string[] = [];
+	for (const cmd of cmds) {
+		args.push("-c", cmd);
+	}
+	args.push(PLIST_PATH);
+	return execFileSync("/usr/libexec/PlistBuddy", args, {
+		encoding: "utf-8",
+	}).trim();
+}

Then replace patterns like:

-execSync(
-	`/usr/libexec/PlistBuddy -c "Set :CFBundleIdentifier ${BUNDLE_ID}" "${PLIST_PATH}"`,
-);
+plistBuddy(`Set :CFBundleIdentifier ${BUNDLE_ID}`);

This avoids shell interpretation of the values entirely.

🤖 Fix all issues with AI agents
In `@apps/desktop/scripts/patch-dev-protocol.ts`:
- Around line 178-211: The unlink/rename/symlink sequence around
ELECTRON_APP_PATH can leave Electron.app broken if unlinkSync(ELECTRON_APP_PATH)
succeeds but a later renameSync or symlinkSync throws; wrap the inner branch
that does unlinkSync, renameSync, and symlinkSync (the block handling
stats.isSymbolicLink() and the else real-directory branch) in its own try/catch
and on failure restore the original symlink target (using symlinkSync with the
prior currentTarget or recreate a symlink to the oldTargetPath) and/or log a
clear recovery instruction; update references to ELECTRON_APP_PATH,
DESIRED_APP_NAME, currentTarget, oldTargetPath, renameSync and symlinkSync in
that block so the code attempts to recover the original state before rethrowing
or continuing.

In `@apps/desktop/src/lib/electron-app/factories/app/setup.ts`:
- Around line 85-91: Replace the raw process.env access and hardcoded fallback
with the Zod-validated value from the shared env module: use
env.DESKTOP_AUTOMATION_PORT (or the already-imported env object) when computing
cdpPort inside the development block in setup.ts instead of
String(process.env.DESKTOP_AUTOMATION_PORT || 9223); also extract the numeric
default (9223) to a named constant at the top of the module (e.g.,
DEFAULT_DESKTOP_AUTOMATION_PORT) and use that only if
env.DESKTOP_AUTOMATION_PORT is undefined, then pass the computed cdpPort to
app.commandLine.appendSwitch as before.

In `@apps/desktop/src/main/lib/dock-icon.ts`:
- Around line 61-74: The third branch in getIconPath uses __dirname which can be
unreliable with electron-vite bundling in non-packaged, non-development runs;
add a concise comment above that return explaining this branch only applies to
uncommon test/preview builds and why __dirname is used (and that electron-vite
may change its value), and optionally replace or guard the fallback with a more
robust resolution (e.g., prefer app.getAppPath() or check process.resourcesPath
first) so the path resolves consistently under electron-vite; reference
getIconPath and the __dirname return when making the change.
- Around line 107-129: The findContentBounds function can return inverted bounds
when no pixel exceeds the alpha threshold; update findContentBounds to detect
that case (e.g., if top > bottom or left > right after the loops) and return a
safe empty rectangle (top=0,left=0,bottom=0,right=0 or other sentinel) so
downstream callers like drawBorder and setWorkspaceDockIcon never receive
inverted coordinates; reference findContentBounds and ensure callers (including
setWorkspaceDockIcon/icon.isEmpty logic) handle the normalized empty bounds
consistently.

In `@apps/desktop/src/main/lib/local-db/index.ts`:
- Around line 90-96: The try/catch around migrate(localDb, { migrationsFolder })
currently swallows all errors and always logs "[local-db] Migrations complete";
change it to only suppress known idempotent SQLite migration errors (e.g., “no
such table/column” or other benign codes your migration lib documents) while
logging full error context via processLogger or console.error, and rethrow any
unknown/unexpected errors so failures don't allow the app to continue with an
incomplete schema; also move the success log ("[local-db] Migrations complete")
inside the successful path (after migrate returns) so it is only emitted on
success. Ensure you reference the migrate call and the catch block around it
when implementing this behavior.

In `@packages/db/drizzle/0018_sandbox_and_slack_users.sql`:
- Around line 65-69: The migration drops the "repositories" table with DROP
TABLE "repositories" CASCADE which already removes referencing foreign keys,
then tries to drop the same constraint tasks_repository_id_repositories_id_fk
and index tasks_repository_id_idx causing a "constraint does not exist" error;
fix by either removing the explicit DROP CONSTRAINT
"tasks_repository_id_repositories_id_fk" and DROP INDEX
"tasks_repository_id_idx" lines, or reorder the statements so that ALTER TABLE
"tasks" DROP CONSTRAINT "tasks_repository_id_repositories_id_fk" and DROP INDEX
"tasks_repository_id_idx" occur before DROP TABLE "repositories" CASCADE.

In `@packages/desktop-mcp/src/mcp/connection/connection-manager.ts`:
- Around line 21-27: getPage() can start multiple simultaneous connect() calls
when this.page is unset; add an in-flight promise guard (e.g. a private member
like connectingPromise) to serialize connection creation: before calling
connect() check if connectingPromise exists and await it, otherwise set
connectingPromise = connect(), await it, then clear connectingPromise; ensure
getPage() still calls focusLock.inject(this.page) when the page is available and
that connect() resolves to and assigns this.page/browser so subsequent calls see
the established connection.

In `@packages/desktop-mcp/src/mcp/dom-inspector/dom-inspector.ts`:
- Around line 45-46: The code constructs cssSelector using testId without
escaping which can produce malformed selectors; update the branch that sets
cssSelector (the else if handling testId) to pass testId through
CSS.escape()—mirroring the earlier use for node.id—so cssSelector becomes an
attribute selector built from the escaped testId value.

In `@packages/desktop-mcp/src/mcp/focus-lock/focus-lock.ts`:
- Around line 57-65: The load event handler in attach(page) uses an async
callback and calls this.inject(page) without guarding against rejection; wrap
the await this.inject(page) call in a try/catch inside the page.on("load", ...)
listener, and on error call the module logger (or processLogger) with contextual
information including the page and that inject failed (do not swallow the error
silently) then either rethrow or handle per local error policy; ensure you
reference the attach method and the this.inject(page) invocation when making the
change.

In `@packages/desktop-mcp/src/mcp/tools/click/click.ts`:
- Around line 14-16: The selector-building for testId in click.ts is vulnerable
because testId is interpolated directly into '[data-testid="' + testId + '"]';
update the code that assigns el (the document.querySelectorAll call) to use a
sanitized selector by applying CSS.escape to testId before interpolation (i.e.,
use CSS.escape(testId) when constructing the '[data-testid="..."]' selector) so
querySelectorAll('[data-testid="' + CSS.escape(testId) + '"]')[index] cannot
break or match unintended elements.

In `@packages/desktop-mcp/src/mcp/tools/evaluate-js/evaluate-js.ts`:
- Around line 22-25: The text field can become undefined because
JSON.stringify(undefined) returns undefined; update the logic in evaluate-js.ts
around the result handling (the variable used to set text in the returned
content) to explicitly handle undefined before calling JSON.stringify — e.g. if
result === undefined set text to the string "undefined" (or use String(result)),
otherwise if typeof result === "string" use result, else use
JSON.stringify(result, null, 2) and if that returns undefined fall back to
String(result); adjust the code that builds the content/text to follow this
order so text is always a string.

In `@packages/desktop-mcp/src/mcp/tools/type-text/type-text.ts`:
- Around line 29-33: The clear-first logic in the type routine uses a hardcoded
Meta modifier (page.keyboard.down("Meta") / press("a") / up("Meta")) which only
works on macOS; update the args.clearFirst branch in the type-text logic to
detect the OS (e.g., check process.platform === "darwin") and choose the
modifier variable accordingly (use "Meta" for macOS, "Control" for
Linux/others), then use that modifier when calling page.keyboard.down(mod),
page.keyboard.press("a"), and page.keyboard.up(mod) so Select All works
cross-platform.
🧹 Nitpick comments (29)
apps/desktop/src/main/terminal-host/session-lifecycle.test.ts (1)

367-368: All tests in this file are now skipped — consider tracking re-enablement.

With these two being the last active tests, every test case in this suite is now it.skip. This is reasonable for unblocking Linux CI, but without a tracking issue the skips tend to become permanent. Consider adding a TODO(issue-url) linking to an issue for fixing the bun/node-pty ABI mismatch so these can be re-enabled.

Also applies to: 404-405

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

190-216: Subscription captures workspace list only at subscribe-time; late additions won't be watched.

allWorkspaces is queried once when the subscription starts (line 192). Any workspace created after the subscription is established won't be watched, so its static-port file changes will be silently missed until the client re-subscribes.

If this is acceptable for your use-case (e.g., the UI re-mounts or the user navigates), no action needed. Otherwise, consider listening to a workspace-creation event and dynamically adding watches.


203-205: onChange emits for any workspace change, not just the ones this subscription watches.

The staticPortsWatcher is a shared singleton. If another subscribeStatic call watches an additional workspace, this onChange handler will also fire for that workspace since there's no filter against watchedIds. The consumer currently just invalidates the full getAllStatic cache, so this is benign, but it's worth noting for future callers that expect scoped events.

.superset/teardown.sh (1)

120-128: ((killed++)) post-increment returns exit 1 on first invocation — harmless here but worth knowing.

When killed is 0, ((killed++)) evaluates to 0 (the old value), which bash treats as falsy (exit code 1). The || true saves you, but this is a classic bash gotcha that could trip someone up if the || true is ever removed. Using pre-increment (++killed) avoids the ambiguity since the result is always ≥ 1.

Suggested tweak
-        kill "$pid" 2>/dev/null && ((killed++)) || true
+        kill "$pid" 2>/dev/null && ((++killed)) || true
.superset/setup.sh (1)

189-202: Copying a live SQLite database with cp may produce an inconsistent snapshot.

If the source database is actively being written to (e.g., by another dev workspace), cp can capture a mid-transaction state where the main DB file and WAL are out of sync. Consider using sqlite3 "$source_db" ".backup '$dest_db'" for an atomic, consistent copy. WAL/SHM files won't need separate handling with .backup.

Suggested fix
-  if ! cp "$source_db" "$dest_db"; then
-    error "Failed to copy local database"
-    return 1
-  fi
-  chmod 600 "$dest_db"
-
-  # Copy WAL and SHM files if present for consistency
-  for suffix in "-wal" "-shm"; do
-    if [ -f "${source_db}${suffix}" ]; then
-      cp "${source_db}${suffix}" "${dest_db}${suffix}" 2>/dev/null || true
-      chmod 600 "${dest_db}${suffix}" 2>/dev/null || true
-    fi
-  done
+  if command -v sqlite3 &> /dev/null; then
+    if ! sqlite3 "$source_db" ".backup '$dest_db'"; then
+      error "Failed to clone local database via sqlite3 backup"
+      return 1
+    fi
+  else
+    warn "sqlite3 not found — falling back to file copy (may be inconsistent if DB is active)"
+    if ! cp "$source_db" "$dest_db"; then
+      error "Failed to copy local database"
+      return 1
+    fi
+    for suffix in "-wal" "-shm"; do
+      if [ -f "${source_db}${suffix}" ]; then
+        cp "${source_db}${suffix}" "${dest_db}${suffix}" 2>/dev/null || true
+        chmod 600 "${dest_db}${suffix}" 2>/dev/null || true
+      fi
+    done
+  fi
+  chmod 600 "$dest_db"
packages/desktop-mcp/src/mcp/tools/navigate/navigate.ts (1)

18-47: Unhandled navigation errors will surface as raw exceptions to the MCP client.

If page.goto() fails (timeout, net error, invalid URL), the raw Puppeteer error propagates. Wrapping in a try/catch would let you return a structured isError: true response consistent with the "no url/path" branch.

Also, the as string cast on line 22 is unnecessary — the truthy check already narrows string | undefined to string.

Suggested improvement
 		async (args) => {
-			const page = await getPage();
-
-			if (args.url) {
-				await page.goto(args.url as string);
-			} else if (args.path) {
-				await page.evaluate(
-					`window.location.hash = ${JSON.stringify(`#${args.path}`)}`,
-				);
-			} else {
+			if (!args.url && !args.path) {
 				return {
 					content: [
-						{
-							type: "text" as const,
-							text: "Must provide url or path",
-						},
+						{ type: "text" as const, text: "Must provide url or path" },
 					],
 					isError: true,
 				};
 			}
 
-			const currentUrl = page.url();
-			return {
-				content: [
-					{
-						type: "text" as const,
-						text: `Navigated to ${currentUrl}`,
-					},
-				],
-			};
+			try {
+				const page = await getPage();
+
+				if (args.url) {
+					await page.goto(args.url);
+				} else if (args.path) {
+					await page.evaluate(
+						`window.location.hash = ${JSON.stringify(`#${args.path}`)}`,
+					);
+				}
+
+				return {
+					content: [
+						{ type: "text" as const, text: `Navigated to ${page.url()}` },
+					],
+				};
+			} catch (err) {
+				return {
+					content: [
+						{
+							type: "text" as const,
+							text: `Navigation failed: ${err instanceof Error ? err.message : String(err)}`,
+						},
+					],
+					isError: true,
+				};
+			}
packages/desktop-mcp/src/mcp/console-capture/console-capture.ts (1)

40-47: getLogs returns a direct reference to the internal logs array when no filters are applied.

If level is undefined and limit is undefined, filtered is the same reference as this.logs. A caller doing result.push(...) or result.length = 0 would corrupt internal state. Consider returning a shallow copy.

Suggested fix
 	getLogs({
 		level,
 		limit,
 	}: {
 		level?: number;
 		limit?: number;
 	}): ConsoleLogEntry[] {
-		let filtered = this.logs;
+		let filtered: ConsoleLogEntry[] = this.logs;
 		if (level !== undefined) {
 			filtered = filtered.filter((log) => log.level === level);
 		}
 		if (limit !== undefined) {
 			filtered = filtered.slice(-limit);
 		}
+		// Return a copy if no filter/limit produced a new array
+		if (filtered === this.logs) {
+			filtered = [...filtered];
+		}
 		return filtered;
 	}
packages/desktop-mcp/src/mcp/focus-lock/focus-lock.ts (1)

85-89: Silent catch should log for debuggability.

Per coding guidelines, errors should not be swallowed silently. A debug-level log would help troubleshoot automation issues without adding noise.

Suggested fix
 		try {
 			await page.evaluate(UNLOCK_SCRIPT);
 		} catch {
-			// page may have navigated or been destroyed
+			console.debug("[FocusLock] unlock failed — page may have navigated or been destroyed");
 		}
packages/desktop-mcp/src/mcp/connection/connection-manager.ts (1)

29-58: connect() does not handle errors — a failed connection leaves no path to retry.

If puppeteer.connect() or page lookup throws, internal state stays null which is fine, but the error propagates unlogged. Per coding guidelines, errors should at minimum be logged with context before rethrowing. Also, the protocolTimeout of 60 s is an inline magic number.

Suggested improvement
+const PROTOCOL_TIMEOUT_MS = 60_000;
+
 private async connect(): Promise<Page> {
-	this.browser = await puppeteer.connect({
-		browserURL: `http://127.0.0.1:${CDP_PORT}`,
-		protocolTimeout: 60_000,
-		defaultViewport: null,
-	});
+	try {
+		this.browser = await puppeteer.connect({
+			browserURL: `http://127.0.0.1:${CDP_PORT}`,
+			protocolTimeout: PROTOCOL_TIMEOUT_MS,
+			defaultViewport: null,
+		});
+	} catch (err) {
+		console.error("[desktop-mcp/connect] CDP connection failed", err);
+		throw err;
+	}
packages/desktop-mcp/src/zod.ts (2)

47-50: NavigateRequestSchema allows an empty object — is that intentional?

Both url and path are optional, so {} passes validation. If the tool requires at least one, add a refinement:

export const NavigateRequestSchema = z.object({
  url: z.string().optional(),
  path: z.string().optional(),
}).refine(d => d.url || d.path, { message: "Either url or path is required" });

If an empty payload is valid (e.g., "return current URL"), ignore this.


110-116: ConsoleLogEntrySchema.level is z.number() while ConsoleLogsRequestSchema.level is a string enum.

The request filters by "log" | "warn" | "error" | "info" | "debug" but the response entry stores level as a number. This is likely correct (CDP uses numeric log levels internally), but consumers will need to map between the two representations. Consider documenting the mapping or using a consistent representation.

packages/desktop-mcp/src/bin.ts (1)

5-7: No error handling around startup — an unhandled rejection crashes silently.

If createMcpServer() or server.connect() throws, the process exits with an opaque unhandled-rejection trace. A minimal wrapper improves debuggability:

Suggested improvement
-const server = createMcpServer();
-const transport = new StdioServerTransport();
-await server.connect(transport);
+try {
+	const server = createMcpServer();
+	const transport = new StdioServerTransport();
+	await server.connect(transport);
+} catch (err) {
+	console.error("[desktop-mcp] Failed to start MCP server", err);
+	process.exit(1);
+}
packages/desktop-mcp/src/mcp/tools/evaluate-js/evaluate-js.ts (1)

14-39: Missing error handling for getPage() failure.

If the CDP connection is down, getPage() will throw, but it's called outside the try/catch block. This would propagate as an unhandled error to the MCP framework rather than returning a user-friendly error response.

Move `getPage()` inside the try block
 		async (args) => {
-			const page = await getPage();
 			try {
+				const page = await getPage();
 				const result = await page.evaluate(args.code as string);
packages/desktop-mcp/src/mcp/tools/take-screenshot/take-screenshot.ts (2)

11-21: Consider adding .positive() constraints on width and height.

Zero or negative values for width/height would cause puppeteer's screenshot() to throw a confusing error. Adding validation at the schema level gives a clearer error message.

Proposed schema tightening
 				rect: z
 					.object({
 						x: z.number().describe("X coordinate of capture region"),
 						y: z.number().describe("Y coordinate of capture region"),
-						width: z.number().describe("Width of capture region"),
-						height: z.number().describe("Height of capture region"),
+						width: z.number().positive().describe("Width of capture region"),
+						height: z.number().positive().describe("Height of capture region"),
 					})

24-47: No error handling — getPage() or page.screenshot() failures will propagate unhandled.

Other tools like evaluate_js wrap the handler in try/catch and return isError: true. Screenshots can fail for various reasons (page not ready, invalid clip region). Consider wrapping for consistency and a better agent experience.

packages/desktop-mcp/src/mcp/tools/inspect-dom/inspect-dom.ts (2)

36-64: Consider wrapping the handler in try/catch for robustness.

If the DOM inspector script throws (e.g., invalid selector passed by the agent), the error propagates unhandled. A try/catch returning isError: true would give the agent actionable feedback, consistent with how evaluate_js handles errors.


5-15: DomElement interface could be shared.

This interface represents the contract of DOM_INSPECTOR_SCRIPT's output. If it's also used or referenced elsewhere (e.g., in dom-inspector.ts), consider co-locating it with the script to keep the contract in one place.

packages/desktop-mcp/src/mcp/tools/click/click.ts (2)

77-87: Coordinate click succeeds even when only one of x/y is provided.

If a caller passes x without y (or vice versa), the coordinate branch is skipped and the tool falls through to the element-search path with all-null selectors, returning a confusing "Element not found" error instead of a clear validation message.

Proposed fix
 		// Click by coordinates
-		if (args.x !== undefined && args.y !== undefined) {
+		const hasX = args.x !== undefined;
+		const hasY = args.y !== undefined;
+		if (hasX !== hasY) {
+			return {
+				content: [
+					{ type: "text" as const, text: "Both x and y coordinates are required" },
+				],
+				isError: true,
+			};
+		}
+		if (hasX && hasY) {
 			await page.mouse.click(args.x as number, args.y as number);

90-96: Redundant ?? 0 / ?? true — Zod .default() guarantees these values.

index and fuzzy have .default() in the input schema, so they will never be undefined after parsing. The nullish coalescing is dead code.

packages/desktop-mcp/src/mcp/tools/send-keys/send-keys.ts (2)

72-75: Pressing a held-down modifier again may produce unexpected double key events.

When only modifiers are provided (e.g., ["Shift"]), the code does down("Shift") then press("Shift") (which is down + up) then up("Shift"). This results in two keydown and two keyup events for the same key, which could confuse some applications. Consider whether this branch is truly needed — if the intent is "just press the modifier once", skip the down/up wrapper and only call press.


77-79: modifiers.reverse() mutates the array in place.

Array.prototype.reverse() mutates the original array. This is safe here since modifiers isn't reused, but using [...modifiers].reverse() or modifiers.toReversed() would be clearer about intent and safer against future refactors.

apps/desktop/scripts/patch-dev-protocol.ts (3)

108-126: Empty catch {} blocks silently swallow errors.

Lines 112 and 126 have bare catch {} that hide failures without any logging. The outer catch (line 126) guards the "already patched?" check — a failure there means the script falls through to re-patch, which is acceptable behavior, but a debug log would help troubleshooting. The inner catch (line 112) on readFileSync similarly hides I/O errors.

At minimum, log a debug-level message so failures are diagnosable. As per coding guidelines: "Never swallow errors silently; at minimum log errors with context before rethrowing or handling them explicitly."

Proposed fix
 	try {
 		pathTxtCorrect =
 			readFileSync(pathTxtCheck, "utf-8").trim() ===
 			`${DISPLAY_NAME}.app/Contents/MacOS/Electron`;
-	} catch {}
+	} catch (err) {
+		console.log("[patch-dev-protocol] Could not read path.txt, will re-patch:", err);
+	}
 
 	if (
 		currentBundleId === BUNDLE_ID &&
@@ ...
 		process.exit(0);
 	}
-} catch {}
+} catch (err) {
+	console.log("[patch-dev-protocol] Could not read current plist state, will re-patch:", err);
+}

102-106: electronPkgDir path is computed twice.

electronPkgCheck (line 102) and electronPkgDir (line 230) resolve the same ../node_modules/electron path. Extract it as a top-level constant alongside ELECTRON_DIST_DIR to avoid duplication.

Proposed fix
+const ELECTRON_PKG_DIR = resolve(import.meta.dirname, "../node_modules/electron");
 const ELECTRON_DIST_DIR = resolve(
 	import.meta.dirname,
-	"../node_modules/electron/dist",
+	ELECTRON_PKG_DIR,
+	"dist",
 );

Then use ELECTRON_PKG_DIR in both the check (line 102) and the write (line 230).

Also applies to: 230-231


139-144: Another silent catch {}.

Same concern as the earlier empty catches — the Delete :CFBundleDisplayName failure is expected when the key doesn't exist, but logging at debug level would still help distinguish that case from unexpected errors.

apps/desktop/src/main/lib/dock-icon.ts (2)

226-233: Extract magic numbers to named constants.

Several tuning values are inlined. Per the coding guidelines, hardcoded magic numbers should be extracted to named constants at module top.

♻️ Suggested extraction
+// Dock icon border styling constants
+const BORDER_SATURATION = 75;
+const BORDER_LIGHTNESS = 55;
+const BORDER_THICKNESS_RATIO = 0.038;
+const CORNER_RADIUS_RATIO = 0.22;
+const CONTENT_ALPHA_THRESHOLD = 10;
+const BORDER_ALPHA_THRESHOLD = 0.001;
+
 /**
  * Sets the macOS dock icon with a colored border based on the workspace name.

Then reference them in the body:

-		const rgb = hslToRgb(hue, 75, 55);
+		const rgb = hslToRgb(hue, BORDER_SATURATION, BORDER_LIGHTNESS);

-		const thickness = Math.round(size.width * 0.038);
-		const cornerRadius = Math.round(size.width * 0.22);
+		const thickness = Math.round(size.width * BORDER_THICKNESS_RATIO);
+		const cornerRadius = Math.round(size.width * CORNER_RADIUS_RATIO);

And similarly for the thresholds at lines 119 and 184.

As per coding guidelines: "Extract hardcoded magic numbers, strings, and enums to named constants at module top instead of leaving them inline in logic."


80-102: sdfRoundedRect uses 7 positional parameters.

Per coding guidelines, functions with 2+ params should use object parameters. That said, this is a standard SDF math primitive where positional args follow established convention. Consider refactoring if this function is reused elsewhere; acceptable as-is for an internal utility.

As per coding guidelines: "Use object parameters for functions with 2 or more parameters instead of positional arguments."

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

162-162: Silent catch {} swallows errors without any logging.

This bare catch makes it difficult to diagnose file-read failures for untracked files. Consider logging with context at a debug/warn level so issues are at least observable.

Suggested fix
-		} catch {}
+		} catch (err) {
+			console.warn(`[changes/status] Failed to count lines for untracked file: ${file.path}`, err);
+		}

As per coding guidelines: "Never swallow errors silently; at minimum log errors with context before rethrowing or handling them explicitly."

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

140-176: Inconsistent error-handling pattern: syncBranch returns result objects while sibling procedures throw.

The other procedures in this router (reorder, update, setUnread, setActive) throw new Error(...) for invalid states (e.g., workspace not found), but syncBranch returns { success: false, reason: ... }. This forces callers to inspect return values rather than rely on tRPC's standard error propagation, and it means the caller must handle two different error shapes depending on which procedure is called.

Consider either aligning with the existing throw pattern (ideally using TRPCError with appropriate codes like NOT_FOUND / BAD_REQUEST) or documenting why this procedure intentionally diverges.

As per coding guidelines: "Use appropriate TRPCError codes consistently: NOT_FOUND (resource doesn't exist), BAD_REQUEST (invalid input)."


156-160: updatedAt not refreshed on branch change.

The direct localDb.update here doesn't set updatedAt. The local-db SQLite schema uses $defaultFn (insert-time only), not $onUpdate, so the timestamp won't auto-update. Other mutations in this file use touchWorkspace (line 91) to handle this. If updatedAt accuracy matters for cache freshness or UI ordering, consider including it:

 localDb
   .update(workspaces)
-  .set({ branch })
+  .set({ branch, updatedAt: Date.now() })
   .where(eq(workspaces.id, workspaceId))
   .run();

Comment thread apps/desktop/scripts/patch-dev-protocol.ts Outdated
Comment thread apps/desktop/src/lib/electron-app/factories/app/setup.ts Outdated
Comment thread apps/desktop/src/main/lib/dock-icon.ts Outdated
Comment thread apps/desktop/src/main/lib/dock-icon.ts Outdated
Comment on lines 90 to 96
try {
migrate(localDb, { migrationsFolder });
} catch (error) {
const sqliteError = error as Error & { code?: string };
const errorCode = sqliteError.code?.toLowerCase() ?? "";
const errorMessage = sqliteError.message?.toLowerCase() ?? "";

const isSqliteError = errorCode === "sqlite_error";
const isIdempotentMessage =
errorMessage.includes("duplicate column name") ||
errorMessage.includes("already exists") ||
errorMessage.includes("no such column");

if (isSqliteError && isIdempotentMessage) {
console.log(`[local-db] Skipped idempotent error: ${sqliteError.message}`);
} else {
throw error;
}
console.error("[local-db] Migration failed:", error);
}

console.log("[local-db] Migrations complete");
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

Swallowing all migration errors risks silent data corruption.

The previous code selectively handled known-idempotent SQLite errors; this change catches everything and continues. If a migration fails due to disk issues, schema corruption, or a genuine bug, the app proceeds with an incomplete schema — likely causing confusing downstream failures. Line 96 also misleadingly logs "Migrations complete" after a failure.

At minimum, re-throw unknown/unexpected errors and only swallow truly idempotent ones. As per coding guidelines: "Never swallow errors silently; at minimum log errors with context before rethrowing or handling them explicitly."

Proposed fix
 try {
 	migrate(localDb, { migrationsFolder });
 } catch (error) {
-	console.error("[local-db] Migration failed:", error);
+	const message = error instanceof Error ? error.message : String(error);
+	const isIdempotent =
+		message.includes("already exists") ||
+		message.includes("duplicate column name");
+	if (isIdempotent) {
+		console.warn("[local-db] Idempotent migration warning (safe to ignore):", message);
+	} else {
+		console.error("[local-db] Migration failed:", error);
+		throw error;
+	}
 }
 
 console.log("[local-db] Migrations complete");
📝 Committable suggestion

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

Suggested change
try {
migrate(localDb, { migrationsFolder });
} catch (error) {
const sqliteError = error as Error & { code?: string };
const errorCode = sqliteError.code?.toLowerCase() ?? "";
const errorMessage = sqliteError.message?.toLowerCase() ?? "";
const isSqliteError = errorCode === "sqlite_error";
const isIdempotentMessage =
errorMessage.includes("duplicate column name") ||
errorMessage.includes("already exists") ||
errorMessage.includes("no such column");
if (isSqliteError && isIdempotentMessage) {
console.log(`[local-db] Skipped idempotent error: ${sqliteError.message}`);
} else {
throw error;
}
console.error("[local-db] Migration failed:", error);
}
console.log("[local-db] Migrations complete");
try {
migrate(localDb, { migrationsFolder });
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
const isIdempotent =
message.includes("already exists") ||
message.includes("duplicate column name");
if (isIdempotent) {
console.warn("[local-db] Idempotent migration warning (safe to ignore):", message);
} else {
console.error("[local-db] Migration failed:", error);
throw error;
}
}
console.log("[local-db] Migrations complete");
🤖 Prompt for AI Agents
In `@apps/desktop/src/main/lib/local-db/index.ts` around lines 90 - 96, The
try/catch around migrate(localDb, { migrationsFolder }) currently swallows all
errors and always logs "[local-db] Migrations complete"; change it to only
suppress known idempotent SQLite migration errors (e.g., “no such table/column”
or other benign codes your migration lib documents) while logging full error
context via processLogger or console.error, and rethrow any unknown/unexpected
errors so failures don't allow the app to continue with an incomplete schema;
also move the success log ("[local-db] Migrations complete") inside the
successful path (after migrate returns) so it is only emitted on success. Ensure
you reference the migrate call and the catch block around it when implementing
this behavior.

Comment thread packages/desktop-mcp/src/mcp/dom-inspector/dom-inspector.ts Outdated
Comment thread packages/desktop-mcp/src/mcp/focus-lock/focus-lock.ts Outdated
Comment thread packages/desktop-mcp/src/mcp/tools/click/click.ts Outdated
Comment thread packages/desktop-mcp/src/mcp/tools/evaluate-js/evaluate-js.ts Outdated
Comment thread packages/desktop-mcp/src/mcp/tools/type-text/type-text.ts Outdated
@AviPeltz AviPeltz force-pushed the feat/desktop-linux-build-and-auth-hardening branch from 6e46435 to 397b900 Compare February 14, 2026 07:03
@AviPeltz AviPeltz merged commit 1d14439 into main Feb 14, 2026
28 of 32 checks passed
@Kitenite Kitenite deleted the feat/desktop-linux-build-and-auth-hardening branch February 16, 2026 00:04
This was referenced Feb 17, 2026
AviPeltz added a commit that referenced this pull request Feb 17, 2026
… Linux

PR #1477 unconditionally sent local_callback for all platforms, which
bypassed the reliable deep link flow on macOS. Only send the localhost
callback on Linux where deep links are unreliable, restoring the
original macOS auth behavior.
AviPeltz added a commit that referenced this pull request Feb 17, 2026
… Linux (#1550)

PR #1477 unconditionally sent local_callback for all platforms, which
bypassed the reliable deep link flow on macOS. Only send the localhost
callback on Linux where deep links are unreliable, restoring the
original macOS auth behavior.
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