Commit Graph

86 Commits

Author SHA1 Message Date
Yaojia Wang
a09c131539 fix(relay-auth): close 5 pre-production security findings (F1–F5)
F1 (HIGH): bind step-up freshness to the required method (stepUpMethod) so a
  login-time or weaker (TOTP) factor can no longer satisfy a passkey requirement.
F2 (HIGH): make the step-up gate host-driven and fail-closed on connect+reattach
  (deny when policy.required and the principal is missing/stale/wrong-method).
F3 (MED): bind the WebAuthn assertion to the stored credential (credentialId
  cross-check + forward publicKey/credentialId to the verifier).
F4 (LOW): make verifyDpopProof totally fail-safe (no unhandled throw), wrap the
  authz boundary into a clean audited 401, and validate cnfJkt format at issue.
F5 (LOW): return newSignCount from finishAuthentication so the caller can persist
  the advanced counter (WebAuthn clone detection).

All fixes ship regression tests. relay-auth: 122 tests green, tsc clean.
2026-07-02 16:41:18 +02:00
Yaojia Wang
1529d2c94c Merge feat/desktop-electron: Electron all-in-one desktop shell (Mac/Windows)
Some checks failed
relay-tripwire / cross-tenant-tripwire (push) Has been cancelled
2026-07-02 06:13:29 +02:00
Yaojia Wang
cf8cfccab4 feat(desktop): Electron all-in-one desktop shell (Mac/Windows) embedding the server
Add a `desktop/` Electron app that embeds the existing Node server + node-pty
(all-in-one): the window loads http://127.0.0.1:<port>/ and reuses the frontend
unchanged; other LAN devices can still connect. The server needs zero changes —
startServer(cfg)/loadConfig already support programmatic embedding.

- Pure, unit-tested modules: port, shell, server-config, deep-link, notify-policy,
  notifications, live-poll, prefs, settings-store (94 tests; desktop/src ~97% cov)
- Electron glue: main/window/tray/menu/preload/embedded-server/logger
  (hardened: contextIsolation, sandbox, deny foreign-origin navigation)
- Native value: OS notifications driven by /live-sessions status, tray, deep links
- Packaging (electron-builder -> arm64 .dmg): ships dist/ + public/ + node_modules
  on-disk under Resources so the server resolves its deps from /Applications;
  node-pty rebuilt for the Electron ABI
- Docs: docs/DESKTOP_PLAN.md; PROGRESS_LOG updated

Verified: desktop tsc clean; 1401 tests pass; coverage >=80% (desktop/src 97/95/100/97);
.dmg built and launch-tested on arm64 (server boots, UI serves 200, node-pty loads).
2026-07-02 06:13:17 +02:00
Yaojia Wang
2af57e6686 feat(relay): rendezvous-relay service — 7 packages + plans (contracts/transport/agent/control-plane/e2e/auth/web)
Multi-tenant reverse-tunnel service ("ngrok for Claude Code" with E2E): a
host-agent dials OUT to an operator-run relay; external devices reach the host
THROUGH the relay, routed by per-tenant subdomain, forwarding ciphertext only
(the relay never sees plaintext). Lets a customer reach their own self-hosted
web-terminal from anywhere with zero networking setup.

Packages — all tsc-strict + vitest green (656 tests), cross-package integration verified:
- relay-contracts: frozen shared contracts (mux frame codec, data model,
  capability token, E2E envelope, pairing) — the src/types.ts analog
- term-relay:   native WS mux + stateless data plane (subdomain routing, ciphertext forward)
- agent:        host-agent (pairing, per-host Ed25519 + mTLS dial-out, forwards to 127.0.0.1:3000)
- control-plane: accounts/hosts registry, pairing-code flow, routing table, provisioning
- relay-e2e:    browser<->agent E2E (X25519 ECDH through relay, AEAD, anti-replay, recoverable replay key)
- relay-auth:   Passkey/WebAuthn, capability tokens, per-host certs, deny-by-default tenant isolation
- relay-web:    browser login + Web Crypto E2E + client-side preview rendering

Security invariants INV1-15 enforced; cross-tenant isolation CI tripwire live
(.github/workflows/relay-tripwire.yml). Design + implementation-level plans in
docs/PLAN_RELAY_*.md and docs/EXPLORE_RELAY_SERVICE.md.

NOTE: generated autonomously per the reviewed plans. The security-critical
packages (relay-e2e, relay-auth) REQUIRE expert security audit before any real
deployment — passing tests prove self-consistency, not resistance to attackers.
Base app (src/, public/) unchanged; concurrent desktop work left uncommitted.
2026-07-02 06:10:16 +02:00
Yaojia Wang
e4c327e25e feat: voice command mapping — context-gated approve/reject over voice
While a tool-permission gate is held on the active tab, spoken confirm
phrases resolve it via the existing approve/reject WS channel — no server
change (byte-shuttle intact). Everything else stays ordinary dictation.

Safety: whole-utterance-exact matching + leading-negation guard + reject
precedence + confidence gate (approve only) + a cancellable 1.5s confirm
window whose commit re-validates gate identity/epoch/connectivity (TOCTOU
guard) + stale-gate epoch bound at PTT-start.

New pure modules public/voice-commands.ts and public/voice-confirm.ts at
100% coverage; wiring in voice/terminal-session/tabs/main. 1307 tests pass,
typecheck clean, build:web OK. Independent code + security reviews flagged a
confirm-window TOCTOU and an unwired cancel path — both fixed and covered.

Plan: docs/PLAN_VOICE_COMMANDS.md.
2026-07-01 10:38:32 +02:00
Yaojia Wang
03612323c0 feat(v0.6): auto-group Projects by namespace + Active-now band + cross-device prefs
The flat Projects grid was overwhelming at 50-100 repos. Explored the UX with
4 parallel agents (auto-grouping / manual folders / density-segmentation /
competitive research); all converged on namespace auto-grouping as the primary
fix — the dotted repo names (Billo.Platform.*, Billo.Infrastructure.*) already
encode the hierarchy, so it's a string-split, not a taxonomy the user builds.

Frontend:
- groupProjects(): depth-2 namespace grouping, >=2-member threshold (singletons
  fall to "Other"), degrade-to-flat when repos share no prefix, running projects
  duplicated into a pinned "Active now" section.
- displayLabel(): cards show the namespace tail (Payment, not
  Billo.Platform.Payment); full name in Active/Other.
- Collapsible sticky group headers with an "N active" badge so a collapsed
  section never hides running work; search force-expands matching groups.
- public/prefs.ts: server-first load with a localStorage offline mirror, migrates
  the legacy per-device proj-favs.

Backend (server-side prefs so favourites + collapse-state follow the user across
devices — localStorage was per-device, which broke the multi-device premise):
- src/http/prefs-store.ts: ~/.web-terminal-prefs.json store (mirrors push-store),
  every field bounded + sanitized.
- GET /prefs (read-only) + PUT /prefs (Origin-guarded); UiPrefs type +
  PREFS_STORE_PATH config.

Tests: +12 prefs-store, +21 groupProjects/displayLabel. Full suite 1227 passed.
Verified live: grouping renders, collapse persists to the server and survives a
full page reload (cross-device sync proven end-to-end).
2026-07-01 04:25:57 +02:00
Yaojia Wang
7b3ba8491a docs: rewrite README to cover all features (v0.1–v0.7) 2026-06-30 21:26:24 +02:00
Yaojia Wang
7fd2ef3fc8 polish(v0.7): styled hover tooltips on timeline + push toggle (explain the icon)
Give the two tab-bar line-icon buttons the same styled data-tip hover tooltip as
the toolbar icons, so their meaning is clear on hover (native title was slow/ugly):
- timeline → "Activity timeline"
- push toggle → "Notifications off — tap to turn on" / "…on — tap to turn off"
Left-anchored (they sit on the left of the tab bar). 158 push+tabs tests green.
2026-06-30 19:28:15 +02:00
Yaojia Wang
b273b61795 polish(v0.7): restyle timeline + push toggle to match the .toolbtn line-icon look
The icons were swapped to lucide line icons, but the buttons kept a boxed/light
background that clashed with the minimal toolbar icons. CSS-only fix: render
.tab-timeline + .push-toggle-btn as transparent, monochrome 18px line-icon
buttons (text-dim, accent on hover/active) — identical to .toolbtn — overriding
the earlier boxed default. Markup unchanged (tests stay green: 158 push+tabs).
2026-06-30 19:18:38 +02:00
Yaojia Wang
e554c053ee polish(v0.7): replace timeline 📜 + push 🔔 emoji with themed line icons
Swap the two v0.7 emoji buttons for lucide line icons (stroke=currentColor) so
they take the Amber theme colour like the toolbar icons:
- timeline toggle (tabs.ts) → scroll-text icon
- push toggle + disabled bell (push.ts) → bell icon, keeping the On/Off label
- icons.ts: ICON_TIMELINE, ICON_BELL; style.css sizes the inline svgs (16px)

Frontend-only. web tsc + build:web clean, 102 push tests green, bundle emoji-free.
2026-06-30 18:54:11 +02:00
Yaojia Wang
d6809c65c4 feat(v0.7): Walk-away Workbench (Band A + B) — multi-agent parallel build
Implements docs/PLAN_WALKAWAY_WORKBENCH.md (27 tasks, waves R0→W0→W1×14→W2→W3→W4)
via module-builder agents. 23 tasks built, 0 blocked.

Band A (finish the walk-away loop): A1 Web Push + lock-screen approve/deny
(web-push dep), A2 voice dictation, A3 quick-reply chips + saved-prompt palette,
A4 activity timeline, A5 stuck/idle alert.
Band B (workbench above the terminal): B1 read-only git diff viewer, B2 statusLine
telemetry → per-tab cost/context/PR gauges, B3 create git worktrees from the UI,
B4 plan-mode / permission-mode relay.

New: src/push/* (subscription store + VAPID push), src/http/{diff,statusline}.ts,
src/session/timeline.ts, public/{diff,timeline,quickreply,push-ui,...}.ts, sw-push,
statusLine script; extends hook intake, manager, server routes (Origin/CSRF guards
+ per-IP rate limits on state-changing ones; loopback-only ingest), terminal-session,
tabs, projects detail, service worker, setup-hooks (statusLine + ntfy bridge).

Orchestrator reconciled a W0 contract gap: added the 21 v0.7 Config fields to the
Config interface in types.ts (T-types had left them only in config.ts's return).

Verified: both tsc clean, full vitest + coverage 91.4/84.1/92.2/93.4 (≥80×4),
build:web OK. W4 review: no CRITICAL/HIGH; all security checks pass. Follow-ups
(non-blocking): move approve.mode validation into parseClientMessage, drop CSP
ws:/wss: wildcard, validate worktree base ref, +2 targeted tests.
2026-06-30 17:42:18 +02:00
Yaojia Wang
4f1d3ebc6b docs(v0.7): PRD + implementation plan for Walk-away Workbench (Band A + B)
Authored via multi-agent orchestration (PRD: Band A ∥ Band B → architect
review → integrate; Plan: frontend ∥ backend ∥ security → review → integrate).

- docs/FEATURE_WALKAWAY_WORKBENCH.md — PRD for 9 features:
  Band A (push+lock-screen approve, voice, quick-reply chips, activity
  timeline, stuck/idle alert) and Band B (read-only git diff, statusLine
  cost/context/PR gauges, UI worktree-create, plan-mode relay).
- docs/PLAN_WALKAWAY_WORKBENCH.md — 27 tasks (waves W-1..W4), disjoint Owns,
  contract resolutions, 30-item security checklist, dispatch schedule, AC map.

Planning only — no source changed.
2026-06-30 15:51:58 +02:00
Yaojia Wang
88b960bac5 feat(v0.6): show CLAUDE.md on the project detail page + /init generate/update button
The detail page now reads <repo>/CLAUDE.md and shows it in a scrollable
monospace box, with one smart button: "↻ Update" when it exists / " Generate"
when it doesn't. The button opens an interactive Claude session in the repo
running /init (claude "/init") so you review what it writes — reuses the project
launcher, no new backend orchestration, stays a byte-shuttle.

- ProjectDetail += hasClaudeMd / claudeMd; buildProjectDetail reads CLAUDE.md
  (truncated 64KB, best-effort, in the existing Promise.all)
- renderProjectDetail: CLAUDE.md section (content box or empty state) + button
- style.css: .proj-section-row / .proj-claudemd-btn / .proj-claudemd

Tests +5 (458): backend read present/absent; frontend content+Update,
empty+Generate, button opens `claude "/init"`. Verified: tsc clean, full suite
green, build OK, curl returns the content, in-browser shows the box + button.
2026-06-30 14:51:42 +02:00
Yaojia Wang
67b0a43b39 polish(v0.6): replace toolbar emoji with themed line icons + hover tooltips
The glossy emoji (🔍⚙▦🕘🔗📱) clashed with the Amber dark theme. Swap them
for clean lucide line icons (MIT, stroke=currentColor) so they take the theme
colour — muted by default, Amber on hover — and add a styled CSS hover tooltip
(data-tip, right-anchored so it never clips the edge) showing what each does.

- public/icons.ts: 7 line icons (search/settings/dashboard/history/keyboard/
  share/device), script-generated from lucide
- 7 toolbar modules: innerHTML = ICON_* + data-tip + aria-label
- style.css .toolbtn: 18px svg, --text-dim → --accent on hover, [data-tip] tooltip

Frontend-only. web typecheck + build clean; bundle has 0 toolbar emoji; verified
in-browser: line icons in theme colour, amber + tooltip on hover.
2026-06-30 14:36:26 +02:00
Yaojia Wang
59de784c8a polish(v0.6): make "New session" a grid tile instead of a heavy header button
The big solid-amber "+ New session" button clashed with the refined segmented
control. Replace it with a dashed "+" tile that leads the session grid (same
size as the session cards, re-prepended each refresh), so it reads as a natural
"create" cell. Header is now just the title + Sessions/Projects toggle; the
redundant empty-state message is gone (the tile is the CTA).

Frontend-only (launcher.ts + style.css). Verified: web typecheck + build clean,
preview-grid tests green, in-browser the grid leads with the New-session tile
followed by the session cards (Open/Kill).
2026-06-30 14:25:56 +02:00
Yaojia Wang
bf5241e87b feat(v0.6): remove standalone Manage page — manage sessions on the Sessions view
The Sessions chooser already shows every running session with a live thumbnail
and Open + New session, so fold delete into it and drop the separate page:

- add a Kill ✕ button to each session card (DELETE /live-sessions/:id + refresh),
  reusing makePreviewCard's extraActions and the existing .mg-kill style
- remove public/manage.html + public/manage.ts and the esbuild manage entry
- remove the 🗂 toolbar button (main.ts) and the 🗂 Manage header link (launcher.ts)

Backend DELETE/preview routes stay (used by the launcher kill, thumbnails, and
the project pages). Frontend + build-config only.

Verified: full suite 453 green, web typecheck clean, build:web emits only main
(no manage.* artifacts), and in-browser the Sessions cards have Open + Kill (2→1
on kill), no Manage entry, and GET /manage.html → 404.
2026-06-30 14:08:00 +02:00
Yaojia Wang
46698b8b5e feat(v0.6): kill a session directly from the Projects grid cards
The detail view could kill sessions, but the grid card's session rows were
enter-only. Add a ✕ kill button to each card session row (stopPropagation so it
doesn't also enter the session); makeSessionRow gains an optional onKill,
makeProjectCard an optional onKillSession, and mountProjects wires it to
DELETE /live-sessions/:id followed by a refresh.

Frontend-only. projects-panel tests +1 (51): ✕ present only with the callback,
click kills the right id without entering. Verified in-browser: a card with 2
sessions dropped to 1 after clicking ✕.
2026-06-30 13:35:53 +02:00
Yaojia Wang
99bc6fd9f6 feat(v0.6): per-project detail view — branch/worktrees + active sessions
Clicking a project's name opens an in-app detail view (back button returns to
the grid) with:
- header: name, path, branch, dirty
- Branch / Worktrees: each `git worktree list` entry (branch or detached@head,
  main/current/locked/prunable tags, path) — a single-worktree repo shows its
  branch; a multi-worktree repo shows them all
- Active sessions (N): detailed rows (status, devices watching, started-ago)
  with Open + Kill
- the Claude/Codex/VS Code launcher row

Backend: src/http/worktrees.ts (parseWorktrees pure + listWorktrees via
execFile, no shell, best-effort); buildProjectDetail(cfg, path, liveSessions)
in projects.ts (validates absolute existing dir, reuses branch/dirty/session
helpers, uncached — detail is on-demand and wants fresh state); read-only
GET /projects/detail?path= (no Origin guard, like /projects; 400 missing /
404 invalid). Types: WorktreeInfo, ProjectDetail.

Frontend: detail view + navigation in projects.ts (grid<->detail), project
name becomes a link, Kill via DELETE /live-sessions/:id.

Tests +21 (452 total): worktree parser, buildProjectDetail (validation,
non-git, session merge, real git-init worktree), renderProjectDetail.
worktrees.ts 100% / projects.ts 93.8% cov. Verified in-browser: opened a
Claude session, then the detail showed its branch+worktree and the live
session with Open/Kill.
2026-06-30 13:29:41 +02:00
Yaojia Wang
a390130205 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).
2026-06-30 12:46:12 +02:00
Yaojia Wang
3323bc81c0 polish(v0.6): put Sessions/Projects toggle on the filter row (kill whitespace)
The toggle sat on its own dedicated row, leaving a large empty band above the
project grid. On desktop (>=769px) float it top-right on the SAME row as the
filter box / launcher header: panels fill from the top (inset 0), launcher-head
reserves right padding for the pill. Mobile keeps the centred stacked toggle to
avoid overlapping the full-width filter on narrow screens. Also reclaim the
hidden key-bar's space on the home view (body.home-open #term { bottom: 0 }).

CSS-only (style.css is served directly). Verified in-browser: toggle and filter
share a row on Projects, no overlap with New/Manage on Sessions.
2026-06-30 12:03:30 +02:00
Yaojia Wang
32fb4b09b1 polish(v0.6): redesign Sessions/Projects toggle + hide keybar on home
- Segmented control: replace the two free-standing bordered boxes with a
  single rounded track holding two pill buttons (iOS-style), active segment
  filled Amber; centred via fit-content + margin auto. Panel top-offset 44→68px.
- Hide the bottom key-bar on the home chooser (it only targets an active
  terminal): updateHomeView toggles body.home-open; CSS hides #keybar.

Frontend-only. 415 tests green; both typechecks clean; verified in-browser
(toggle pill renders, keybar hidden on home, reappears when a tab opens).
2026-06-30 11:51:47 +02:00
Yaojia Wang
985ff8a178 feat(v0.6): add Home (⌂) button to reach Sessions/Projects from a session
Once a project/session tab was open, the home chooser (Sessions/Projects
segmented control) was hidden and only reachable by closing every tab — no
way back to the Projects view to open another project.

Add a ⌂ Home button to the tab bar that overlays the chooser on top of the
open tabs (homeForced flag; updateHomeView shows home when no tab OR forced,
hiding the terminal panes). Clicking ⌂ again, clicking a tab (activate resets
homeForced), or opening another session/project dismisses the overlay.
Closing the last tab resets the flag. The button highlights while active.

Tests: +3 (tabs.test.ts). Full suite 415 green; both typechecks clean;
verified in-browser (open tab → ⌂ → Projects panel reachable → ⌂ → terminal).
2026-06-30 11:39:38 +02:00
Yaojia Wang
7b4adf5072 feat(v0.6): Project Manager — repo discovery + Projects panel + tab wiring
Home screen gains a Sessions↔Projects segmented control. The Projects view
discovers the host's git repos and, on click, opens a tab named after the
repo, spawned in the repo dir, auto-running `claude` (1:N sessions per project).

Backend (P2/P4):
- src/http/projects.ts — parseGitHead + buildProjects(cfg, liveSessions):
  depth-capped BFS for .git (skips node_modules/dotdirs/symlinks, stops
  descending at a repo), .git/HEAD branch parse, rate-limited `git status`
  dirty check (execFile, no shell, 2s timeout, concurrency 8), merge of
  ~/.claude/projects cwds (reuses history.listSessions), cwd-prefix session
  merge (fresh each call), TTL discovery cache with in-flight dedup.
- src/server.ts — read-only GET /projects (no Origin guard, []-fallback).

Frontend (P5/P6):
- public/projects.ts — mountProjects: 1:N cards (branch chip, dirty dot,
  session rows, "+ start claude here"), live search, localStorage favourites;
  pure helpers extracted for unit tests.
- public/tabs.ts — openProject (#n suffix for dup names, sends 'claude\r'),
  countOpenWithTitlePrefix, Sessions↔Projects home toggle (updateHomeView).
- public/style.css — Projects panel + .home-seg control styles.

Config (P3, hardened): PROJECT_ROOTS/SCAN_DEPTH/SCAN_TTL/DIRTY_CHECK already
added; this commit adds fail-fast rejection of relative PROJECT_ROOTS.

Review hardening (4 confirmed HIGH + boundary validation):
- carriage-return fix so claude auto-executes (A3); seg-control z-order;
  parallelised history merge; concurrent-cache-miss dedup.
- normalizeProject guards the /projects response; getFavs filters non-strings.

Tests: +57 (398 total). Backend src/http/projects.ts 93.4%, config.ts 98%.
Verified: both typechecks clean, full suite green, build:web OK, coverage
≥80×4, real-browser smoke (83 repos, branch/dirty correct, click→claude tab).
2026-06-30 11:04:49 +02:00
Yaojia Wang
dc5d073374 chore(v0.6): checkpoint foundation — Amber theme, P1 types, P3 config, design docs
Prior-session prep for the v0.6 Project Manager, checkpointed on the
v0.6-projects branch as a clean base for the parallel builders:

- P1 (src/types.ts): ProjectInfo + ProjectSessionRef contracts; Config
  gains projectRoots/projectScanDepth/projectScanTtlMs/projectDirtyCheck.
- P3 (src/config.ts): parseBool + parseProjectRoots helpers; parse the
  4 PROJECT_* env vars with defaults ([homeDir]/4/10000/true).
- UI: public/style.css indigo -> Amber (#e3a64a) accent theme.
- Docs: FEATURE_PROJECT_MANAGER.md (1:N session design), THEME.md,
  docs/mockups/ (final-amber.png is the locked visual).

Typecheck (backend + web) green. No behavior change to runtime code yet;
P2/P4/P5/P6 implement the feature on top of this.
2026-06-30 10:05:40 +02:00
Yaojia Wang
f9964a517d merge: review-report fixes (security/architecture/quality/tests) 2026-06-21 04:48:33 +02:00
Yaojia Wang
d22dcd24f7 fix: address review report across security, architecture, quality, tests
Implements the fixes from docs/REVIEW_REPORT.md (4-agent parallel review).
typecheck clean; 341 tests pass (16 files, +113); build:web ok; coverage
thresholds (80%) enforced in vitest.config.ts.

Critical:
- multi-device approval race: release held approval only when the last
  client detaches (closing one mirror no longer cancels another's prompt)
- unbounded session creation (DoS): Config.maxSessions cap (env MAX_SESSIONS),
  enforced in manager via the existing M4 exit(-1) path
- signal-handler leak: named SIGINT/SIGTERM/uncaughtException refs removed in close()
- terminal-session initialInput timer tracked + cleared on dispose
- tabs.addEntry null-as-cast type hole removed (build session before entry)

Should-fix:
- security-headers middleware + Origin/CSRF guard on DELETE /live-sessions[/:id]
- history.ts converted to fs/promises (async /sessions handler)
- removed dead clientDims map + blur protocol message end-to-end
- per-connection WS message rate limit (Config.maxMsgsPerSec)
- /sessions behavior kept; documented as accepted LAN risk (TECH_DOC §7)

Tests:
- new tmux / preview-grid / terminal-session (jsdom) / tabs (jsdom) suites
- extended history/config/manager/integration coverage incl. regressions

Hygiene:
- parsePositiveInt -> parseNonNegativeInt; ALLOWED_ORIGINS scheme validation
- log-injection sanitize; isLoopback handles 127.0.0.0/8 + IPv4-mapped
- operational constants moved into Config
- extracted public/preview-grid.ts (DRY launcher/manage)
- doc sweeps: ARCHITECTURE §8 runtime-handle exception, stale comments
2026-06-20 18:27:45 +02:00
Yaojia Wang
97d57326fd docs: record v0.5 home session chooser (no auto tabs) 2026-06-19 14:52:33 +02:00
Yaojia Wang
50406caab0 feat(v0.5): home session chooser — no auto-created/restored tabs
Opening the app no longer auto-creates a blank tab or auto-restores/discovers
sessions as tabs. Instead it lands on a launcher (session chooser): a grid of the
host's running sessions as live preview thumbnails — pick one to open (replays its
full scrollback), or '+ New session'. Closing the last tab returns to the chooser.

- public/launcher.ts: mountLauncher() renders the chooser into the terminal area
  (live xterm thumbnails via /live-sessions + /preview, 5s refresh), New session +
  Manage link
- tabs.ts: drop restore()/syncLiveSessions() auto-tab behavior; constructor lands
  on the launcher; rebuild() toggles launcher when tab count is 0; closeTab no
  longer spawns a blank tab
- style.css: .launcher chrome (reuses the .mg-* card/grid/thumbnail styles)

Verified: fresh load → 0 tabs, chooser with thumbnails; Open → tab + chooser
hides; close last tab → chooser returns. 228 tests green, tsc + build clean.
2026-06-19 14:52:05 +02:00
Yaojia Wang
9466bf4b6e docs: record manage-page live preview thumbnail grid 2026-06-19 11:37:57 +02:00
Yaojia Wang
787f806e02 feat(v0.4): session manager grid with live preview thumbnails
The flat session list didn't tell you what each session was doing. Rebuild
/manage.html as a full-page grid where each card renders a LIVE preview
thumbnail — a read-only xterm showing the session's actual current screen,
scaled down like a screenshot — so you can recognize each at a glance.

- RingBuffer.tail(maxBytes): newest scrollback on chunk boundaries (no ANSI/UTF-8
  split); GET /live-sessions/:id/preview returns {cols,rows,data} (no attach, so
  it doesn't inflate watcher counts or keep sessions alive)
- manage.ts: card grid; per card a scaled read-only Terminal renders CLEAR+tail,
  auto-refresh every 4s, click thumbnail/Open to join, Kill / bulk kill
- bundles xterm into manage.js (+manage.css linked in manage.html)
- ring-buffer tail tests

Verified: 3 sessions rendered as recognizable thumbnails (top / git log / ls),
colored, with status/age; click opens. 228 tests green, tsc clean.
2026-06-19 11:37:32 +02:00
Yaojia Wang
199e29d15b docs: record latest-writer-wins PTY sizing (full-screen per device) 2026-06-19 11:17:26 +02:00
Yaojia Wang
edbfc62f7f fix(v0.4): full-screen per device — latest-writer-wins PTY sizing
A shared PTY can only be one size; min-sizing clamped the shared session to the
SMALLEST viewer, so a wide desktop got letterboxed when an iPad was also attached
(iPad looked full-screen, desktop did not).

Switch to latest-writer-wins: the device that most recently fit/focused drives the
PTY size, so whichever device you're actively using is full-screen.
- session.ts: setClientDims resizes the PTY directly (drop min across clients);
  attach/detach/blur never resize (the active device keeps its size)
- frontend: TerminalSession.refit() force-resends dims; window 'focus' /
  visibilitychange re-assert the active tab's size, so switching devices reclaims
  full-screen
- tests updated for latest-wins (incl. join/hidden/blur don't resize)

Verified (two ws clients): 200x50 → iPad 100x40 → desktop refit 200x50 → iPad blur
keeps 200x50. 225 tests green.
2026-06-19 11:16:59 +02:00
Yaojia Wang
d782ec8488 docs: record mirror size-clamp fix + 🗂 session manager page 2026-06-19 11:05:28 +02:00
Yaojia Wang
021a514b2d fix(v0.4): mirror size-clamp bug + session manager page
The mirror looked broken because a backgrounded tab on one device still pinned
the shared PTY to its (default 80x24) size — so the device actively viewing got
a cramped terminal.

Fix: only an ACTIVELY-VIEWING client votes on PTY size.
- attachWs no longer seeds a default size vote (a join may be a hidden mirror)
- new 'blur' client message + clearClientDims(): a tab going hidden withdraws its
  size vote (still mirrors output); show() re-casts it
- PTY size = min cols/rows across clients that have actually reported dims
- session/protocol tests cover hidden-mirror-doesn't-clamp + blur

Session manager (separate page, for the 'too many sessions' problem):
- GET stays; add DELETE /live-sessions/:id and DELETE /live-sessions[?detached=1]
- manager.killById(); LiveSessionInfo gains cols/rows
- public/manage.html + manage.ts: list/open/kill sessions, kill-all / kill-detached,
  auto-refresh; 🗂 toolbar button; bundled as a 2nd esbuild entry

Verified: two concurrent clients mirror output + shared input; manage page
lists/kills (3→2→0). 225 tests green, tsc clean.
2026-06-19 11:04:38 +02:00
Yaojia Wang
22210fadbc docs: record v0.4 multi-device session sharing (relaxes invariant #5) 2026-06-19 10:31:28 +02:00
Yaojia Wang
24b3cbd507 feat(v0.4): multi-device discovery + share link (frontend)
- tabs.ts: on open, syncLiveSessions() fetches /live-sessions and adds a tab for
  each host session not already open → any device sees & mirrors everything
  running on the host. Empty storage + no live sessions → one fresh tab.
  activeSessionId() + openSession(id) helpers.
- share.ts: 🔗 toolbar button → QR + <origin>/?join=<id> for the active session.
- main.ts: ?join=<id> on load opens/focuses that shared session, then strips the
  param. Mount share button (toolbar: 🔍 ⚙ ▦ 🕘🔗 📱).
- style.css: #sharemodal reuses the QR card/backdrop.

Verified in-browser: fresh device auto-discovered the host session and replayed
its scrollback; two concurrent clients (browser + ws) both received live output
(clientCount=2); share modal shows the join URL/QR.
2026-06-19 10:30:40 +02:00
Yaojia Wang
0718b92267 feat(v0.4): multi-device session sharing (backend)
Relax the one-WS-per-session invariant (#5) so multiple devices mirror the
same live terminal (everyone sees output, anyone can type — tmux-style).

- types.ts: Session.attachedWs → clients Set + clientDims Map; add cwd;
  SessionManager.list() + LiveSessionInfo
- session.ts: broadcast() output/exit to all clients; attachWs JOINS (no kick,
  takes dims); detachWs removes one client, stamps detachedAt only when the last
  leaves; setClientDims resizes PTY to the MIN cols/rows across clients
- manager.ts: join instead of kick; broadcast status; onExit drops only when a
  client was attached; list() for discovery
- server.ts: GET /live-sessions; resize → per-client setClientDims; close →
  detach one client; permission gate uses clients.size
- tests: rewrote session/manager tests for multi-client (join/broadcast/min-dims/
  last-detach) + list(); 222 pass, tsc clean
2026-06-19 10:24:34 +02:00
Yaojia Wang
bae4d72929 docs: log UI refresh + keybar captions + ⌨ cheat-sheet 2026-06-19 09:42:39 +02:00
Yaojia Wang
cc3e7b8450 feat: keybar function captions + Ctrl+R/L/D, plus ⌨ shortcut cheat-sheet
keybar:
- add Ctrl+R (搜历史/\x12), Ctrl+L (重绘/\x0c), Ctrl+D (退出/\x04)
- each key now shows a short function caption under the glyph (self-documenting
  on touch where tooltips don't appear); 17 keys total
- test/keybar.test.ts updated to 17 keys

cheat-sheet (new public/shortcuts.ts):
- ⌨ toolbar button opens a grouped reference of common Claude Code shortcuts
  (general / editing / multiline / macOS / prefixes), each labeled with what it
  does; keys also on the bottom bar are tagged 键栏
- Esc / click-outside to close; styled card with sticky header + accent tags

216 tests green; tsc + build clean.
2026-06-19 09:41:58 +02:00
Yaojia Wang
524f2073a0 style: modern dark UI refresh
- design-token palette (:root vars): deeper #0e0f13 base, indigo #7c8cff accent,
  layered surfaces, softer borders, shadow + radius scale
- tabs as floating rounded chips (active = elevated surface); refined dots
- key bar as rounded chips with gaps; Esc gets the accent tint
- toolbar icon buttons with rounded hover
- overlays (history/dashboard/qr/search/settings) get backdrop-blur + card +
  shadow; indigo primary buttons; consistent inputs with accent focus ring
- chrome uses a system UI font (terminal stays monospace)
- xterm dark theme + meta theme-color + manifest + app icon synced to the
  palette (indigo-gradient icon, accent cursor)
216 tests green; tsc + build clean.
2026-06-18 09:08:02 +02:00
Yaojia Wang
e1a9439cce merge: O2 resume-past-sessions + tech-debt cleanup + cleaner terminal
- O2: /sessions reads ~/.claude/projects JSONL; 🕘 panel resumes a past session
  in a new tab (claude --resume <id>) in its project dir
- chore: *.css module decl removes the last @ts-ignore
- ui: drop in-terminal Connecting/Connected lines (tab dot shows it)
216 tests green.
2026-06-18 08:10:33 +02:00
Yaojia Wang
c5a70a2f36 docs: log O2 done + tech-debt cleared; only O1 + F4/F7(device) remain 2026-06-18 08:10:33 +02:00
Yaojia Wang
99bae333f3 chore: clear tech-debt — *.css module decl removes the @ts-ignore; tick F-table
- public/css.d.ts: declare module '*.css' so tsc resolves CSS side-effect
  imports; removed the @ts-ignore in main.ts
- PROGRESS_LOG F1-F9 table ticked to match what's actually verified (F4/F7 still
  need a physical device)
2026-06-18 08:06:03 +02:00
Yaojia Wang
e36ca272ed feat(v0.3): O2 — resume past Claude sessions
- src/http/history.ts: listSessions() reads ~/.claude/projects/*/*.jsonl
  (mtime-sorted, top 50), parseSessionMeta() extracts cwd + first user prompt
  (pure, 4 unit tests); GET /sessions
- terminal-session: opts.initialInput (typed ~700ms after the shell is ready)
- tabs.newTabForResume(cwd,id): new tab in the project dir running
  'claude --resume <id>'
- public/history.ts: 🕘 panel listing sessions (project · time · preview) with
  Resume buttons
- Verified: /sessions returns 44 real sessions; panel lists project/time/preview.
  216 tests green.
2026-06-18 08:04:04 +02:00
Yaojia Wang
25269cad3f fix(ui): drop in-terminal Connecting/Connected/reconnecting lines
The tab connection dot (green/amber/red) already shows connection state, so the
'[terminal] Connecting…/Connected' and 'Disconnected — reconnecting' lines just
cluttered the scrollback. Rely on the dot; keep only the actionable exit message.
2026-06-18 07:39:29 +02:00
Yaojia Wang
5df3f9158c merge: v0.3 cockpit — Claude Code status/approve/notify + tmux keepalive + FE polish
High value: H2/H4 hooks→live status + browser notifications; H3 remote
approve/reject (held PermissionRequest); H1 tmux keepalive (survive restart).
Medium: keybar Claude shortcuts, scrollback search, clickable links, QR connect,
PWA, themes, session dashboard, new-tab-in-cwd.
Also fixed: tab-switch flakiness; idle keep-alive sockets hanging close().
212 tests green.
2026-06-18 07:29:08 +02:00
Yaojia Wang
5b357e67df docs: mark v0.3 cockpit complete; only optional O1/O2 remain 2026-06-18 07:28:55 +02:00
Yaojia Wang
eb28a5593f docs: README + CLAUDE.md updated for v0.2/v0.3 (cockpit, tmux, hooks, toolbar) 2026-06-18 07:28:21 +02:00
Yaojia Wang
915a51e4bb docs: log v0.3 medium batch M3/M6/M7 2026-06-18 07:15:35 +02:00
Yaojia Wang
87e11734d8 feat(v0.3): M6 — new tab opens in the active tab's directory
- title-util.cwdFromOsc7: parse cwd from OSC 7 (file:// URL); 4 unit tests
- terminal-session: register OSC 7 handler → cwd getter; opts.cwd → sent in the
  attach frame on a fresh session
- protocol/types: ClientMessage attach.cwd (absolute path); validated
- session/manager/server: thread cwd → createSession spawn cwd (defaults homeDir)
- tabs: '+' (newTab) opens in the active tab's reported cwd, falls back to home
- Verified: server attach.cwd spawns there; cd /private/tmp → + → new tab pwd is
  /private/tmp. 212 tests green.
2026-06-18 07:14:52 +02:00