Skip to content

feat(desktop): color dock icon and label per workspace#1478

Merged
saddlepaddle merged 2 commits into
mainfrom
color-electron
Feb 14, 2026
Merged

feat(desktop): color dock icon and label per workspace#1478
saddlepaddle merged 2 commits into
mainfrom
color-electron

Conversation

@saddlepaddle
Copy link
Copy Markdown
Collaborator

@saddlepaddle saddlepaddle commented Feb 14, 2026

Summary

  • Adds a colored rounded-rect border on the dock icon, with hue deterministically derived from the workspace name, so each worktree gets a unique color
  • Renames Electron.app to Superset (<workspace>).app with symlink + path.txt update so macOS shows the correct dock label (e.g. "Superset (color-electron)")
  • Patches CFBundleName/CFBundleDisplayName in the dev Electron plist and registers with LaunchServices
  • Sets the window title to include the workspace name (e.g. "Superset — color-electron")

Test plan

  • Run bun dev from a worktree and verify the dock icon has a colored border
  • Verify the dock label shows "Superset ()" instead of "Electron"
  • Verify the window title includes the workspace name
  • Run bun dev from a second worktree and verify it gets a different border color
  • Run bun install then bun dev and verify everything still works (handles fresh Electron.app)

Summary by CodeRabbit

  • New Features

    • Workspace-specific colored dock icons on macOS (development mode) for quick visual identification
    • Window titles show the workspace name alongside the app name in development for clearer context
  • Improvements

    • More reliable macOS app registration and launch behavior, with better handling for renamed app bundles and backward compatibility

Adds workspace-specific dock icon coloring and labeling so developers
can visually distinguish multiple Electron dev instances running from
different worktrees.

- Add dock-icon.ts: draws a colored rounded-rect border on the app icon
  using an SDF approach, with hue derived from the workspace name
- Rename Electron.app to "Superset (<workspace>).app" and update
  path.txt so macOS resolves the correct bundle name for the dock label
- Patch CFBundleName/CFBundleDisplayName in the dev Electron plist
- Set window title to include workspace name
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Feb 14, 2026

📝 Walkthrough

Walkthrough

Adds macOS workspace-aware behavior: patches Electron .app and Info.plist at runtime for workspace-specific bundle metadata and path, renders a deterministic workspace-colored border onto the macOS dock icon in development, and shows workspace names in window titles (dev-only).

Changes

Cohort / File(s) Summary
macOS Electron App Registration
apps/desktop/scripts/patch-dev-protocol.ts
Introduces workspace-aware macOS patching: resolves ELECTRON_DIST_DIR / Electron.app path, derives DISPLAY_NAME, validates and updates Info.plist keys (CFBundleIdentifier, CFBundleName, CFBundleDisplayName, CFBundleURLTypes), renames Electron.app to workspace-specific name, creates/updates symlink for compatibility, re-registers with Launch Services, and updates path.txt. Adds logging and try/catch error handling.
Workspace Dock Icon Renderer
apps/desktop/src/main/lib/dock-icon.ts
New module exporting setWorkspaceDockIcon(): computes deterministic hue from workspace name, converts HSL→RGB, locates app icon across modes, finds content bounds, draws an anti-aliased inward rounded border on the icon bitmap, and applies the modified icon via app.dock.setIcon() (macOS, dev-only).
Integration & Window Title
apps/desktop/src/main/index.ts, apps/desktop/src/main/windows/main.ts
Calls setWorkspaceDockIcon() during startup (dev-only on macOS) and changes window title logic to include workspace name in development mode (format: "ProductName — WorkspaceName").

Sequence Diagram(s)

sequenceDiagram
    actor App as Electron App (macOS)
    participant Workspace as Workspace Configuration
    participant FS as File System / Icon Resource
    participant Renderer as Bitmap Renderer
    participant Dock as macOS Dock Display

    App->>Workspace: request workspace name
    Workspace-->>App: workspace name

    App->>App: hashToHue(workspace name)
    App->>FS: load app icon bitmap
    FS-->>App: bitmap (pixels, width, height)

    App->>Renderer: compute content bounds & border params
    Renderer-->>App: modified bitmap with colored border

    App->>Dock: app.dock.setIcon(modified bitmap)
    Dock-->>Dock: update dock icon display
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

Poem

🐰 I hopped through code and found a name so bright,
I painted the dock with a stripe of light,
The app now wears a hue from the workspace it keeps,
Titles whisper projects while the system sleeps,
A rabbit's little patch — tidy, snug, and light.

🚥 Pre-merge checks | ✅ 3 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Merge Conflict Detection ⚠️ Warning ❌ Merge conflicts detected (44 files):

⚔️ .superset/setup.sh (content)
⚔️ .superset/teardown.sh (content)
⚔️ apps/api/src/app/api/github/install/route.ts (content)
⚔️ apps/api/src/app/api/integrations/linear/connect/route.ts (content)
⚔️ apps/api/src/app/api/integrations/slack/connect/route.ts (content)
⚔️ apps/api/src/app/api/integrations/slack/events/process-app-home-opened/process-app-home-opened.ts (content)
⚔️ apps/api/src/app/api/integrations/slack/events/process-assistant-message/process-assistant-message.ts (content)
⚔️ apps/api/src/app/api/integrations/slack/events/process-mention/process-mention.ts (content)
⚔️ apps/api/src/app/api/integrations/slack/events/utils/run-agent/run-agent.ts (content)
⚔️ apps/api/src/app/api/integrations/slack/link/route.ts (content)
⚔️ apps/desktop/scripts/patch-dev-protocol.ts (content)
⚔️ apps/desktop/src/lib/trpc/routers/ports/ports.ts (content)
⚔️ apps/desktop/src/lib/trpc/routers/terminal/terminal.ts (content)
⚔️ apps/desktop/src/main/index.ts (content)
⚔️ apps/desktop/src/main/lib/terminal/daemon/daemon-manager.ts (content)
⚔️ apps/desktop/src/main/lib/terminal/env.test.ts (content)
⚔️ apps/desktop/src/main/lib/terminal/env.ts (content)
⚔️ apps/desktop/src/main/lib/terminal/session.ts (content)
⚔️ apps/desktop/src/main/lib/terminal/types.ts (content)
⚔️ apps/desktop/src/main/windows/main.ts (content)
⚔️ apps/desktop/src/renderer/react-query/projects/useReorderProjects.ts (content)
⚔️ apps/desktop/src/renderer/react-query/workspaces/useOpenExternalWorktree.ts (content)
⚔️ apps/desktop/src/renderer/react-query/workspaces/useOpenWorktree.ts (content)
⚔️ apps/desktop/src/renderer/react-query/workspaces/useReorderWorkspaces.ts (content)
⚔️ apps/desktop/src/renderer/screens/main/components/WorkspaceInitEffects.tsx (content)
⚔️ apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/PortsList/hooks/usePortsData.ts (content)
⚔️ apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/index.tsx (content)
⚔️ apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/mosaic-theme.css (content)
⚔️ apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/Terminal/hooks/useTerminalConnection.ts (content)
⚔️ apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/Terminal/types.ts (content)
⚔️ apps/desktop/src/renderer/stores/theme/utils/index.ts (content)
⚔️ apps/marketing/src/lib/blog-utils.ts (content)
⚔️ apps/marketing/src/lib/blog.ts (content)
⚔️ apps/marketing/src/lib/changelog-utils.ts (content)
⚔️ apps/marketing/src/lib/changelog.ts (content)
⚔️ apps/marketing/src/lib/compare-utils.ts (content)
⚔️ apps/marketing/src/lib/compare.ts (content)
⚔️ apps/web/src/app/(dashboard)/settings/billing/page.tsx (content)
⚔️ bun.lock (content)
⚔️ packages/db/drizzle/meta/_journal.json (content)
⚔️ packages/db/src/utils/index.ts (content)
⚔️ packages/trpc/src/router/integration/utils.ts (content)
⚔️ packages/trpc/src/router/organization/organization.ts (content)
⚔️ packages/trpc/src/router/user/user.ts (content)

These conflicts must be resolved before merging into main.
Resolve conflicts locally and push changes to this branch.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the main feature: coloring the dock icon and adding workspace-specific labels, matching the primary changes across multiple files.
Description check ✅ Passed The description provides clear summaries of changes, test plan, and implementation details. However, the required template sections (Related Issues, Type of Change) are not filled out.
Docstring Coverage ✅ Passed Docstring coverage is 87.50% which is sufficient. The required threshold is 80.00%.

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

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch color-electron
⚔️ Resolve merge conflicts (beta)
  • Auto-commit resolved conflicts to branch color-electron
  • Create stacked PR with resolved conflicts
  • Post resolved changes as copyable diffs in a comment

No actionable comments were generated in the recent review. 🎉

🧹 Recent nitpick comments
apps/desktop/src/main/lib/dock-icon.ts (3)

21-56: Positional parameters on hslToRgb.

Per coding guidelines, functions with 2+ parameters should use object parameters. That said, hslToRgb(h, s, l) is a well-known mathematical signature where positional args are conventional and aid readability. Flagging for awareness only.


228-233: Extract magic numbers to named constants.

The inline literals 75, 55, 0.038, 0.22, and the alpha threshold 10 (line 119) lack semantic context. Per coding guidelines, hardcoded magic numbers should be extracted to named constants at module top.

♻️ Suggested extraction
+const BORDER_SATURATION = 75;
+const BORDER_LIGHTNESS = 55;
+const BORDER_THICKNESS_RATIO = 0.038;
+const CORNER_RADIUS_RATIO = 0.22;
+const ALPHA_VISIBILITY_THRESHOLD = 10;
+
 // ... inside setWorkspaceDockIcon:
-		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);

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


107-129: Edge case: fully transparent bitmap yields inverted bounds.

If every pixel has alpha ≤ 10, the returned bounds will have top > bottom and left > right. Downstream in drawBorder, the for loops won't execute so there's no crash, but it's a silent no-op with no log. Consider adding a guard in setWorkspaceDockIcon after findContentBounds to warn and bail out if bounds are degenerate, for easier debugging.

🛡️ Suggested guard
 		const bounds = findContentBounds(bitmap, size.width, size.height);
+		if (bounds.top > bounds.bottom || bounds.left > bounds.right) {
+			console.warn("[dock-icon] Icon has no visible content, skipping border");
+			return;
+		}
 		const thickness = Math.round(size.width * BORDER_THICKNESS_RATIO);

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

🤖 Fix all issues with AI agents
In `@apps/desktop/scripts/patch-dev-protocol.ts`:
- Around line 186-194: The code can create a dangling symlink when oldTargetPath
is missing; modify the block around unlinkSync/renameSync/symlinkSync so you
only create the symlink when the target directory exists (or create the
directory first). Specifically, in the else branch handling a workspace-name
change, after computing oldTargetPath and before calling
symlinkSync(ELECTRON_APP_PATH,...), check existsSync(desiredAppPath) (or call
mkdirSync(desiredAppPath, { recursive: true }) to ensure it exists) and only
then call symlinkSync(DESIRED_APP_NAME, ELECTRON_APP_PATH) and set actualAppPath
= desiredAppPath; otherwise skip creating the symlink (or fail early) to avoid
leaving a dangling link.
🧹 Nitpick comments (5)
apps/desktop/scripts/patch-dev-protocol.ts (3)

130-147: Shell-injection risk from workspace name interpolated into shell commands.

DISPLAY_NAME (which contains workspaceName) is interpolated directly into execSync command strings. If a workspace name ever contains characters like ", $, or backticks, PlistBuddy commands will break or behave unexpectedly.

Workspace names today are likely safe (alphanumeric + hyphens), but this is fragile. Consider sanitizing or at least validating the workspace name before interpolation.

🛡️ Suggested defensive guard (near line 68)
 const PROTOCOL_SCHEME = `superset-${workspaceName}`;
+
+if (!/^[\w-]+$/.test(workspaceName)) {
+	console.warn(`[patch-dev-protocol] Unsafe workspace name "${workspaceName}", skipping`);
+	process.exit(0);
+}
+
 const BUNDLE_ID = `com.superset.desktop.${workspaceName}`;

98-112: DRY: electronPkgDir / electronPkgCheck is resolved twice.

Lines 102-106 compute electronPkgCheck and pathTxtCheck, and Lines 230-232 compute the same paths again as electronPkgDir and pathTxtPath. Consider hoisting these to module-level constants alongside ELECTRON_DIST_DIR.

Also applies to: 230-232


139-147: Empty catch {} on PlistBuddy delete silently swallows all errors.

Line 144's empty catch is intentional for the "key doesn't exist" case, but it also hides unexpected failures (e.g., plist corruption, permission errors). A brief log at warn level would help debugging.

Suggested improvement
 try {
 	execSync(
 		`/usr/libexec/PlistBuddy -c "Delete :CFBundleDisplayName" "${PLIST_PATH}" 2>/dev/null`,
 	);
-} catch {}
+} catch {
+	// Expected when key doesn't exist yet
+}
apps/desktop/src/main/lib/dock-icon.ts (2)

226-232: Extract magic numbers for border styling to named constants.

Lines 227, 231, and 232 use inline magic numbers for saturation (75), lightness (55), border thickness ratio (0.038), and corner radius ratio (0.22). Per coding guidelines, these should be named constants at module top for clarity and easy tuning.

Suggested extraction
+const BORDER_SATURATION = 75;
+const BORDER_LIGHTNESS = 55;
+const BORDER_THICKNESS_RATIO = 0.038;
+const BORDER_CORNER_RADIUS_RATIO = 0.22;
+
 export function setWorkspaceDockIcon(): void {

Then reference them in the function body:

-		const rgb = hslToRgb(hue, 75, 55);
+		const rgb = hslToRgb(hue, BORDER_SATURATION, BORDER_LIGHTNESS);
 
 		const bounds = findContentBounds(bitmap, size.width, size.height);
-		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 * BORDER_CORNER_RADIUS_RATIO);

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


107-129: Edge case: findContentBounds returns inverted bounds when the icon is fully transparent.

If every pixel has alpha ≤ 10, the returned bounds will be { top: height, left: width, bottom: 0, right: 0 }. The drawBorder loop is safe (it won't iterate), but the caller has no signal that the bounds are invalid. Since icon.isEmpty() is checked earlier in setWorkspaceDockIcon, this is unlikely to be hit in practice.

Comment on lines +186 to +194
} else {
// Different workspace name from previous run — update
const oldTargetPath = resolve(ELECTRON_DIST_DIR, currentTarget);
unlinkSync(ELECTRON_APP_PATH);
if (existsSync(oldTargetPath)) {
renameSync(oldTargetPath, desiredAppPath);
}
symlinkSync(DESIRED_APP_NAME, ELECTRON_APP_PATH);
actualAppPath = desiredAppPath;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Dangling symlink when the previous workspace's app directory is missing.

If oldTargetPath doesn't exist (Line 190 is false), the renameSync is skipped but symlinkSync on Line 193 still creates a symlink pointing to DESIRED_APP_NAME — which won't exist either. This leaves a dangling symlink and the rest of the script (lsregister, path.txt) will operate on a nonexistent path.

🛡️ Suggested fix
 		const oldTargetPath = resolve(ELECTRON_DIST_DIR, currentTarget);
 		unlinkSync(ELECTRON_APP_PATH);
 		if (existsSync(oldTargetPath)) {
 			renameSync(oldTargetPath, desiredAppPath);
+		} else if (!existsSync(desiredAppPath)) {
+			console.warn("[patch-dev-protocol] Previous Electron.app not found and desired target missing — re-install electron");
+			process.exit(1);
 		}
 		symlinkSync(DESIRED_APP_NAME, ELECTRON_APP_PATH);
📝 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
} else {
// Different workspace name from previous run — update
const oldTargetPath = resolve(ELECTRON_DIST_DIR, currentTarget);
unlinkSync(ELECTRON_APP_PATH);
if (existsSync(oldTargetPath)) {
renameSync(oldTargetPath, desiredAppPath);
}
symlinkSync(DESIRED_APP_NAME, ELECTRON_APP_PATH);
actualAppPath = desiredAppPath;
} else {
// Different workspace name from previous run — update
const oldTargetPath = resolve(ELECTRON_DIST_DIR, currentTarget);
unlinkSync(ELECTRON_APP_PATH);
if (existsSync(oldTargetPath)) {
renameSync(oldTargetPath, desiredAppPath);
} else if (!existsSync(desiredAppPath)) {
console.warn("[patch-dev-protocol] Previous Electron.app not found and desired target missing — re-install electron");
process.exit(1);
}
symlinkSync(DESIRED_APP_NAME, ELECTRON_APP_PATH);
actualAppPath = desiredAppPath;
🤖 Prompt for AI Agents
In `@apps/desktop/scripts/patch-dev-protocol.ts` around lines 186 - 194, The code
can create a dangling symlink when oldTargetPath is missing; modify the block
around unlinkSync/renameSync/symlinkSync so you only create the symlink when the
target directory exists (or create the directory first). Specifically, in the
else branch handling a workspace-name change, after computing oldTargetPath and
before calling symlinkSync(ELECTRON_APP_PATH,...), check
existsSync(desiredAppPath) (or call mkdirSync(desiredAppPath, { recursive: true
}) to ensure it exists) and only then call symlinkSync(DESIRED_APP_NAME,
ELECTRON_APP_PATH) and set actualAppPath = desiredAppPath; otherwise skip
creating the symlink (or fail early) to avoid leaving a dangling link.

@saddlepaddle saddlepaddle merged commit 2c98dc5 into main Feb 14, 2026
6 checks passed
@github-actions
Copy link
Copy Markdown
Contributor

🧹 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! 🎉

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