feat: Claude Code Monitor — lanes, pipelines and a merged workspace

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.
This commit is contained in:
2026-07-29 17:07:45 +07:00
commit 57dc91585d
783 changed files with 221743 additions and 0 deletions
+91
View File
@@ -0,0 +1,91 @@
#!/bin/bash
# Generate icon.icns + tray-icon-Template.png{,@2x.png} from the SVG sources.
#
# Uses macOS-built-in tools only — no Homebrew or npm dependencies:
# * qlmanage : SVG → PNG via Quick Look (always present on macOS)
# * sips : PNG resize/format
# * iconutil : .iconset directory → .icns
#
# This script is idempotent. Run from desktop/ or from anywhere.
# Author: Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
# @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
set -euo pipefail
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ASSETS="$(cd "$HERE/../assets" && pwd)"
require() {
command -v "$1" >/dev/null 2>&1 || {
echo "error: required tool '$1' not found. This script only runs on macOS." >&2
exit 1
}
}
require qlmanage
require sips
require iconutil
cd "$ASSETS"
echo ">>> rendering icon.svg → icon.png (1024)"
TMP="$(mktemp -d)"
trap 'rm -rf "$TMP"' EXIT
qlmanage -t -s 1024 -o "$TMP" icon.svg >/dev/null 2>&1
mv "$TMP/icon.svg.png" icon.png
echo ">>> building icon.iconset"
rm -rf icon.iconset
mkdir -p icon.iconset
for s in 16 32 64 128 256 512 1024; do
sips -z "$s" "$s" icon.png --out "icon.iconset/icon_${s}x${s}.png" >/dev/null
done
# Apple's @2x naming convention.
cp icon.iconset/icon_32x32.png icon.iconset/icon_16x16@2x.png
cp icon.iconset/icon_64x64.png icon.iconset/icon_32x32@2x.png
cp icon.iconset/icon_256x256.png icon.iconset/icon_128x128@2x.png
cp icon.iconset/icon_512x512.png icon.iconset/icon_256x256@2x.png
cp icon.iconset/icon_1024x1024.png icon.iconset/icon_512x512@2x.png
# Drop the 64-only file; iconutil dislikes unknown sizes.
rm -f icon.iconset/icon_64x64.png
echo ">>> compiling icon.icns"
iconutil -c icns icon.iconset -o icon.icns
rm -rf icon.iconset
echo ">>> rendering tray-icon-Template.png{,@2x.png} via Python"
# qlmanage flattens SVG against an opaque white background — the tray PNG
# ends up with alpha=255 everywhere and macOS template tinting turns the
# whole 22x22 bounding box white in the menu bar. Generate the RGBA PNG
# pixel-by-pixel instead. Geometry mirrors tray-icon.svg (22-unit viewBox).
require python3
python3 - <<'PY'
import struct, zlib
def make_png(width, height, pixels):
def chunk(tag, data):
return struct.pack('>I', len(data)) + tag + data + struct.pack('>I', zlib.crc32(tag + data))
sig = b'\x89PNG\r\n\x1a\n'
ihdr = struct.pack('>IIBBBBB', width, height, 8, 6, 0, 0, 0) # 8-bit RGBA
raw = bytearray()
for y in range(height):
raw.append(0)
raw.extend(pixels[y*width*4:(y+1)*width*4])
return sig + chunk(b'IHDR', ihdr) + chunk(b'IDAT', zlib.compress(bytes(raw), 9)) + chunk(b'IEND', b'')
def draw(w, h, s):
px = bytearray(w * h * 4) # alpha=0 -> transparent
def rect(x, y, rw, rh):
for j in range(y, min(y+rh, h)):
for i in range(x, min(x+rw, w)):
o = (j*w + i) * 4
px[o:o+4] = b'\x00\x00\x00\xff' # opaque black
rect(2*s, 14*s, 4*s, 7*s)
rect(9*s, 11*s, 4*s, 10*s)
rect(16*s, 5*s, 4*s, 16*s)
return px
with open('tray-icon-Template.png', 'wb') as f: f.write(make_png(22, 22, draw(22, 22, 1)))
with open('tray-icon-Template@2x.png', 'wb') as f: f.write(make_png(44, 44, draw(44, 44, 2)))
PY
echo ">>> done."
ls -la icon.icns tray-icon-Template.png tray-icon-Template@2x.png
+121
View File
@@ -0,0 +1,121 @@
<#
.SYNOPSIS
Generate assets/icon.ico from assets/icon.png — the Windows counterpart to
scripts/build-icons.sh (which produces icon.icns + the macOS tray PNGs).
.DESCRIPTION
Uses only the .NET Framework's System.Drawing (always present on Windows) —
no ImageMagick, no npm dependency. icon.png is the 1024x1024 raster already
rendered from assets/icon.svg by the macOS icon pipeline; this script
downscales it to the standard Windows icon sizes and packs them into a
classic, maximally-compatible BMP-based .ico (32bpp BGRA + AND mask). That
format is what electron-builder embeds in the .exe and what NSIS uses for
the installer icon, and it renders correctly on Windows 7 through 11.
Idempotent. Run from anywhere:
powershell -ExecutionPolicy Bypass -File desktop/scripts/build-win-icon.ps1
.NOTES
Author: Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
#>
$ErrorActionPreference = 'Stop'
Add-Type -AssemblyName System.Drawing
$here = Split-Path -Parent $MyInvocation.MyCommand.Path
$assets = Join-Path (Split-Path -Parent $here) 'assets'
$srcPng = Join-Path $assets 'icon.png'
$outIco = Join-Path $assets 'icon.ico'
if (-not (Test-Path $srcPng)) {
throw "icon.png not found at $srcPng. Generate it first (scripts/build-icons.sh renders it from icon.svg)."
}
# Standard Windows icon ladder. 256 is required by electron-builder; the small
# sizes keep the taskbar / Alt-Tab / tray crisp.
$sizes = 16, 24, 32, 48, 64, 128, 256
$src = [System.Drawing.Image]::FromFile($srcPng)
$entries = New-Object System.Collections.ArrayList
try {
foreach ($s in $sizes) {
$bmp = New-Object System.Drawing.Bitmap($s, $s, [System.Drawing.Imaging.PixelFormat]::Format32bppArgb)
$g = [System.Drawing.Graphics]::FromImage($bmp)
$g.InterpolationMode = [System.Drawing.Drawing2D.InterpolationMode]::HighQualityBicubic
$g.PixelOffsetMode = [System.Drawing.Drawing2D.PixelOffsetMode]::HighQuality
$g.SmoothingMode = [System.Drawing.Drawing2D.SmoothingMode]::HighQuality
$g.CompositingQuality = [System.Drawing.Drawing2D.CompositingQuality]::HighQuality
$g.Clear([System.Drawing.Color]::Transparent)
$g.DrawImage($src, 0, 0, $s, $s)
$g.Dispose()
# Pull raw pixels: Format32bppArgb is stored little-endian as B,G,R,A —
# exactly the byte order a 32bpp DIB wants. Rows are top-down here.
$rect = New-Object System.Drawing.Rectangle(0, 0, $s, $s)
$data = $bmp.LockBits($rect, [System.Drawing.Imaging.ImageLockMode]::ReadOnly, [System.Drawing.Imaging.PixelFormat]::Format32bppArgb)
$stride = $data.Stride
$buf = New-Object byte[] ($stride * $s)
[System.Runtime.InteropServices.Marshal]::Copy($data.Scan0, $buf, 0, $buf.Length)
$bmp.UnlockBits($data)
$bmp.Dispose()
# Build the DIB: BITMAPINFOHEADER(40) + XOR bitmap (bottom-up BGRA) +
# 1bpp AND mask (bottom-up, all zeros — alpha channel does the masking).
$ms = New-Object System.IO.MemoryStream
$bw = New-Object System.IO.BinaryWriter($ms)
$bw.Write([int]40) # biSize
$bw.Write([int]$s) # biWidth
$bw.Write([int]($s * 2)) # biHeight = XOR height + AND height
$bw.Write([int16]1) # biPlanes
$bw.Write([int16]32) # biBitCount
$bw.Write([int]0) # biCompression = BI_RGB
$bw.Write([int]0) # biSizeImage
$bw.Write([int]0) # biXPelsPerMeter
$bw.Write([int]0) # biYPelsPerMeter
$bw.Write([int]0) # biClrUsed
$bw.Write([int]0) # biClrImportant
# XOR pixels, bottom-up.
for ($y = $s - 1; $y -ge 0; $y--) {
$bw.Write($buf, $y * $stride, 4 * $s)
}
# AND mask: 1 bit/pixel, each row padded to a 4-byte boundary, all zero.
$maskRow = [int]([math]::Floor((($s + 31) / 32)) * 4)
$zeros = New-Object byte[] ($maskRow)
for ($y = 0; $y -lt $s; $y++) { $bw.Write($zeros, 0, $maskRow) }
$bw.Flush()
[void]$entries.Add([pscustomobject]@{ Size = $s; Data = $ms.ToArray() })
$bw.Dispose(); $ms.Dispose()
}
}
finally {
$src.Dispose()
}
# Assemble the .ico: ICONDIR header, then one ICONDIRENTRY per image, then data.
$out = New-Object System.IO.MemoryStream
$w = New-Object System.IO.BinaryWriter($out)
$w.Write([int16]0) # reserved
$w.Write([int16]1) # type = icon
$w.Write([int16]$entries.Count) # image count
$offset = 6 + 16 * $entries.Count
foreach ($e in $entries) {
$dim = if ($e.Size -ge 256) { 0 } else { $e.Size } # 0 means 256 in the dir
$w.Write([byte]$dim) # width
$w.Write([byte]$dim) # height
$w.Write([byte]0) # palette color count
$w.Write([byte]0) # reserved
$w.Write([int16]1) # color planes
$w.Write([int16]32) # bits per pixel
$w.Write([int]$e.Data.Length) # size of image data
$w.Write([int]$offset) # offset of image data
$offset += $e.Data.Length
}
foreach ($e in $entries) { $w.Write($e.Data, 0, $e.Data.Length) }
$w.Flush()
[System.IO.File]::WriteAllBytes($outIco, $out.ToArray())
$w.Dispose(); $out.Dispose()
Write-Output ("Wrote {0} ({1:N0} bytes, sizes: {2})" -f $outIco, (Get-Item $outIco).Length, ($sizes -join ', '))
+40
View File
@@ -0,0 +1,40 @@
#!/usr/bin/env node
/**
* @file Desktop dependency installer with actionable failure help.
*
* Thin wrapper around `npm install` (which still runs the `postinstall`
* `electron-builder install-app-deps` to rebuild native modules for Electron).
* On success it behaves exactly like a bare `npm install`. On failure — almost
* always the `better-sqlite3` native build — it prints the prerequisite
* guidance + the no-toolchain alternative commands, then exits non-zero so the
* normal command still fails loudly rather than silently leaving a half-set-up
* `node_modules`.
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
*/
const { spawnSync } = require("node:child_process");
const path = require("node:path");
const { printNativeDepHelp, hasBetterSqliteBinary } = require("./preflight");
const desktopRoot = path.resolve(__dirname, "..");
// On Windows `npm` is a `.cmd` shim that `spawnSync` can only launch via a
// shell; without this it fails with ENOENT. POSIX is unaffected.
const result = spawnSync("npm", ["install"], {
cwd: desktopRoot,
stdio: "inherit",
shell: process.platform === "win32",
});
// `npm install` failed outright (e.g. node-gyp could not find a compiler), or
// it "succeeded" but the native binary never landed (a prebuilt download was
// skipped). Either way the desktop app cannot boot — surface the fix and fail.
if (result.status !== 0) {
printNativeDepHelp("`npm install` failed while building the native better-sqlite3 module.");
process.exit(result.status || 1);
}
if (!hasBetterSqliteBinary()) {
printNativeDepHelp("Dependencies installed, but the better-sqlite3 native binary is missing.");
process.exit(1);
}
+48
View File
@@ -0,0 +1,48 @@
/**
* @file electron-builder afterSign hook for Apple notarization.
*
* This is opt-in: it only does anything when all three Apple credentials
* are present as environment variables. In every other case (local builds,
* fork CI without secrets) the hook is a no-op. That keeps the default
* `npm run dmg` working for contributors without an Apple Developer
* account while letting the project maintainer flip a switch later.
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
*/
exports.default = async function notarizeIfConfigured(context) {
const { electronPlatformName, appOutDir, packager } = context;
if (electronPlatformName !== "darwin") return;
const { APPLE_ID, APPLE_TEAM_ID, APPLE_APP_SPECIFIC_PASSWORD } = process.env;
if (!APPLE_ID || !APPLE_TEAM_ID || !APPLE_APP_SPECIFIC_PASSWORD) {
console.log("[notarize] Apple credentials not set — skipping notarization (ad-hoc only).");
return;
}
// Lazy-require: @electron/notarize is only needed when we actually notarize,
// so contributors without Apple credentials don't have to install it.
let notarize;
try {
({ notarize } = require("@electron/notarize"));
} catch {
console.log(
"[notarize] Apple credentials present but @electron/notarize is not installed. Run `npm install --save-dev @electron/notarize` in desktop/."
);
return;
}
const appName = packager.appInfo.productFilename;
const appPath = `${appOutDir}/${appName}.app`;
console.log(`[notarize] notarizing ${appPath}`);
await notarize({
tool: "notarytool",
appBundleId: packager.appInfo.id,
appPath,
appleId: APPLE_ID,
appleIdPassword: APPLE_APP_SPECIFIC_PASSWORD,
teamId: APPLE_TEAM_ID,
});
console.log("[notarize] done");
};
+100
View File
@@ -0,0 +1,100 @@
#!/usr/bin/env node
/**
* @file Pre-build guard.
*
* Ensures the desktop bundle has everything it needs before TypeScript
* compiles. Specifically:
* 1. The root repo's node_modules exists (Express + friends).
* 2. The client has been built (client/dist exists). In production mode the
* Express server serves the SPA from client/dist; if it's missing the
* DMG would ship a 404-only dashboard.
* 3. Asset PNGs exist (or we leave a clear warning — icons can be
* regenerated via scripts/build-icons.sh).
* 4. The desktop-local better-sqlite3 native binary matches this machine's
* CPU architecture. A prior `electron-builder --mac --x64/--arm64` build
* rebuilds it for the target arch; left mismatched it breaks `desktop:dev`
* and `desktop:test` with ERR_DLOPEN_FAILED. We rebuild it if so.
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
*/
const fs = require("node:fs");
const path = require("node:path");
const { spawnSync } = require("node:child_process");
const { hasBetterSqliteBinary, printNativeDepHelp } = require("./preflight");
const desktopRoot = path.resolve(__dirname, "..");
const repoRoot = path.resolve(__dirname, "..", "..");
const clientDist = path.join(repoRoot, "client", "dist");
const rootNodeModules = path.join(repoRoot, "node_modules");
const assets = path.join(desktopRoot, "assets");
function run(cmd, args, opts = {}) {
// On Windows `npm`/`npx` are `.cmd` shims that `spawnSync` can only launch
// through a shell; without this it fails with ENOENT. POSIX is unaffected.
const result = spawnSync(cmd, args, {
stdio: "inherit",
shell: process.platform === "win32",
...opts,
});
if (result.status !== 0) {
throw new Error(`${cmd} ${args.join(" ")} failed with exit ${result.status}`);
}
}
if (!fs.existsSync(rootNodeModules)) {
console.log("[prebuild] installing root dependencies…");
run("npm", ["ci"], { cwd: repoRoot });
}
if (!fs.existsSync(clientDist) || !fs.existsSync(path.join(clientDist, "index.html"))) {
console.log("[prebuild] building client (client/dist missing)…");
run("npm", ["ci"], { cwd: path.join(repoRoot, "client") });
run("npm", ["run", "build"], { cwd: repoRoot });
}
const trayIcon = path.join(assets, "tray-icon-Template.png");
if (!fs.existsSync(trayIcon)) {
console.warn(
"[prebuild] WARN: tray-icon-Template.png missing. Run `npm run build:icons` to regenerate from assets/icon.svg."
);
}
// Heal a better-sqlite3 native binary left built for the wrong CPU arch by a
// prior `electron-builder --mac --x64/--arm64` run. Without this, `desktop:dev`
// and `desktop:test` fail to load the module (ERR_DLOPEN_FAILED) until the
// contributor manually re-runs `npm run desktop:install`.
if (process.platform === "darwin") {
const bsNode = path.join(
desktopRoot,
"node_modules",
"better-sqlite3",
"build",
"Release",
"better_sqlite3.node"
);
if (fs.existsSync(bsNode)) {
const desc = spawnSync("file", ["-b", bsNode], { encoding: "utf8" }).stdout || "";
// A universal binary works on both arches; only act on a clear mismatch.
const universal = /universal/i.test(desc);
const wrongArch =
!universal &&
((process.arch === "arm64" && !/arm64/.test(desc)) ||
(process.arch === "x64" && !/x86_64/.test(desc)));
if (wrongArch) {
console.log(
"[prebuild] better-sqlite3 is built for the wrong CPU arch (a prior DMG build left it that way) — rebuilding for this machine…"
);
run("npx", ["electron-builder", "install-app-deps"], { cwd: desktopRoot });
}
}
}
// The embedded server `require`s better-sqlite3 at boot; without its native
// binary the desktop app dies with a fatal dialog after compiling cleanly.
// Catch it here (a build-time, copy-pasteable failure) rather than at runtime.
if (!hasBetterSqliteBinary()) {
printNativeDepHelp("The desktop-local better-sqlite3 native binary is missing.");
process.exit(1);
}
console.log("[prebuild] ok");
+82
View File
@@ -0,0 +1,82 @@
#!/usr/bin/env node
/**
* @file Shared native-dependency preflight checks + actionable failure help.
*
* The desktop shell embeds the dashboard server in-process, which `require`s
* the native `better-sqlite3` module rebuilt against Electron's Node ABI. That
* build is the single most common setup failure: it needs either a C++ toolchain
* (to compile from source) or a Node version new enough to have a prebuilt
* binary. When it's missing we want a clear, copy-pasteable message instead of a
* raw node-gyp stack trace or a runtime "Cannot find module" deep inside boot.
*
* This module is shared by `install.js` (wraps the dependency install) and
* `prebuild.js` (gates every `desktop:*` build/dev script) so both surfaces
* print the same guidance.
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
*/
const fs = require("node:fs");
const path = require("node:path");
const desktopRoot = path.resolve(__dirname, "..");
/** Absolute path to the compiled/prebuilt better-sqlite3 native binary. */
function betterSqliteBinary() {
return path.join(
desktopRoot,
"node_modules",
"better-sqlite3",
"build",
"Release",
"better_sqlite3.node"
);
}
/** True when the Electron-ABI better-sqlite3 binary is present on disk. */
function hasBetterSqliteBinary() {
return fs.existsSync(betterSqliteBinary());
}
/**
* Print prerequisite guidance and the no-toolchain alternative commands to
* stderr. Callers should `process.exit(1)` after this so the failing npm
* command exits non-zero (never leave the user thinking setup succeeded).
*/
function printNativeDepHelp(reason) {
const line = "─".repeat(74);
const out = (s) => process.stderr.write(s + "\n");
out("");
out(line);
out(" Claude Code Monitor — desktop native dependency setup did not complete");
out(line);
if (reason) {
out(` ${reason}`);
out("");
}
out(" The desktop app embeds the dashboard server, which needs the native");
out(" 'better-sqlite3' module built for Electron's Node ABI. This typically");
out(" fails for one of two reasons:");
out("");
out(" 1. No C++ build toolchain, so the module can't compile from source:");
out(' • Windows: install "Visual Studio Build Tools" with the');
out(' "Desktop development with C++" workload.');
out(" • macOS: xcode-select --install");
out(" • Linux: install build-essential + python3.");
out("");
out(" 2. Your Node.js is newer than any published better-sqlite3 prebuilt");
out(` binary (you are on Node ${process.version}). A Node LTS (20 or 22)`);
out(" ships prebuilt binaries and avoids the compile entirely.");
out("");
out(" Or skip the source build and fetch Electron's prebuilt binary directly");
out(" (no C++ toolchain needed):");
out("");
out(" cd desktop");
out(" npm install --ignore-scripts");
out(" node node_modules/electron/install.js");
out(" npx electron-builder install-app-deps");
out("");
out(line);
out("");
}
module.exports = { betterSqliteBinary, hasBetterSqliteBinary, printNativeDepHelp };