Commit Graph

170 Commits

Author SHA1 Message Date
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
Yaojia Wang
f79f14b89d feat(v0.3): M3 themes/font settings + M7 session dashboard
- M3: settings.ts (⚙ panel: dark/light/solarized theme + font size A-/A+),
  persisted to localStorage; terminal-session.applyTheme; TabApp.applySettings
  applies to all + new tabs
- M7: dashboard.ts (▦ overlay listing every tab with connection dot + folder +
  Claude status; click a row to focus; live re-render while open);
  TabApp.snapshot()/focusTab()
- Browser-verified: light theme renders + persists; dashboard lists tabs and
  focuses on click. 208 tests green, typechecks + build ok.
2026-06-18 05:40:35 +02:00
Yaojia Wang
dcb1bf4845 docs: log v0.3 H1 tmux keepalive 2026-06-17 19:49:30 +02:00
Yaojia Wang
9099f73534 feat(v0.3): H1 — tmux keepalive (sessions survive a server restart)
- src/session/tmux.ts: sync tmux CLI wrappers (available/has/kill), best-effort
- config: useTmux from USE_TMUX env (1/0/auto→detect tmux on PATH)
- session: when useTmux, spawn 'tmux new-session -A -s web_<id> <shell>' (the
  node-pty proc is a tmux CLIENT); createSession takes an optional id for
  re-attach; Session.tmuxName; kill() runs tmux kill-session
- manager: handleAttach re-attaches to a surviving 'web_<id>' tmux session after
  a restart (Case 3.5); shutdown kills only the client pty for tmux sessions so
  the shell keeps running
- tests: USE_TMUX:0 in shared integration cfg (no tmux leakage); unit CFGs
  useTmux:false; integration 'H1' (real tmux): set var → restart server →
  re-attach → var survived. 208 tests green.
2026-06-17 19:48:39 +02:00
Yaojia Wang
af2a0879e3 docs: log v0.3 H3 remote approve/reject 2026-06-17 19:13:38 +02:00
Yaojia Wang
9a6150c355 feat(v0.3): H3 — remote approve/reject (no typing)
- types/protocol: ClientMessage approve/reject; ServerMessage status.pending
- server: POST /hook/permission HELD until the client decides; approve/reject
  ws messages resolve it with {hookSpecificOutput.decision.behavior allow|deny};
  5-min timeout + release-on-disconnect fall back to Claude's own prompt
- manager.handleHookEvent threads pending flag
- setup-hooks: PermissionRequest uses the held curl (writes decision to stdout);
  status events stay fire-and-forget
- FE: terminal-session approve()/reject() + pending state; tabs approval banner
  (Approve/Reject) for the active tab's held request
- Tests: protocol approve/reject; integration ⑧ held→approve→allow. 207 green.
- Browser-verified: banner 'Claude wants to use Bash' → Approve → resolves allow.
2026-06-17 19:13:07 +02:00
Yaojia Wang
04355f05e9 docs: log v0.3 H2/H4 Claude status feature 2026-06-17 19:01:10 +02:00
Yaojia Wang
c81cebdc47 feat(v0.3): H4 — live Claude status badge + notifications + setup-hooks
- terminal-session: handle 'status' frame → claudeStatus + onClaudeStatus
- tabs: per-tab Claude glyph (⚙ working /  waiting / ✓ idle); inactive
  'waiting' tab gets an amber highlight + a browser notification (permission
  requested on first tab switch)
- scripts/setup-hooks.mjs + 'npm run setup-hooks': idempotently install/remove
  command hooks in ~/.claude/settings.json (inline curl to $WEBTERM_HOOK_URL with
  $WEBTERM_SESSION; no-op outside web-terminal); backs up settings.json
- Browser-verified end-to-end: PermissionRequest→, PreToolUse→⚙, Stop→✓;
  inactive waiting tab shows amber + glyph. 205 tests green.
2026-06-17 19:00:34 +02:00
Yaojia Wang
a411c89ee9 feat(v0.3): H2 server — Claude Code hooks → live status side-channel
- types: ClaudeStatus + ServerMessage 'status'; Session.claudeStatus;
  SessionManager.handleHookEvent
- session: inject WEBTERM_SESSION + WEBTERM_HOOK_URL into the spawned shell env
  so hooks know which tab they belong to
- http/hook.ts: pure event→status mapper (PreToolUse→working, PermissionRequest/
  Notification:permission_prompt→waiting, Stop/idle_prompt→idle); 6 unit tests
- server: POST /hook (loopback-only, express.json 64kb) → manager pushes 'status'
- fix: closeIdleConnections() on shutdown so an idle hook keep-alive socket
  doesn't hang close()/SIGTERM
- integration ⑦ (real PTY): attach → POST /hook → ws receives status. 205 tests green.
2026-06-17 18:44:07 +02:00
Yaojia Wang
8c2a6825cc docs: log v0.3 step 1 (FE quick wins) 2026-06-17 18:33:31 +02:00
Yaojia Wang
fbc218c57f feat(v0.3): QR connect (M5) + PWA installable (M4)
- M5: 📱 toolbar button → modal with a client-side QR of location.origin
  (shareable when opened via the LAN IP) + localhost tip
- M4: manifest.webmanifest + icon.svg + sw.js (network-first, never intercepts
  /term or /hook) + registration in main.ts; apple-touch + theme-color meta
- Browser-verified: QR canvas renders, SW 'controlled', manifest linked,
  no console errors. 199 tests green, build ok.
2026-06-17 18:32:57 +02:00
Yaojia Wang
eaeacf3a78 feat(v0.3): keybar Claude shortcuts + scrollback search + clickable links
- keybar: add Esc·Esc / ^O / ^T / ^B + per-button tooltips (KEY_MAP 14 keys)
- M1 search: @xterm/addon-search per terminal; 🔍 toolbar button + search box
  (Enter=next, Shift+Enter=prev, Esc=close)
- M2 links: @xterm/addon-web-links per terminal (tap URLs Claude prints)
- tab bar split into #tabs (scrollable) + #toolbar (utility cluster) for future
  utilities (QR/settings/dashboard)
- Browser-verified: 14 keybar buttons, search highlights matches, no console errors.
  199 tests green, typechecks + build ok.
2026-06-17 18:28:40 +02:00
Yaojia Wang
110c1752d4 docs: log tab-switching fix 2026-06-17 15:46:17 +02:00
Yaojia Wang
e058751962 fix(v0.2): tab switching needed multiple clicks
Root cause: whole tab was draggable=true, so a click with any micro-movement
became a (no-op) drag instead of a click → switch failed. Plus every event
(activity/title/status) rebuilt the entire tab bar via replaceChildren, which
could destroy the element mid-click.

Fix:
- switch on pointerdown (fires before drag arms; also works on touch)
- build tab DOM once; update classes/dot/label IN PLACE (refreshTab); full
  rebuild only on structural change (add/close/reorder/rename)
- close/rename-input stopPropagation on pointerdown so they don't re-activate

Verified: single pointerdown/click switches reliably across tabs; rename + drag
still work. 198 tests green.
2026-06-17 15:45:18 +02:00
Yaojia Wang
bf21ac0ab7 docs: log folder-name tab titles 2026-06-17 15:38:09 +02:00
Yaojia Wang
4091283773 feat(v0.2): tab title = current folder (from OSC title)
- title-util.ts: folderFromTitle() parses 'user@host:~/path' → last folder
  segment (home stays ~); pure, dependency-free, 7 unit tests
- terminal-session.ts: onTitleChange → folderFromTitle so the tab auto-names
  to the cwd folder and updates live on cd
- Browser-verified: ~ → cd project → 'web-terminal' → cd docs → 'docs' → cd ~ → '~'.
  198 tests green, typecheck + build ok.
2026-06-17 15:27:01 +02:00
Yaojia Wang
459a27eda8 docs: log status dot + activity + drag-reorder 2026-06-17 14:55:48 +02:00