Internal SmartGift build of a Claude Code monitoring dashboard. Lanes: a durable unit of parallel agent work, one per working directory, tracked across session restarts. Managed lanes are git worktrees the dashboard provisions and can reset or remove behind a three-check destroy guard and a counted preflight; adopted lanes are directories you already own and are never destroyable. Pipelines: a lane moves through pipeline stages. A stage the agent declares with evidence renders green; a stage inferred from the tool-event stream renders dashed amber and never counts as done. Detection is forward-only within a 30-minute window, and never writes the declared stage. Workspace: one page at /run with a lane grid, the selected lane's pipeline, and a full Claude console behind a disclosure.
26 KiB
Claude Code Monitor — Desktop App (macOS & Windows)
The dashboard ships with an optional native desktop app (built with Electron 35) that wraps the existing server + client into a single application you install once and forget — a macOS .app (shipped as a .dmg) and a Windows .exe (an NSIS installer plus a no-install portable build). Everything you see in the browser at localhost:4820 lives inside this window, with native OS lifecycle on top: a menu-bar / notification-area (tray) icon, a native application menu, auto-start at login, and a single quit button that cleans up the server.
Why this exists in addition to the PWA
The PWA (added in #144) makes the dashboard installable in Chromium-based browsers, which is great for users who already keep the server running. The desktop app solves the orthogonal problem: starting and keeping the server running without a terminal window. Concretely:
| Capability | PWA | Desktop App |
|---|---|---|
| Installs to dock / Applications | ✅ | ✅ |
| Manages the Express server | ❌ — user must npm start separately |
✅ — embedded in-process |
| Auto-starts at login | ❌ | ✅ via macOS Login Items / Windows HKCU\…\Run |
| Menu-bar / notification-area (tray) icon for always-on status | ❌ | ✅ |
| Native application menu (⌘ / Ctrl shortcuts, etc.) | ❌ | ✅ |
| Survives browser restart | ⚠️ depends on browser | ✅ |
The two coexist — install whichever fits your workflow.
Quick install
Option A — download a pre-built installer (recommended):
-
Open Releases → latest and grab the asset for your platform. Every
mastercommit that bumps the version inpackage.jsoncuts a newvX.Y.Zrelease automatically (CI publishes it), so this link always lands on the current build — no GitHub sign-in required.Platform Asset Notes macOS (Apple Silicon) ClaudeCodeMonitor-<ver>-arm64.dmgdrag into /ApplicationsmacOS (Intel) ClaudeCodeMonitor-<ver>-x64.dmgdrag into /ApplicationsWindows (installer) ClaudeCodeMonitor-Setup-<ver>-x64.exeper-user install, no admin Windows (portable) ClaudeCodeMonitor-<ver>-x64-portable.exerun without installing -
Want a per-commit build instead of waiting for a release? Every green CI run uploads a workflow artifact (sign-in required, 14-day retention) —
ClaudeCodeMonitor-dmgfrom the🍎 macOS Desktop (DMG)job andClaudeCodeMonitor-winfrom the🪟 Windows Desktop (EXE)job:gh run download <run-id> -R Smartgift-AI/Claude-Code-Monitor -n ClaudeCodeMonitor-dmg # or ClaudeCodeMonitor-win -
macOS: double-click the DMG → drag
Claude Code Monitor.appinto yourApplicationsfolder. Open it; macOS may show a Gatekeeper warning the first time — see Gatekeeper & SmartScreen below. -
Windows: run
ClaudeCodeMonitor-Setup-<ver>-x64.exe(per-user, no admin) and follow the wizard, or just run the*-portable.exeto launch without installing. Windows SmartScreen may show "Windows protected your PC" the first time — see Gatekeeper & SmartScreen below.
Option B — build locally:
# In the project root, after `git clone`:
npm run setup # installs root + client + vscode-extension deps
npm run build # builds the React client
npm run desktop:install # installs Electron + electron-builder
# Build for macOS (run ON macOS) — pick one:
npm run desktop:dmg:arm64 # Apple Silicon only — FAST (~1 min); use this for your own Mac
npm run desktop:dmg:x64 # Intel only — FAST
npm run desktop:dmg # BOTH per-arch DMGs (arm64 + x64) — the release build; slower (packages each arch)
npm run desktop:dmg:universal # ONE merged universal DMG (arm64 + x86_64 in a single file) — optional, slowest
# Build for Windows (run ON Windows) — pick one:
npm run desktop:win # NSIS installer → desktop/release/ClaudeCodeMonitor-Setup-<ver>-x64.exe
npm run desktop:win:portable # no-install portable → desktop/release/ClaudeCodeMonitor-<ver>-x64-portable.exe
# electron-builder packages for the HOST OS — you cannot build a Windows .exe
# on macOS or a macOS .dmg on Windows.
# Open the macOS DMG you just built. desktop:dmg:arm64 / :x64 wipe release/ and emit
# one DMG; desktop:dmg wipes release/ and emits both (…-arm64.dmg + …-x64.dmg).
open desktop/release/ClaudeCodeMonitor-*-arm64.dmg # …-x64.dmg for the Intel build
desktop:dmgbuilds both architectures, so it takes longer. It packages and ad-hoc-signs the app twice — once forarm64, once forx64— and emits two separate DMGs (…-arm64.dmg+…-x64.dmg). It does not merge them into a single universal binary; the release ships the two per-arch DMGs. For running on your own Mac, use the arch-specific command (desktop:dmg:arm64/desktop:dmg:x64) — half the work, and it finishes in about a minute. CI runsdesktop:dmgfor you and uploads both DMGs as theClaudeCodeMonitor-dmgartifact, so you rarely need to build them locally.
What happens when you launch the app
- The Electron main process picks a free port — preferring 4820, falling back to 4821–4829, then a random high port if all those are taken.
- If something already answers
/api/healthon port 4820 (e.g. you rannpm startin a terminal), the app adopts that server and skips starting a second one. No double-binding, no SQLite contention. - Otherwise it
require()sserver/index.jsdirectly in-process — same Node runtime as the main process, same memory. Boot is typically under two seconds. - On startup the server records its live port to
~/.claude/.agent-dashboard.json. The Claude Code hook handler reads that file, so events still reach the dashboard when the app bound a fallback port instead of 4820. - The dashboard window opens — unless the app was launched at login (on macOS via Login Items; on Windows via the
HKCU\…\Runentry, detected through a--ccam-hiddenlaunch arg since Windows has nowasOpenedAtLogin), in which case it stays tray-only. - A tray icon appears — the macOS menu bar or the Windows notification area. One click opens a dropdown with a live status snapshot (server port, active sessions, working agents, events today — all clickable to jump into the dashboard) plus Open Dashboard, Open in Browser, Restart Server, Show Logs, Open at Login (toggle), and Quit.
Lifecycle semantics
- Closing the window hides it. The server keeps running, the tray icon stays, and (on macOS) the dock icon stays too — clicking either re-opens the window. Independent signals that the app is still alive.
- Quitting (⌘Q / Ctrl+Q, Quit in the application menu, or Quit in the tray menu) pops a confirmation dialog — "Quit Claude Code Monitor? Press ⌘Q again to skip this prompt and quit immediately." Press Quit in the dialog, or press ⌘Q / Ctrl+Q a second time to bypass the prompt. Either way the SQLite handle is checkpointed cleanly before the process exits.
- Tray — the macOS menu bar / Windows notification area. macOS uses a black template glyph the OS tints for light/dark menu bars; Windows uses the colored
icon.ico, because a template glyph would vanish on the dark taskbar. A single click (left or right) opens the dropdown, which shows a live status snapshot pulled straight from the embedded SQLite handle each time it opens: server port, active sessions, working agents, and events today. Snapshot rows are clickable — they open the dashboard. The tray's Open Dashboard reliably raises the window even when it is minimized or behind other windows. (The application menu's File ▸ Open Dashboard / ⌘1 is macOS-only — on Windows/Linux a window-attached menu accelerator can't reopen a hidden window, so reopening is the tray's job there.) - Window / taskbar icon — the
BrowserWindowsets itsiconto the colored app logo (icon.icoon Windows,icon.pngelsewhere — the same logo as the macOS Dock, rendered fromassets/icon.svg), so an unpackageddesktop:devrun shows the real app logo in the title bar / taskbar instead of the generic Electron icon. The macOS dev Dock icon is set too; packaged apps already get theirs from the bundle.icns/.exe. - Open-at-login toggle: flip Open at Login in the tray menu (or the app menu). Both platforms go through Electron's first-party
app.*LoginItemSettingsAPI — no third-party deps. On macOS it registers via theSMAppServiceAPI, so the entry appears under → System Settings → General → Login Items. On Windows it writes a per-userHKCU\Software\Microsoft\Windows\CurrentVersion\Runentry, visible under Task Manager → Startup; a login-triggered launch is detected via a--ccam-hiddenarg (Windows has nowasOpenedAtLogin). On Linux the toggle is a no-op (unsupported). - Single-instance: double-launching just focuses the existing window. No second server, no port collision. (Applies on every platform.)
- Logs live at
~/Library/Logs/Claude Code Monitor/desktop.logon macOS and%APPDATA%\Claude Code Monitor\logs\desktop.logon Windows (use Show Logs in the tray menu to open the folder). - Your data (the SQLite database and VAPID keys) lives outside the app bundle / install dir, so it survives app reinstalls and updates —
~/Library/Application Support/Claude Code Monitor/data/on macOS,%APPDATA%\Claude Code Monitor\data\on Windows. The Windows NSIS uninstaller keeps this data by default (deleteAppDataOnUninstall: false), mirroring how dragging the.appto the Trash on macOS never touches your data. - The
claudeCLI on PATH. On macOS the app resolves it using your login-shellPATH, recovered at startup — so "Run Claude" works even though a Finder/Dock-launched app would otherwise only inherit a minimalPATH. On Windows the inherited userPATHalready includes it, so no recovery is needed. - Notifications (including the in-dashboard Send test notification button) are delivered as native OS toasts on both platforms when running inside the app — the embedded server calls Electron's
NotificationAPI directly. On Windows the app sets anAppUserModelId(com.vn.smartgift.ccam.desktop, matching the electron-builderappId) so toasts attribute to the app and its taskbar windows group correctly. Web Push doesn't work reliably inside Electron (Chromium-in-Electron ships without Firebase Cloud Messaging credentials, sopushManager.subscribereturns endpoints nothing can deliver to), and this path bypasses it entirely. The web dashboard atnpm startcontinues to use Web Push as before. - Coexists with the web dashboard. You can run the desktop app and
npm run dev(ornpm start) at the same time. Each server writes its{port, pid, startedAt, dataDir}entry to a shared discovery file at~/.claude/.agent-dashboard.json, and the Claude Code hook handler POSTs to one ingest target per unique SQLite data directory (lowest port wins when both share~/.claude/agent-dashboard, so events are never double-ingested). Servers with different databases (e.g. the desktop app's Application Support dir alongsidenpm run dev) still each receive hooks and stay real-time.
File layout (for contributors)
desktop/
├── package.json # Electron + electron-builder
├── tsconfig.json
├── electron-builder.yml # macOS (dmg) + Windows (nsis/portable) targets; signing/notarization hooks
├── assets/ # icon.svg + generated icon.icns (macOS) + icon.ico (Windows) + tray PNGs
├── src/
│ ├── main.ts # main process entry, lifecycle; setAppUserModelId on win32
│ ├── server-host.ts # in-process Express boot, port discovery, adopt
│ ├── window.ts # BrowserWindow + persisted state
│ ├── tray.ts # tray icon (platform image: template PNG on macOS, icon.ico on Windows) + context menu
│ ├── menu.ts # native application menu
│ ├── login-item.ts # open-at-login (macOS Login Items + Windows HKCU\…\Run startup)
│ ├── shell-path.ts # recover the user's shell PATH (find `claude`)
│ ├── preload.ts # (empty — kept for future renderer bridges)
│ ├── logger.ts # file logger
│ └── constants.ts # incl. APP_ID (matches electron-builder appId)
├── scripts/
│ ├── install.js # `desktop:install` wrapper: runs npm install, then prints actionable native-dep help + exits non-zero on failure
│ ├── preflight.js # shared native-dep check (hasBetterSqliteBinary) + per-OS prerequisite help (printNativeDepHelp)
│ ├── prebuild.js # ensures root + client are built before tsc; shells npm/npx on Windows (.cmd shims); fails fast with setup help when the better-sqlite3 native binary is missing
│ ├── build-icons.sh # SVG → PNG/ICNS + tray PNGs via qlmanage/sips/iconutil (macOS)
│ ├── build-win-icon.ps1 # icon.png → icon.ico for Windows (PowerShell + .NET)
│ └── notarize.js # electron-builder afterSign hook (opt-in; macOS only)
└── tests/
└── smoke.test.mjs # spawn-and-probe /api/health (resolves the real electron binary via createRequire)
Changes outside desktop/ are deliberately minimal:
server/index.js— a behavior-preserving refactor: the post-listen bootstrap (one-time legacy-session import, update scheduler, Claude Code config watcher, orphaned-run reconciliation) was extracted into an exportedstartBackgroundServices()so the embedded server runs exactly whatnode server/index.jsruns. The standalone server path is functionally unchanged. (The legacy-session import previously sat in the standalone-onlyrequire.mainblock, so the desktop dashboard started empty — moving it intostartBackgroundServices()fixes that.) It also now publishes its live port viaserver/lib/server-info.json startup.server/lib/server-info.js(new) — writes/reads the~/.claude/.agent-dashboard.jsonport discovery file.scripts/hook-handler.js— resolves the dashboard port from the discovery file (falling back toCLAUDE_DASHBOARD_PORT, then 4820), so hook events reach the server even when it bound a fallback port.
client/, mcp/, and vscode-extension/ are untouched. The Electron main process is otherwise just a host for the same code.
Gatekeeper & SmartScreen (first launch)
macOS — Gatekeeper
The DMG is ad-hoc signed by default — that's all the project can offer without a paid Apple Developer ID. macOS will warn the first time you open it: "Apple could not verify…".
Two ways past it:
# Easiest: strip the quarantine attribute from the DMG before opening.
xattr -cr ~/Downloads/ClaudeCodeMonitor-*.dmg
Or open → System Settings → Privacy & Security, scroll to the blocked DMG, click Open Anyway.
Windows — SmartScreen
The Windows .exe (both the installer and the portable build) is unsigned by default, so Windows SmartScreen may show "Windows protected your PC" the first time you run it. Click More info → Run anyway to launch it.
Authenticode signing is opt-in for the maintainer: provide a code-signing certificate via CSC_LINK (a base64-encoded .p12) and CSC_KEY_PASSWORD and electron-builder signs the .exe automatically — no code change required. A signed build skips the SmartScreen prompt.
Notarization (for the maintainer)
When you're ready to make this go away for everyone, add these three repository secrets:
| Secret | Where it comes from |
|---|---|
APPLE_ID |
Your Apple ID email |
APPLE_TEAM_ID |
Your Apple Developer team ID |
APPLE_APP_SPECIFIC_PASSWORD |
An app-specific password created at appleid.apple.com |
Optionally, also CSC_LINK (base64-encoded .p12) and CSC_KEY_PASSWORD to provide an explicit Developer ID certificate from outside the runner keychain. The CI workflow picks them up automatically — no code change required. See desktop/scripts/notarize.js for the hook.
Local builds are always ad-hoc signed: the
packagescript setsCSC_IDENTITY_AUTO_DISCOVERY=false, so a code-signing certificate already in your macOS keychain is never auto-discovered (an Apple Development cert would otherwise be picked up and fail distribution-type signing). Real signing activates only through the explicitCSC_LINKcertificate above — that path is unaffected by the flag.
Development workflow
# Hot-iterate on the main process (rebuilds tsc on save would be next steps;
# v1 ships without watch mode — just re-run desktop:dev after changes):
npm run desktop:dev
# Smoke test (also runs in CI on macOS):
npm run desktop:test
# macOS — single-architecture DMG — fast (~1 min):
npm run desktop:dmg:arm64 # or desktop:dmg:x64 for Intel
# macOS — both per-arch DMGs — slower (builds + signs each architecture):
npm run desktop:dmg
# macOS — one merged universal DMG (arm64 + x86_64 in a single file) — optional, slowest:
npm run desktop:dmg:universal
# Windows — NSIS installer / no-install portable (run ON Windows):
npm run desktop:win # NSIS installer .exe
npm run desktop:win:portable # no-install portable .exe
electron-builder packages for the host OS — build DMGs on macOS and the Windows
.exes on Windows. The Windows icon regenerates fromicon.pngwithnpm run build:win-icon(PowerShell + .NET); the macOS icns + tray PNGs come fromnpm run build:icons. On Windows,better-sqlite3is fetched as a prebuilt Electron binary bynpm run desktop:install(its postinstall runselectron-builder install-app-deps), so no Visual Studio C++ toolchain is needed in the common case. If that fetch/rebuild does fail (no C++ toolchain, or a Node version with no prebuilt binary),npm run desktop:install— and anydesktop:*build, gated byprebuild.js— prints the exact per-OS fix plus a no-toolchain alternative and fails loudly rather than crashing at runtime:cd desktop npm install --ignore-scripts node node_modules/electron/install.js npx electron-builder install-app-depsA Node LTS (20/22) ships prebuilt
better-sqlite3binaries and avoids the compile entirely.
After
npm run cleanindesktop/, you mustnpm run buildagain before packaging —cleanremovesout/, andelectron-builderonly packages, it does not compile. Thedesktop:dmg*scripts chain the build for you; a bareelectron-buildercall does not, and fails with "entry file out/main.js does not exist".
The smoke test does not exercise the BrowserWindow (no display on headless CI). It spawns Electron, waits for the embedded server to answer /api/health, then shuts down. Anything that depends on the renderer is part of the manual QA checklist on the PR.
Known caveats
- Bundle size ≈ 80 MB DMG, ≈ 250 MB on disk. The standard Electron tax. The Windows installer is comparable. Tauri would cut this dramatically but at the cost of a sidecar-process model and a Rust toolchain dependency — fair to revisit in a follow-up PR if bundle size becomes a real complaint.
- Native modules:
better-sqlite3is rebuilt against Electron's Node version automatically viaelectron-builder install-app-depsin the desktop workspace'spostinstall. On Windows it is fetched as a prebuilt Electron binary, so no Visual Studio C++ toolchain is needed in the common case. If that build does fail (or the binary is missing afterward),npm run desktop:install— and anydesktop:*build — prints the exact per-OS fix (Windows: Visual Studio Build Tools with the "Desktop development with C++" workload; macOS:xcode-select --install; Linux: build-essential + python3) plus a no-toolchain alternative (npm install --ignore-scripts→node node_modules/electron/install.js→npx electron-builder install-app-deps), and exits non-zero — failing loudly at install/build time rather than crashing at runtime. Even so, if the module is unavailable the server falls back tonode:sqlite(per #37), so the app still boots. - Per-architecture DMGs:
npm run desktop:dmgbuilds both macOS DMGs (onearm64, onex64) — the release build, and slower because it packages each architecture separately. It does not produce a merged universal binary; the release ships the two per-arch DMGs.npm run desktop:dmg:arm64andnpm run desktop:dmg:x64build a single architecture instead — much faster, and roughly half the disk. If you specifically want a single merged universal binary (both slices in one.dmg,lipo-fat),npm run desktop:dmg:universalproduces one via@electron/universal— the slowest option, and not what the release ships, but handy for hand-distributing one file that runs on any Mac. - Auto-update: not wired on either platform. The current update path is re-download the latest installer (DMG on macOS,
.exeon Windows).electron-updater+ GitHub Releases is the natural follow-up.
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
| "Apple could not verify…" on first launch (macOS) | Unnotarized DMG | xattr -cr ~/Downloads/ClaudeCodeMonitor-*.dmg |
| "Windows protected your PC" on first launch (Windows) | The .exe is unsigned by default (SmartScreen) |
Click More info → Run anyway. To remove the prompt for everyone, the maintainer can enable Authenticode signing via CSC_LINK + CSC_KEY_PASSWORD |
| macOS prompts to install Rosetta when opening the app | You installed the x64 build on an Apple Silicon Mac | Check your arch with uname -m (arm64 → Apple Silicon, build with desktop:dmg:arm64). The arch-specific desktop:dmg:arm64 / desktop:dmg:x64 builds each wipe release/ and emit a single DMG whose mounted-volume title states the architecture — e.g. Claude Code Monitor (Apple Silicon) — so there is no ambiguous window to drag from. (desktop:dmg emits both per-arch DMGs at once, for release.) If stale DMGs from an older build linger, clear them with rm -rf desktop/release and rebuild |
| Window shows but content is blank (macOS) | Server didn't boot — check ~/Library/Logs/Claude Code Monitor/desktop.log |
Restart from tray → Restart Server |
| Window shows but content is blank (Windows) | Server didn't boot — check %APPDATA%\Claude Code Monitor\logs\desktop.log |
Restart from tray → Restart Server |
| Tray icon missing (macOS) | The OS hides tray icons when the menu bar is full | Move other menu-bar items aside, or look in the overflow chevron |
| Tray icon missing (Windows) | Windows tucked it into the notification-area overflow | Click the ^ overflow chevron in the taskbar; drag the icon out to keep it pinned |
| App didn't auto-start at login (macOS) | Login Items entry got revoked by macOS | Toggle Open at Login off and on again from the tray menu |
| App didn't auto-start at login (Windows) | The HKCU\…\Run startup entry is missing or was disabled |
Toggle Open at Login off and on again from the tray menu, then confirm the entry under Task Manager → Startup is Enabled |
npm run desktop:win / :win:portable fails or produces nothing |
electron-builder packages for the host OS — you ran it on macOS/Linux | Build the Windows .exe on Windows (and DMGs on macOS) |
Desktop build/install fails on better-sqlite3 / native binary missing |
No C++ toolchain, or no prebuilt for your Node version | Run npm run desktop:install and follow the printed help, or use the no-toolchain alternative (npm install --ignore-scripts → node node_modules/electron/install.js → npx electron-builder install-app-deps); or use Node LTS 20/22 |
| Port 4820 already in use, app refuses to start | Something other than the dashboard is on 4820 and it doesn't answer /api/health |
The app will pick a fallback (4821–4829, then a random high port) — check the tray menu's port indicator |
| Dashboard stays empty — 0 sessions, 0 agents, no real-time updates | The app bound a fallback port (4820 was taken), and the Claude Code hooks were posting events to the wrong port | Fixed — the server publishes its live port to ~/.claude/.agent-dashboard.json and the hook handler reads it. After upgrading from a pre-fix build, start a new Claude Code session so the updated hooks take effect |
desktop:dmg seems slow |
Not stuck — it packages two architectures back-to-back (arch=x64 then arch=arm64) |
Wait it out, or build a single architecture with desktop:dmg:arm64 / desktop:dmg:x64 |
Build fails: entry file out/main.js does not exist |
electron-builder was run without compiling TypeScript first |
Build via npm run desktop:dmg* (chains the build); don't invoke electron-builder bare |
Signing fails with Application … could not be found |
A code-signing certificate in your keychain was auto-discovered | Fixed — the package script sets CSC_IDENTITY_AUTO_DISCOVERY=false; build via npm run desktop:dmg* |
"Run Claude" reports the claude CLI isn't on your PATH |
A Finder/Dock-launched app inherits launchd's minimal PATH, not your shell PATH | Fixed — the app recovers your login-shell PATH at startup. If it persists, ensure claude is a real executable (not a shell alias/function) and on your shell PATH |
| Imported history / sessions vanished after updating the app | Older builds stored the database inside the (replaceable) app bundle | Fixed — data now lives in ~/Library/Application Support/Claude Code Monitor/data/ and survives reinstalls. After upgrading from a pre-fix build, re-run Import History → Rescan once |
Signing fails: Application … could not be found after retries |
A keychain code-signing certificate was auto-discovered | Fixed — the package script sets CSC_IDENTITY_AUTO_DISCOVERY=false; build via npm run desktop:dmg* |