feat(v0.6): project card launchers — Claude/Codex/VS Code logos + active highlight

Replace each card's "+ start claude here" with a row of three brand-logo
launcher buttons (logo + caption, touch-friendly, tooltips):

- Claude (coral burst)  → new terminal tab running `claude`
- Codex  (OpenAI mark)  → new terminal tab running `codex`
- VS Code (blue ribbon) → opens the repo in VS Code ON THE HOST

VS Code: new guarded POST /open-in-editor runs `<EDITOR_CMD> <path>` (default
`code`) on the host via execFile (no shell); path validated as an existing
absolute dir; Origin guard (CSRF) like the DELETE routes. EDITOR_CMD config added.

Active-session highlight (user request): when a project has a running Claude
session (Claude-hook status != unknown — plain shells/codex stay unknown), the
Claude logo gets a coral ring + tint, with a count badge when more than one.

New: public/icons.ts (simple-icons SVGs, fill=currentColor), src/http/editor.ts.
onOpenProject gains an optional cmd; makeProjectCard exported for tests.

Tests +16 (431 total): editor validation/launch, EDITOR_CMD config, launcher
row (3 buttons, claude/codex commands), and the active-Claude highlight + badge.
Verified: coverage >=80x4, build OK, both typechecks clean; in-browser the 3
logos render with brand colours, and POST /open-in-editor actually launched
VS Code on the host (403 no-Origin / 400 bad-path / 204 valid-dir all correct).
This commit is contained in:
Yaojia Wang
2026-06-30 12:46:12 +02:00
parent 3323bc81c0
commit a390130205
12 changed files with 447 additions and 42 deletions

View File

@@ -34,6 +34,7 @@ const DEFAULT_REAP_INTERVAL_MS = 60_000 // idle reaper sweeps every minute
const DEFAULT_PREVIEW_BYTES = 24 * 1024 // manage-page preview: tail of scrollback
const DEFAULT_PROJECT_SCAN_DEPTH = 4 // v0.6: how deep to scan roots for .git
const DEFAULT_PROJECT_SCAN_TTL_MS = 10_000 // v0.6: /projects discovery cache TTL
const DEFAULT_EDITOR_CMD = 'code' // v0.6: POST /open-in-editor launches this on the host
// ── helpers ───────────────────────────────────────────────────────────────────
@@ -242,6 +243,7 @@ export function loadConfig(env: EnvLike): Config {
DEFAULT_PROJECT_SCAN_TTL_MS,
)
const projectDirtyCheck = parseBool(env['PROJECT_DIRTY_CHECK'], true)
const editorCmd = env['EDITOR_CMD']?.trim() || DEFAULT_EDITOR_CMD
return Object.freeze({
port,
@@ -263,5 +265,6 @@ export function loadConfig(env: EnvLike): Config {
projectScanDepth,
projectScanTtlMs,
projectDirtyCheck,
editorCmd,
} satisfies Config)
}