The server half shipped 2026-07-30 as feature A (4892fa7/f6ef19e/d39a0ab) and both native clients are at zero. This is NOT a regression — OrphanSessionInfo is deliberately a separate type rather than a flag on LiveSessionInfo, and src/server.ts:600 names the reason: the clients decode /live-sessions, so a variant shape there would have broken them. It is worth planning anyway because SessionManager.shutdown only detaches the tmux client for tmux-backed sessions (manager.ts:410-417), so a server restart or a desktop-app quit turns every session into an orphan. The web launcher lists them and re-adopts on tap; the phone still shows an empty list — which is the walk-away case this product exists for. The plan doc carries the eleven traps a client implementer will hit, each one a shipped review finding or a documented server subtlety — notably that preview returns cols/rows of literally 0 (both clients currently size thumbnails from that response), that a 429 must not collapse to "no orphans", and that the destructive confirmation must come from count-idle rather than card count (that one nearly ended 33 shells while the dialog said 24). Also corrects two now-false claims found while cross-referencing: the ROADMAP row for tmux discovery was half-stale, and ANDROID_CLIENT_PLAN's do-not-consume list still listed POST /projects/worktree (both clients consume it now) and GET /sessions (iOS consumes it, Android does not — so "match iOS" now argues for building it, not against).
161 lines
11 KiB
Markdown
161 lines
11 KiB
Markdown
# Roadmap
|
||
|
||
> **Status (2026-07-13):** ALL of Wave 1-5 (11 items) implemented + committed on `develop` — see docs/PROGRESS_LOG.md for per-feature commits. Remaining: only the deferred backlog table (smaller, several now cheap on the shipped PTY-inject + approval-preview primitives).
|
||
|
||
Prioritized backlog of features to build next, derived from a grounded analysis of
|
||
the codebase + the existing planning docs (5 exploration lenses → 24 candidates →
|
||
this synthesis). Each item keeps **what it touches** (real files/subsystems) and a
|
||
rough **effort** so it's actionable, not aspirational.
|
||
|
||
**The throughline:** the product already *captures* everything — Claude Code hooks,
|
||
statusLine telemetry, the activity timeline, git diff, live-sessions — but under-*acts*
|
||
on it. The highest-leverage work turns passive capture into a **trustworthy remote
|
||
review-and-drive surface**, without touching the byte-shuttle. Two small primitives
|
||
unlock a whole family of features: a **server-side PTY-inject** call and a **preview on
|
||
the approval bar**.
|
||
|
||
Effort key: **S** ≈ 1–2 days · **M** ≈ 3–4 days · **L** ≈ 1–2 weeks. Order = suggested
|
||
build sequence (dependencies noted).
|
||
|
||
---
|
||
|
||
## Recently shipped (context)
|
||
- **Split-grid watch board (v0.8, desktop)** — 1×2/1×3/2×2/2×3 layouts, click-to-focus,
|
||
per-quadrant inline approve / maximize / read-only monitor, drag-to-quadrant,
|
||
resizable splitters, saved presets. (`public/grid-layout.ts`, `grid-presets.ts`,
|
||
`cell-monitor.ts`, `tabs.ts`.)
|
||
- **Fixed:** browser worktree-create was 400ing (frontend sent `repoPath`, server reads
|
||
`path`); now aligned + regression-tested (`public/projects.ts`, `test/worktree-form.test.ts`).
|
||
|
||
---
|
||
|
||
## Wave 1 — ship first (high-trust / high-delight, independent)
|
||
|
||
- [x] **Approval preview — see-what-you-approve** ⭐ _(strongest)_
|
||
Show the pending `Bash` command or `Edit`/`Write` diff *above* Approve/Reject, so remote
|
||
one-tap approval stops being blind (today you could tap Approve on `rm -rf build`).
|
||
*The core walk-away trust gap.*
|
||
**Touches:** `src/http/hook.ts` (`tool_input` is already parsed at `:104` — derive a
|
||
bounded preview for Bash/Edit/Write/MultiEdit) → `/hook/permission` + `pendingApprovals`
|
||
in `src/server.ts:423` (attach + re-send to late joiners, like `gate` already does) →
|
||
optional bounded `preview` on the `status` ServerMessage in `src/types.ts` → `manager`
|
||
→ approval bar in `public/tabs.ts` (reuse `public/diff.ts` renderer + `sanitizeField`).
|
||
Pure side-channel. **Effort: M.** Risk: truncate + strip control chars + cap bytes,
|
||
`textContent`/diff-render only, unknown tools fall back to today's name-only bar.
|
||
|
||
- [x] **Clickable URLs & file paths in the terminal**
|
||
Tap the dev-server URL or file path Claude prints instead of soft-keyboard copy gymnastics
|
||
(paths reuse `POST /open-in-editor`). TECH_DOC named `@xterm/addon-web-links` in v0.2; never built.
|
||
**Touches:** frontend only — `public/terminal-session.ts` (add the addon + a path-matcher
|
||
regex → `/open-in-editor`). Zero server change. **Effort: S.**
|
||
|
||
---
|
||
|
||
## Wave 2 — the unlocking primitive
|
||
|
||
- [x] **Server-side PTY-inject + queued follow-up prompt** _(unlocks a family)_
|
||
A thin Origin/loopback-guarded route that writes text into a session's PTY, plus a
|
||
per-session queue that fires one entry when Claude goes idle ("now run the tests", then
|
||
"now open a PR"). Walk-away = give a task and leave; queuing lets the session advance itself.
|
||
**Touches:** new `POST /live-sessions/:id/queue` calling the existing `writeInput`
|
||
(`src/session/session.ts:201`); a small queue in `manager.ts`; dequeue in the existing
|
||
Stop/SessionEnd hook branch (`src/server.ts:414`); FE near `public/quick-reply.ts`.
|
||
Injection is identical to a keystroke → byte-shuttle preserved, broadcasts to mirrors.
|
||
**Effort: M.** Risk: gate firing on `claudeStatus==='idle'` + settle delay; surface the
|
||
queue in UI. **Foundation for** templated launches, auto-continue, issue-intake (backlog).
|
||
|
||
---
|
||
|
||
## Wave 3 — read-only side-channel batch (review from the phone)
|
||
|
||
- [x] **Diff against a base branch** (`?base=<rev>`)
|
||
Review a whole agent branch vs `main` before landing, not just uncommitted changes.
|
||
`src/http/diff.ts` already deferred this (FR-B1.9) and named its mitigation:
|
||
`git rev-parse --verify` allow-list before any revision reaches the CLI.
|
||
**Touches:** optional `base` on `getDiff()` (guarded `git rev-parse --verify <base>` +
|
||
trailing `--`, then `git diff <base>...`); branch-picker in `public/diff.ts`. **Effort: S/M.**
|
||
|
||
- [x] **PR + CI/checks status via `gh`** (read-only)
|
||
Per-project/session chip: PR state · checks passing · mergeable — glance from the phone,
|
||
re-engage only when red. No `gh` usage exists in `src/` yet; `gh` emits JSON (no parsing pain).
|
||
**Touches:** new `src/http/gh.ts` (mirror `diff.ts`'s `runGit`); `GET /projects/pr?path=`
|
||
guarded by `isValidGitDir`; `PrStatus` in `src/types.ts`; FE chip in project detail.
|
||
Capability-probe + empty-degrade when `gh` absent/unauthed. **Effort: M.**
|
||
|
||
- [x] **Quick wins** (small, cheap, high-delight)
|
||
- [ ] **Sync chip on project cards** — ahead/behind + last-commit, folded into the existing
|
||
per-repo metadata pass in `src/http/projects.ts` (no new route). **S.**
|
||
- [ ] **Cost budget guard + push alert** — `costUsd` already flows via statusLine
|
||
(`handleStatusLine`); add `COST_BUDGET_USD` + a one-shot latch (like `stuckNotified`) +
|
||
warn styling in `public/preview-grid.ts`. The rail that makes unattended auto-continue safe. **S–M.**
|
||
- [ ] **"While you were away" reconnect digest** — read-side aggregate over `manager.list()`
|
||
+ telemetry/timeline/status → "3 done, 1 waiting, $6, 2 PRs" on reconnect. **S–M.**
|
||
- [ ] **Recent-commits log per project** — `git log --oneline -n N` (NUL-delimited) via a
|
||
guarded `GET /projects/log`; inert-text render. **S–M.**
|
||
|
||
---
|
||
|
||
## Wave 4 — close the git loop
|
||
|
||
- [x] **Worktree lifecycle: remove / prune** (create is now fixed)
|
||
Delete losing worktrees + land the winner from any device — closes the create-only loop.
|
||
**Touches:** `removeWorktree`/`pruneWorktrees` in `src/http/worktrees.ts` (same execFile
|
||
no-shell + timeout, validate target in `git worktree list` & not main, reuse realpath
|
||
containment); `DELETE /projects/worktree` + `POST /projects/worktree/prune` (Origin-guarded);
|
||
make the existing `locked`/`prunable` tags actionable. **Effort: S–M.** Risk: destructive —
|
||
require `--force` + confirm for dirty trees, reject the main worktree, safe error messages.
|
||
|
||
- [x] **Stage / commit / push from the diff viewer**
|
||
Claude's done, you reviewed on the phone — now commit + push without typing git into a
|
||
mobile terminal. Highest-risk git write; bound the MVP to per-file stage-toggle + commit +
|
||
push-current-branch only; **defer discard/checkout**; realpath-contain paths, cap msg length,
|
||
push only to existing upstream or `-u`, CSRF-guarded. **Effort: M–L.**
|
||
|
||
---
|
||
|
||
## Wave 5 — bigger bets
|
||
|
||
- [x] **Worktree fan-out board** _(the north star: 真并行不互踩)_
|
||
Fan one task across N branch/agent lanes of one repo, watch them race, approve/kill per lane,
|
||
keep the winner. **Mostly composition of shipped parts** — `createWorktree` + live-sessions +
|
||
the split-grid watch board + statusLine gauges + per-quadrant inline approve; server side is a
|
||
thin "sessions grouped by repo/worktree" endpoint. Cost is UI. **Effort: L.** Depends on Wave 4.
|
||
|
||
- [x] **App-level access token** (leave-the-LAN bar-raiser) — shipped on `develop`
|
||
A constant-time-compared (`crypto.timingSafeEqual` over SHA-256, fixed-length guard)
|
||
`WEBTERM_TOKEN` checked on the WS handshake (alongside, not replacing, Origin) + a central
|
||
gate over every remote HTTP route, set as an `HttpOnly; SameSite=Strict; Secure-when-https`
|
||
cookie after one-time `GET /?token=` or `POST /auth` (rate-limited 10/min/IP), disabled when
|
||
unset (keeps LAN zero-config byte-identical). Charset/length-validated at load; loopback
|
||
`/hook*` ingest exempt. `src/http/auth.ts` + wiring in `src/server.ts`.
|
||
**Honest tradeoff:** a bar-raiser, **not** a TLS/Tailscale substitute — on bare `ws://` the
|
||
token is cleartext and replayable; only hardens the TLS-terminated relay/tunnel path. **Effort: M.**
|
||
|
||
- [x] **Android Projects / Diff / Worktree screens** (client parity)
|
||
Android's whole v0.6/v0.7 projects-git UI is SDK-gated/off in `settings.gradle.kts`.
|
||
Zero server change (the `:api-client` module already speaks the endpoints), but the largest
|
||
scope. Sequence *after* the server-side git features so it's one parity pass. **Effort: L.**
|
||
|
||
---
|
||
|
||
## Deferred backlog (your own recorded intent, surfaced from the docs)
|
||
|
||
Consciously punted in the planning docs; several become cheap once the two unlocking primitives
|
||
(PTY-inject, approval-preview) land.
|
||
|
||
| Feature | Deferred in | Note |
|
||
|---|---|---|
|
||
| Line-level review comments → agent | `FEATURE_WALKAWAY_WORKBENCH.md §B1.6 / US-B1.4` | Tap a diff line, type feedback, composed with `file:line` and sent via `TerminalSession.send`. No new route. Pairs with diff-vs-base. **M.** |
|
||
| GitHub issue → new session/worktree | `FEATURE_PROJECT_MANAGER.md §9` | Pick an issue → spawn a session (optionally fresh worktree) with title+body as the prompt. Rides PTY-inject + host `gh`. Treat issue text as untrusted bytes. **M.** |
|
||
| Templated one-tap launches (repo + prompt + mode) | implied by `attach.cwd` + Projects launchers | "Triage repo X in plan mode" as one tap. Rides PTY-inject + a template store. **M.** |
|
||
| Auto-continue on idle (bounded) | no idle automation today | Opt-in auto-inject "continue" up to N times under a cost ceiling. Rides PTY-inject + budget guard. Build last, low default cap, never auto-approve. **S–M.** |
|
||
| Cross-session cost rollup / trends | `FEATURE_WALKAWAY_WORKBENCH.md §B2.6` | Bounded ring beside the latest-only field + `GET /telemetry/summary` + dashboard panel. Overlaps the budget-guard quick win. **M.** |
|
||
| Mission Control: cross-session feed + durable run log | (no cockpit persistence today) | Merged reverse-chron feed + append-only on-disk tail (reuse `subscription-store.ts` JSONL/atomic write, byte-capped) so overnight runs survive restart. The "while you were away" digest is its first slice. **M–L.** |
|
||
| Per-project task backlog | task tracking unbuilt | Per-repo TODO store mirroring `prefs-store.ts`; one tap spawns Claude pre-filled. Local state, **not** a GitHub Issues sync (YAGNI). **M.** |
|
||
| Host tmux session discovery + attach | user request (this is not raw-iTerm; needs tmux) | **Half shipped 2026-07-30** as feature **A** (`4892fa7`/`f6ef19e`/`d39a0ab`): the server enumerates its own untracked `web_<id>` tmux sessions (`GET /orphan-sessions` + 4 routes) and the web launcher shows them in a "可恢复" section — one tap re-adopts via the existing `attach(id)` path. **Remaining (a) — iOS + Android consume it:** see [`plans/w7-orphan-session-clients.md`](./plans/w7-orphan-session-clients.md). **M.** **Remaining (b) — the original ask, still unbuilt:** *foreign* tmux sessions (manual/iTerm, not named `web_*`) are still invisible; feature A deliberately only catalogues our own. **S–M.** |
|
||
|
||
---
|
||
|
||
_Kept in sync by the maintainer. Ideas are grounded against the code as of `develop` — verify
|
||
file/line references before starting (they drift)._
|