Files
web-terminal/docs/FEATURE_WALKAWAY_WORKBENCH.md
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

63 KiB
Raw Blame History

Feature: Walk-away Workbench 走开即工作台 (web-terminal v0.7)

状态 / Status: 提案 / Draft探索完成已并入两个 Band 的草案并应用评审意见,待评审后进入 PLAN 目标 / Goal: v0.7 把 web-terminal 从"能远程看的终端"推进成一个走开即工作台 (Walk-away Workbench),分两个 Band

  • Band A — 闭合"走开"循环 (Finish the walk-away loop):主机主动找到你的手机Web Push / ntfy让你在锁屏上批准/拒绝一个被 hold 住的工具权限(不开 app并把移动端输入做得更快语音、快捷回复加上"我离开时发生了什么"时间线,和"会话卡住了"主动告警。
  • Band B — 在终端之上长出工作台 (Become a workbench above the terminal):每个会话的只读 git diff/reviewstatusLine 遥测(成本/上下文/PR/额度仪表)、从 UI 建 git worktree(真并行不互踩)、plan-mode/权限模式中继(启动时选模式 + plan 门三选一)。
  • 关键结论 / Key finding9 个特性几乎全部是两条既有侧信道的延伸——(1) loopback hook 侧信道(POST /hook、held POST /hook/permissionsrc/server.ts:261-303)与已安装的 PWApublic/sw.jspublic/manifest.webmanifest(2) 安全 git-exec 模式(execFileAsync('git', …),无 shellsrc/http/projects.ts)。服务端依旧不解析一个终端字节——新增的只有 git 子进程、带外 JSON、和一个出站的推送签名。B3建 worktree是唯一写盘特性,必须挂 Origin/CSRF 守卫;A1 的锁屏审批是本版本最大的正确性改动(要改 /hook/permission 的"无客户端不 hold"门,见 §A1

0. 文档导航 (Section Map)

# 章节 对应需求
1 概览、目标、非目标 (Overview / Goals / Non-Goals) 必备 (1)
2 共享设计原则 (Shared Design Principles) 必备 (3)
3 Band A — A1…A5各含用户故事 / FR 表 / 设计与数据流 / 安全 / 配置 / 验收) 必备 (2)
4 Band B — B1…B4同上结构 必备 (2)
5 合并安全章节 (Consolidated Security) 必备 (4)
6 合并配置 env 表 (Consolidated Config) 必备 (5)
7 优先级与跨特性依赖排程 (Prioritization & Dependency Ordering) 必备 (6)
8 验收标准汇总 (Acceptance-Criteria Summary) 必备 (7)
9 一句话总结

1. 概览、目标与非目标 (Overview, Goals & Non-Goals)

1.1 问题陈述 (Problem)

今天的核心用例是 vibe coding:派 Claude Code 一个任务、走开、回来从任意设备看进展。v0.3v0.6 已经做到"会话不随断连而死""多设备镜像""缩略图墙""项目面板""远程 approve/reject"。但循环仍有两道缺口:

  1. 走开后没人叫你——会话需要批准工具或跑完了,除非你盯着 tab否则不知道A 段补齐"主机主动找你 + 锁屏直接处理")。
  2. 终端之上没有工作台——回看 Claude 改了什么只能滚字节缓冲;看不到这次烧了多少钱/上下文还剩多少;多 agent 并行会踩同一个工作树B 段把这些做成结构化的、带外的视图与受控写操作)。

1.2 目标 (Goals)

  • 闭合走开循环NEEDS-INPUT / DONE / STUCK 三类信号主动推到手机;锁屏 Allow/Deny 即可让 Claude 继续,全程不开 app
  • 更快的移动输入:语音口述、快捷回复 chips、可保存的提示词调色板。
  • 离开期回放:每会话的人话时间线("跑了 Bash · 改了 3 个文件 · 等批准 · 完成")。
  • 终端之上的工作台:只读 diff/review、per-tab 遥测仪表、从 UI 建 worktree、plan-mode 中继。
  • 不破坏护城河自托管、不经云、byte-shuttle、LAN-onlyTailscale 推荐、config 无硬编码、immutable、TDD ≥80%。

1.3 非目标 (Non-Goals全 Band 通用)

  • 不加登录/鉴权/多用户隔离(属未来 Band C。A1 引入的"按 pending 决策的能力 token"只是让锁屏 Allow/Deny 安全的最小手段,不是应用级 auth。
  • 不在服务端解析终端语义 / ANSI——所有新信号都是带外 JSON 或 git 子进程输出xterm.js 仍是唯一的终端解释者。
  • 不做服务端语音转写 / 文件编辑器 / 行级回评 agent / worktree 看板 / 跨会话成本汇总 / 推送任意自定义消息(各特性 §"暂不做"细列)。
  • 不把本服务暴露到公网。Band A1 的 Web Push 需要 HTTPS——通过 Tailscale Serve / TLS 实现(见 §2 与 §A1 的可行性前提),而非裸 LAN-over-HTTP 的公网化。

2. 共享设计原则 (Shared Design Principles)

适用于全部 9 个特性的不可协商前提,逐项落到既有代码:

  • SP1 — Byte-shuttle 不变:服务端不新增任何 ANSI/终端语义解析。git 数据流、statusLine 遥测、时间线全部带外HTTP/JSON 或 git 子进程),与终端 WS 字节流完全隔离——与 v0.3 hook 侧信道同构。
  • SP2 — 复用 hook 侧信道Claude 进程 POST 来的端点(/hook/hook/permission,未来 /hook/status/hook/decision。loopback-only 的摄入端点用 isLoopback(req.socket.remoteAddress) 拦非本机(src/server.ts:70-78唯一例外/hook/decisionA1它被远程设备的 SW 调用,故改用 requireAllowedOrigin + 能力 token见 §A1.5、§5
  • SP3 — 复用 held-permission 机器pendingApprovals / resolvePending / permDecisionsrc/server.ts:114-122, 61-63。A1 的锁屏决策与 B4 的 plan 门都是这条通道上的新 resolver / 新 gate 类型,不新建审批状态机。
  • SP4 — 复用安全 git-exec 模式(强制):任何 git 调用走 execFileAsync('git', [...])无 shell+ timeout + maxBuffer 截断 + mapWithConcurrency 并发上限,照搬 src/http/projects.tssrc/http/worktrees.ts绝不字符串拼 shell。入口 path 一律 path.isAbsolute() + fs.stat().isDirectory() + hasGitEntry 三连校验。
  • SP5 — 只读 vs 状态变更的守卫口径(已确立):只读发现端点(/projects/live-sessions、A4 /…/events、B1 /projects/diff不挂 Origin 守卫;任何写/副作用端点B3 /projects/worktree、A1 /push/subscribe/hook/decision必须requireAllowedOriginsrc/server.ts:215-222,已被 DELETE /live-sessionsPOST /open-in-editor 使用)。
  • SP6 — PWA 是交付载体A1 在 public/sw.jspush/notificationclick,在 manifest.webmanifest 之上利用既有可安装性。WS 仍随页面协议选 ws:/wss:M6CSP 仍 script-src 'self'src/server.ts:135-138)。
  • SP7 — 不信任外部数据 + 防 XSSgit diff/statusLine/hook body/snippet 标签都是未经信任的工具/文件内容。前端一律 textContent/安全 DOM 构建(el()禁止 innerHTML 渲染外部内容makeLauncherinnerHTML 仅限可信品牌 SVG不可作先例。未信任 JSON 用 unknown + 逐字段收窄(仿 parseHookEventnever throw。
  • SP8 — config 无硬编码 + env 命名约定L4服务端 cfg 读的 env 一律无前缀(如 PORTVAPID_PRIVATE_KEYWORKTREE_ROOTspawn 注入 / hook 脚本读的 env 一律 WEBTERM_ 前缀(如 WEBTERM_SESSIONWEBTERM_STATUSLINE_URLWEBTERM_NTFY_TOKEN)。新参数走 src/config.tsparse* 帮手 + src/types.tsConfig。秘密在启动校验/缺失优雅降级never 硬编码、never 入日志。
  • SP9 — 类型契约集中(协调点):所有新共享类型加在 src/types.ts单一 owner 任务,见 §7模块内不本地重声明。
  • SP10 — 共享 UI 组件M4A4 时间线、A5 卡住徽标、B2 仪表、既有 working/waiting/idle 状态点都渲染进 tabs.ts/preview-grid.ts/projects.ts。统一抽出一个 per-session 状态/仪表组件一个 refreshTab owner,复用既有状态色语义,避免三个任务抢同一片 DOM、重复实现陈旧/着色逻辑。

3. Band A — 闭合"走开"循环

触发点A 段共用):服务端已把 hook 折成粗 ClaudeStatusparseHookEventsrc/http/hook.ts:24-57),并经 manager.handleHookEvent 广播(src/session/manager.ts:196-212。Band A 挂在同样两个转移上:

  • NEEDS-INPUT高优 = PermissionRequestNotification(notification_type==='permission_prompt')waitingPermissionRequest 会被服务端 holdpendingApprovals 等决策。
  • DONE低优 = Stop/SessionEndidle。 加上 STUCKA5 新增,非 hook 触发) = 输出静默超阈值。三类信号都经 A1 的统一通知服务出站。

A1. 移动推送 + 锁屏审批 (Mobile Push + Lock-Screen Triage)

A1.1 用户故事 (User Stories)

  • US-A1a核心:装了 PWA 后Claude 需要批准工具时,我手机锁屏收到带 Allow / Deny 的推送;点一下就解决并让 Claude 继续——我不开 app
  • US-A1bClaude 跑完Stop/SessionEnd我收到低优通知("会话完成"),知道该回来看。
  • US-A1c:不想依赖浏览器 PWA 时hook 脚本可额外 ping 一个 ntfy / Pushover topic任何装了那个 app 的设备都会响——与 PWA 是否安装/打开无关(也是裸 LAN-over-HTTP 部署下的可用回退,见 A1.3 可行性前提)。

A1.2 功能需求 (Functional Requirements)

ID 需求 优先级
A1-FR1 Web Push 订阅:POST /push/subscribe 存浏览器 PushSubscriptionDELETE /push/subscribe 删;持久化到 config 目录下的 JSON重启存活写端点,挂 requireAllowedOrigin P0
A1-FR2 服务端在 §3 两个转移上用 VAPID 签名发 Web PushNEEDS-INPUT(高优、requireInteraction)与 DONE(低优)。依赖 web-push npm 包(见 L1/§5 供应链) P0
A1-FR3 GET /push/vapid-key 暴露公钥给 SW 订阅;私钥+subject 来自 env。VAPID env 未设 → 整个 push 优雅禁用(端点 503、UI 隐藏开关、不崩) P0
A1-FR4 /hook/permission 的 hold 门(最关键,见 A1.4 C1:当前"session.clients.size === 0 → 立即 {} 不 hold"会让"零 tab 走开"时根本不 hold、不 waiting、不推送。改为:有 ≥1 个挂着的客户端 push 已启用且有 ≥1 个活跃订阅 → holdhold 时铸一个按 pending 决策的能力 token P0
A1-FR5 SW push 渲染通知NEEDS-INPUT 带 Allow / Deny 动作按钮payload 携带 sessionId、工具名、短 detail、和那次 pending 决策的能力 token P0
A1-FR6 SW notificationclick(Allow/Deny) → POST /hook/decision {sessionId, decision, token},校验 token + Origin 后 resolvePending(...permDecision(...))(与 WS approve/reject 同效)。点正文(无动作)= 打开 app 并聚焦该会话 P0
A1-FR7 同一 tag(按 session) 使通知替换不堆叠;决策被另一设备/超时(cfg.permTimeoutMs)解决后,其它设备的通知被更新/清除(无失效的 Allow/Deny P1
A1-FR8 ntfy/Pushover 桥(秘密走 env见 H3scripts/setup-hooks.mjsWEBTERM_NTFY_URL/WEBTERM_NTFY_TOPIC(或 WEBTERM_PUSHOVER_*)存在时,给 hook 命令追加一条 fire-and-forget curlpriority 映射NEEDS-INPUT=high、DONE=lowtoken 以 spawn 注入的 $WEBTERM_NTFY_TOKEN 引用,绝不写字面量进 settings.json P1
A1-FR9 全局/按会话静音开关(客户端 localStorage 偏好)+ 服务端全局 DND env 默认 P2

A1.3 UX 与可行性前提 (UX & Feasibility)

  • 可行性前提C2置顶Service Worker + pushManager.subscribe 要求安全上下文。手机访问 http://192.168.x.x:3000 不是安全上下文(只有 localhost 是),而项目文档默认是裸 LAN-over-HTTPCLAUDE.md / TECH_DOC §7。因此 Band A1 的 Web Push 需要 HTTPS——推荐 Tailscale Serve / TLS。前端必须 !isSecureContext 检测并给出明确文案("启用推送需经 HTTPS/Tailscale 访问"),并在不安全上下文下隐藏 🔔 开关、引导改用 A1-FR8 的 ntfy/Pushover 桥curl 直连外部服务,不受安全上下文限制,是裸 LAN 部署的回退。iOS 还额外要求 PWA 已加到主屏。CSP connect-src 'self' ws: wss: 在 TLS 同源下满足 push 端点可达。
  • 设置:工具栏 🔔 开关 → 浏览器权限提示 → pushManager.subscribe(vapidKey)POST /push/subscribe;开关反映 granted/denied/unsupported/insecure-context。
  • NEEDS-INPUT 通知:标题 ⏳ Claude needs you — <repo/session>,正文 = 工具名 + detail 首行,动作 [Allow] [Deny]requireInteraction:true。Allow/Deny 不开 app 即解决;下次打开有 toast 确认结果。
  • DONE 通知✅ Session done — <repo/session>,无动作、低优、点开聚焦该 tab。
  • 移动优先核心价值就是锁屏处理happy path 零前台交互;不可用时降级到 ntfy/Pushover。

A1.4 数据流 (Design / Data Flow)

Claude hook (host) ──► POST /hook  或  held POST /hook/permission  (loopback)
   │  parseHookEvent → status/hook/permission: ★改门★
   │     hold ⇔ (session.clients.size>0) OR (push 启用 && 有活跃订阅)   ← C1 修正
   │     hold 时铸 per-decision capability token存入 pendingApprovals 条目
   ▼
manager.handleHookEvent → 转移 (waiting / idle)  →  notifyService.notify(session, class, token?)   ← 新
   ▼  web-push 用 VAPID 签名 → 每个存储的 PushSubscription404/410 自动剪除)
device SW 'push' (public/sw.js) → showNotification(actions:[Allow,Deny], data:{sessionId,token})
   │  用户点 Allow
   ▼
SW 'notificationclick' → POST /hook/decision {sessionId, decision, token}   ← 新路由远程→Origin+token 守卫)
   │  校验 Origin + token按 pending 决策、resolve/超时即失效)→ resolvePending(sessionId, permDecision(...))
   ▼
held curl 返回 decision JSON → Claude 继续(与 WS approve/reject 同路径src/server.ts:439-445, :61-63
  • 新共享类型(加 src/types.tsPushSubscriptionRecordendpoint/keys/createdAtpendingApprovals 条目扩 token + expiresAtNotifyClass = 'needs-input'|'done'|'stuck'
  • 新文件src/push/push-service.ts(签名/发送/通知服务)、src/push/subscription-store.ts(持久化/剪除)、public/push.ts(订阅流 + 🔔 + 决策 fetch

A1.5 安全考量 (Security)

  • /hook/decision 是新敏感面:它被远程设备 SW 调用(非 loopbackisLoopback 不适用),故必须 (1) 过 requireAllowedOriginCSWSH/CSRF(2) 校验能力 tokentoken 绑定到"该 session + 当前那次 pending 请求"resolve/超时即失效M1——泄漏的长期订阅 token 不能解决任意未来审批。失配/陈旧 → 403。对 /push/subscribe/hook/decision 限流。
  • VAPID 私钥 + subject 是秘密 → 仅 env、启动校验、绝不入日志缺失 → 禁用而非崩。
  • 订阅库含 endpoint+keys按敏感处理文件权限 600绝不经任何 GET 暴露);返回 404/410 的订阅剪除;PUSH_MAX_SUBS 封顶防 DoS。
  • ntfy tokenH3:以 spawn 注入 env 引用,不写进 settings.json也不进 curl 字面 argv(避免 ps/备份文件泄密)。
  • payload 最小化:只带 session id + 工具名 + 短 detail无命令输出、无秘密守 byte-shuttle 边界)。
  • 供应链L1:新增 web-push 运行时依赖——纳入安全清单与锁版本。

A1.6 配置 (Config)

Env 默认 说明
VAPID_PUBLIC_KEY 未设→push 关 公钥(暴露给客户端)
VAPID_PRIVATE_KEY —(秘密) 私钥;启用 push 必需
VAPID_SUBJECT mailto:admin@localhost VAPID sub
PUSH_STORE_PATH <homeDir>/.web-terminal/push-subs.json 订阅持久化文件600
PUSH_MAX_SUBS 50 订阅数上限DoS
NOTIFY_DONE 1 是否发低优 DONE 推送
NOTIFY_DND 0 全局免打扰默认A1-FR9
DECISION_TOKEN_TTL_MS =PERM_TIMEOUT_MS 能力 token 寿命(随 pending 决策)
spawn/hook envWEBTERM_NTFY_URL/WEBTERM_NTFY_TOPIC/WEBTERM_NTFY_TOKEN/WEBTERM_PUSHOVER_TOKEN/WEBTERM_PUSHOVER_USER 未设→桥关 ntfy/Pushover 桥token 走 env不入 settings.json

A1.7 暂不做 (Out of Scope)

  • 任意/自定义用户推送、营销、定时通知。
  • 多用户订阅归属 / 按用户路由(无 auth — Band C
  • 原生 iOS/Android app 或 FCM/APNs 直连(仅 Web Push + ntfy/Pushover
  • 富媒体/图片通知。

A1.8 验收标准 (Acceptance)

  • AC-A1.1:装 PWA 并开 🔔 后,POST /push/subscribe 204GET /push/vapid-key 返回配置的公钥。
  • AC-A1.2C1 核心)在零浏览器 tab 挂着的情况下Claude 触发工具批准 → 手机锁屏出现带 Allow/Deny 的通知;点 Allow 解决 held 请求并让 Claude 继续,不开 app;点 Deny 则拒绝。
  • AC-A1.3:一台设备解决(或 cfg.permTimeoutMs 超时)后,其它设备的通知被替换/清除(无失效 Allow/Deny
  • AC-A1.4Stop/SessionEndNOTIFY_DONE=1 时到达低优 DONE 通知。
  • AC-A1.5C2在不安全上下文http LAN下 UI 隐藏 🔔 并提示需 HTTPS/Tailscale不崩HTTPS 下订阅成功。
  • AC-A1.6VAPID env 未设 → push 端点 503、UI 隐藏开关、无崩。
  • AC-A1.7H3WEBTERM_NTFY_* 设置后 setup-hooks.mjs 写出会 ping ntfy 的命令且正确 prioritysettings.json 与进程 argv 中不含任何 token 字面量web-terminal 外该 curl no-op。
  • AC-A1.8M1/hook/decision 拒绝缺/外来 Origin403与坏/陈旧 token403resolve 后旧 token → 403。

A2. 语音口述 (Push-to-Talk Mic on Mobile Input Bar)

A2.1 用户故事

  • US-A2:移动端按住 🎤 说话,松手后转写文本被打进 Claude可选自动 Enter无需软键盘。

A2.2 功能需求

ID 需求 优先级
A2-FR1 按住录音Web Speech SpeechRecognition)、松手停止;最终转写经既有 raw-bytes 路径发送 P0
A2-FR2 实时 interim 转写显示在小 overlay/chip发送前可见 P1
A2-FR3 "发送 vs 仅插入"设置(是否补 \r);默认仅插入、用户手动点 ⏎ P1
A2-FR4 不支持的浏览器(如非 Chromium隐藏 🎤,绝不抛错 P0
A2-FR5 识别语言可选(默认 navigator.language P2

A2.3 UX 与数据流

  • 🎤 置于键栏(public/keybar.tstouchstart/touchend 按住语义并 preventDefault 保持软键盘不弹(仿 keybar.ts:104-108);按住时脉冲红点 + interim chip桌面点击切换回退仿 :111-114)。
  • 松手 → onSend(transcript [+ '\r']) = TerminalSession.sendpublic/terminal-session.ts:316-319)→ {type:'input'}pty.writebyte-shuttle 不变)。纯前端,零服务端改动

A2.4 安全考量

  • 麦克风权限浏览器把关;转写作 raw input.data 逐字透传(协议禁止过滤输入内容),无新注入面。
  • 隐私警示L2Chrome Web Speech 会把音频流给 Google 服务——文档明示;这是客户端选择,自托管服务端永不接触音频(设 AC 验证)。

A2.5 配置:无 env前端。客户端偏好 dictation.autoSend/dictation.langlocalStorage

A2.6 暂不做:服务端 STTWhisper 等)、语音命令(如"approve")、唤醒词/常听、录音存储。

A2.7 验收标准

  • AC-A2.1:支持的移动浏览器上按住 🎤 说话显示 interim松手把最终转写打进当前终端。
  • AC-A2.2使用麦克风时软键盘不弹touch preventDefault)。
  • AC-A2.3:无 SpeechRecognition 的浏览器隐藏 🎤 且不报错。
  • AC-A2.4:自动发送开 → 以 \r 结尾;关 → 等手动 ⏎。
  • AC-A2.5L2:抓包/服务端日志确认无任何音频抵达服务端。

A3. 快捷回复 chips + 提示词调色板 (Quick-Reply Chips + Saved Prompt Palette)

A3.1 用户故事

  • US-A3a:移动端在键栏旁有 tap-to-send chipsyescontinue1/2/3Esc),一点即发。
  • US-A3b:可保存自定义 snippet"run the tests"、"write a commit"、"/clear")到调色板,一点即发,跨会话持久。

A3.2 功能需求

ID 需求 优先级
A3-FR1 内置快捷 chipsyes\r/continue\r/1\r/2\r/3\r/Esc),各经既有 onSend 发送 P0
A3-FR2 用户调色板:增/改/删命名 snippetlocalStorage 持久;每条含文本 + 是否补 \r P0
A3-FR3 一点发送任意 snippet同键栏 ws.send 机制) P0
A3-FR4 重排/收藏;首次运行播种合理默认集 P1
A3-FR5 长按编辑(移动)/ 右键(桌面) P2
A3-FR6 调色板 JSON 导入/导出(手动跨设备搬运) P2

A3.3 UX 与数据流

  • chips 横向滚动行,置于键栏内/上,视觉与既有键栏一致; 开小型调色板编辑器(复用既有玻璃浮层样式)。点 chip 即发(无确认),编辑显式(长按/)。
  • 点 chip/snippet → onSend(text [+ '\r']) = TerminalSession.sendterminal-session.ts:316-319)→ {type:'input'}pty.write。调色板存 localStorage无服务端

A3.4 安全考量

  • snippet 是用户文本,按"和打字同信任级"作 raw input标签以 textContent 渲染,绝不 innerHTML 用户串SP7。localStorage 按 origin 隔离、无服务端存储、无新端点。

A3.5 配置:无 env客户端调色板/默认在 localStorage

A3.6 暂不做:服务端/团队共享 snippet 库(无 auth — Band C、参数化模板、经服务端同步调色板导入/导出是手动桥)。

A3.7 验收标准

  • AC-A3.1:内置 chips 出现并发正确字节(含 \r)。
  • AC-A3.2:可增/改/删/重排 snippet刷新后持久。
  • AC-A3.3:点 snippet 经与键栏同路径发送(终端确实收到)。
  • AC-A3.4:含特殊字符的标签作惰性文本渲染(无 DOM/脚本注入)。

A4. 活动时间线 ("What Happened While I Was Gone")

A4.1 用户故事

  • US-A4:回来时在缩略图/详情旁看到人话、带时间戳的流"10:02 ran Bash · 10:03 edited 3 files · 10:05 waiting for approval (Write) · 10:09 done",几秒看懂离开期发生了什么。

A4.2 功能需求

ID 需求 优先级
A4-FR1 把离散 hook 事件(PreToolUsePostToolUseNotificationStopSessionEndUserPromptSubmit)存入每会话、带时间戳、有界的内存事件环。注意:当前安装器 FF_EVENTS 不含 PostToolUseH1见 §A4.5),必须补注册 P0
A4-FR2 GET /live-sessions/:id/events 返回该会话最近时间线(按数封顶) P0
A4-FR3 每事件含:时间戳、事件类、工具名(有则)、服务端派生的人话短标签(PreToolUse(Bash)→"ran Bash" P0
A4-FR4 前端在详情/缩略图旁渲染流newest-first开着时自动刷新 P0
A4-FR5 有界环(每会话 cfg.timelineMax),旧的淘汰;与字节环互补不替代 P0
A4-FR6 hook body 提供文件路径时Edit/Write 的 tool_input)捕获"改了哪些文件",否则回退工具名 P1
A4-FR7 折叠快速重复事件("ran Bash ×4" P2

A4.3 UX

  • 详情页/缩略图卡片的 "Timeline" 面板:HH:MM · 图标 · 标签 竖列按类着色working=中性、waiting=琥珀、done=绿,复用既有状态语义,经 §SP10 的共享组件)。时间线补充实时缩略图(RingBuffer.tail()src/server.ts:197-209):缩略图是"现在屏幕长啥样",时间线是"这段时间发生了啥"。

A4.4 数据流与对 hook 摄入的扩展H2非纯复用

POST /hook (loopback)  body: hook_event_name, tool_name, notification_type, [tool_input]
   │  ★ parseHookEvent 当前只回 {sessionId,status,detail(=tool_name)},会丢掉时间戳/tool_input ★
   │  → A4 必须扩展 hook 摄入:要么 parseHookEvent 回更富事件,要么把 raw body 传给时间线 appender
   ▼  status 路径不变 + append 离散 TimelineEvent → session.timeline有界环sanitize
GET /live-sessions/:id/events → 最近 TimelineEvent[]  →  前端面板(可见时轮询)
  • 被编辑面(明确列出,非"reuse"src/http/hook.tsparseHookEvent 或新 appender+ /hook 路由(src/server.ts:266-272 当前只把 {status,detail} 传给 manager+ scripts/setup-hooks.mjs(补 PostToolUse)。
  • 新文件src/session/timeline.ts(有界环 + 标签派生 + sanitize纯、可单测public/timeline.ts(面板)。
  • 新类型TimelineEventsrc/types.tsSession 加有界 timeline"在不可变 meta 上挂可变 runtime handle"模式,同 buffer/lastOutputAt)。

A4.5 安全考量

  • /live-sessions 同威胁模型:只读发现、不挂 Origin 守卫(守卫给状态变更路由);暴露工具名/文件路径给 LAN 设备——与"已对 LAN 交出 shell"一致Tailscale-onlyTECH_DOC §7
  • 未信任 hook bodywhitelist hook_event_name、截断字符串长度、sanitize 控制字符(复用 sanitizeForLogsrc/server.ts:80-86)后再存路径/工具名。有界环防事件洪水撑爆内存DoS

A4.6 配置

Env 默认 说明
TIMELINE_MAX 200 每会话事件环上限
TIMELINE_ENABLED 1 捕获/服务时间线总开关

A4.7 暂不做跨重启持久化仅内存同会话、Claude 推理的 token 级全转录(~/.claude 已有深度回放)、服务端 ANSI 推断活动(守 byte-shuttle、时间线搜索/分析。

A4.8 验收标准

  • AC-A4.1:跑一个 Claude 任务产出离散、带时间戳、人话的条目工具用、waiting、done
  • AC-A4.2GET /live-sessions/:id/events 返回最近事件,受 TIMELINE_MAX 界。
  • AC-A4.3:面板在缩略图/详情旁渲染并随新事件更新。
  • AC-A4.4SP7hook body 的工具名/路径经 sanitize无控制字符并截断后才显示。
  • AC-A4.5H1setup-hooks.mjs 安装后 FF_EVENTSPostToolUse"完成 Bash"类事件确实到达。
  • AC-A4.6TIMELINE_ENABLED=0 不存事件、端点返回空。

A5. 卡住/静默主动告警 (Stuck / Idle Proactive Alert)

A5.1 用户故事

  • US-A5:会话既没"完成"也没 hold 审批、只是静默(卡在未挂 hook 的提示,或 wedged无输出超 N 分钟而它看着在忙,我收到"会话可能卡住 — 12 分钟前最后活动"告警。

A5.2 功能需求

ID 需求 优先级
A5-FR1 检测静默:复用 lastOutputAtnow - lastOutputAt > STUCK_TTL 且非 idle/已退出 → stuck-候选 P0
A5-FR2 首次越阈经 A1push/ntfy发通知每个 stuck 回合只发一次(输出恢复后重新武装) P0
A5-FR3 检测扫描跑在既有 reaper 间隔cfg.reapIntervalMs),不新增常驻 timer P0
A5-FR4 UI 出 "stuck" 徽标(缩略图墙/tab 状态,经 §SP10 共享组件),无 push 也可见 P1
A5-FR5 阈值可配;0 禁用 P0
A5-FR6 可选抑制"从未为 vibe-coding 启动且零客户端"的会话——但默认无脑告警(你就是走开了) P2

A5.3 设计与数据流(对 reaper/manager 的真实改动H4

reaper 每 cfg.reapIntervalMssrc/server.ts:317-321 → manager.reapIdle
   │ ★ reapIdle 当前只遍历 detached 会话if detachedAt===null continue★
   │ → A5 需要 manager 新方法 sweepStuck()(或在 reapTimer 里加独立 sweep 回调),
   │    遍历 attached-或-detached 的存活、非 idle 会话,并把 notifyService 注入 manager
   ▼ if now - session.lastOutputAt > STUCK_TTL && !session.stuckNotified
         → session.stuckNotified = true → notifyService.notify(session,'stuck')   ← 经 A1
   ▼ 新 pty 输出lastOutputAt 前进)→ 重置 session.stuckNotified重新武装
  • 复用lastOutputAtsrc/types.ts:163,已是 orphan-reclaim 的 liveness 代理reaper 节拍(不新 timerKISS
  • 新改动(重新界定 Ownsmanager.tssweepStuck() + manager↔notifyService 接线;SessionstuckNotified flag。依赖 A1 先落地 notifyServiceM5

A5.4 安全考量

  • 无新端点,服务端内部;继承 A1 通知安全token、payload 最小化)。阈值经 env、校验为非负整数复用 parseNonNegativeIntsrc/config.ts:42-550=禁用。每回合至多一次告警A5-FR2,防告警风暴。

A5.5 配置

Env 默认 说明
STUCK_TTL 60010 分钟) 静默窗口;0 禁用
STUCK_ALERT 1 卡住告警总开关

A5.6 暂不做:经进程组/子进程探测的真挂死检测node-pty 跨平台不可靠,同 orphan-reclaim 限制ARCHITECTURE §3.5/M3、自动补救发输入/杀会话)、每命令"预期时长"自适应阈值。

A5.7 验收标准

  • AC-A5.1:无输出达 STUCK_TTL(且非 idle/退出)→ 经 A1 通道恰发一次 stuck 通知。
  • AC-A5.2:输出恢复后 flag 重新武装,后续静默可再告警。
  • AC-A5.3H4:扫描不加新 timer跑在既有 reaper pass 内),且确实覆盖 attached 会话(不止 detached
  • AC-A5.4STUCK_TTL=0STUCK_ALERT=0 禁用、无通知。
  • AC-A5.5:缩略图墙/tab 状态出 "stuck" 徽标。

4. Band B — 在终端之上长出工作台


B1. 每会话只读 git diff / review (Read-only Git Diff / Review)

B1.1 用户故事

  • US-B1.1(核心):手机点项目/会话的 Diff → 看到按文件分组的 unified diff每文件带 +增/-删 统计、绿/红高亮、可折叠/跳转——只读,不怕误触改坏。
  • US-B1.2:可在 未暂存(working)/已暂存(staged) 间切换。
  • US-B1.3:窄屏不横向滚到吐——可切 unified↔split默认窄屏 unified + 软换行。
  • US-B1.4(仅记录):行级回评给 agent——Band B 不做。

B1.2 功能需求

ID 需求 优先级
FR-B1.1 GET /projects/diff?path=<abs>&staged=0|1 只读端点:跑 git diff [--staged] -- 取 patch返回结构化 diff P0
FR-B1.2 复用安全 git-execexecFile(无 shell+ timeout + maxBuffer 截断 + 入口 path 校验(绝对+目录+git 仓库) P0
FR-B1.3 纯函数 parseUnifiedDiff(patch) / parseNumstat(out)DiffFile[](可单测、零 DOM P0
FR-B1.4 前端 diff 查看器:按文件分组、+/- 徽标、行级绿/红、文件侧栏跳转/折叠 P0
FR-B1.5 working/staged 切换;git status --porcelain/numstat 标注 untracked(避免 --no-index 的反直觉双操作数与预期非零退出L3 P1
FR-B1.6 入口:项目详情页 + 每会话工具栏各加 Diff/Review 按钮(会话用其 spawn cwd 作 path P1
FR-B1.7 unified↔split 切换;窄屏默认 unified+软换行;空 diff 友好态 P1
FR-B1.8 大 diff 防爆:服务端按文件数/总字节封顶,超限返回 truncated 标记 P2
FR-B1.9 已提交对比:?base=<rev>git diff <base>base 走 git rev-parse --verify 白名单,见安全) P2

B1.3 设计与数据流

详情/会话工具栏 ── Diff ──► GET /projects/diff?path=<abs>&staged=0
   ▼ src/http/diff.ts
   ├ 入口校验 (绝对 + isDirectory + hasGitEntry)   ← 复用 projects.ts:389-399, 112-119
   ├ execFileAsync('git', ['diff','--no-color', staged?'--staged':null, '--'])  ← 无 shell, timeout, maxBuffer
   ├ execFileAsync('git', ['diff','--numstat', ...])   +/- 行数)
   ├ execFileAsync('git', ['status','--porcelain'])     (标注 untrackedL3
   └ parseUnifiedDiff + parseNumstat → DiffResult { files: DiffFile[], staged, truncated }
   ▼ JSON → public/diff.tsrenderDiff纯 DOM/textContent无 innerHTML
  • 新类型(src/types.tsDiffLine{kind,text}DiffFile{oldPath,newPath,status,added,removed,binary,hunks}DiffResult{files,staged,truncated}
  • 新文件src/http/diff.tspublic/diff.ts

B1.4 安全考量

  • 只读:仅 git diff(无写参数),不挂 Origin 守卫(同 /projects/detail 口径)。
  • 无 shell 注入execFile 数组参数;-- 终结选项防 path/base 被当 flag。
  • path 越界:绝对 + isDirectory + hasGitEntry 三连;baseFR-B1.9)必须 git rev-parse --verify 白名单化,否则保持在 P0 之外。
  • XSS关键diff 是任意文件内容——全程 textContent/el(),零 innerHTMLCSP 兜底。
  • DoStimeout + maxBuffer + 文件/字节封顶FR-B1.8)。

B1.5 配置

Env 默认 说明
DIFF_TIMEOUT_MS 2000 单次 git diff 超时
DIFF_MAX_BYTES 2097152 (2MB) patch 截断上限
DIFF_MAX_FILES 300 返回文件数上限,超出标 truncated

B1.6 暂不做:行级回评(仅记录)、编辑/暂存/丢弃/git add -p/commit破坏只读+byte-shuttle明确不做、语法着色不引第三方高亮库

B1.7 验收标准

  • AC-B1.1:有未提交改动的 repo 调端点返回正确 DiffFile[]+/- 与 git diff --numstat 一致;staged=1 返回已暂存集。
  • AC-B1.2:非 git/不存在 path → 404超大 diff → truncated:true 且不 OOM。
  • AC-B1.3:手机查看器逐文件跳转/折叠、working↔staged 切换可用,无横向滚到吐。
  • AC-B1.4SP7:含 <script>/ANSI/反引号的内容原样作文本显示,不执行、不破坏布局。
  • AC-B1.5parseUnifiedDiff/parseNumstat 纯函数单测 ≥80%(含重命名/新增/删除/二进制/空 diff/untracked

B2. statusLine 遥测 → per-tab 成本/上下文/PR 仪表

⚠ 研究优先H5阻断式前提B2 的字段假设(context_window.used_percentagecost.total_cost_usdrate(5h/7d)、pr必须先对照真实 statusLine stdin schema 核实,再让 §7 的 T-types 冻结 StatusTelemetry。研究未确认 → 该任务返回 [!] BLOCKED,不得猜测字段名。

B2.1 用户故事

  • US-B2.1(核心)tab 上一眼看到上下文进度条累计 $ 成本模型 chip未关闭 PR 徽标,不用进会话翻。
  • US-B2.2:缩略图墙/项目卡片也显示,挑"快烧爆上下文/快没额度"的优先处理。
  • US-B2.3:看到 5h/7d 速率额度仪表,避免撞墙。

B2.2 功能需求

ID 需求 优先级
FR-B2.1 setup-hooks 安装 statusLine 脚本:读 stdin statusLine JSON → POST 到 $WEBTERM_STATUSLINE_URL(带 X-Webterm-Session),并回一行精简文本给 Claude 状态行 P0
FR-B2.2 POST /hook/statusloopback-only复用 isLoopbackparseStatusLine 解析→存该 session 最新遥测→广播给挂着的客户端 P0
FR-B2.3 按 sessionId 存最新遥测(类比 claudeStatusattach 时随状态一起补发(晚加入设备立即看到) P0
FR-B2.4 ServerMessage 类型 telemetry同步更新 serialize、客户端 handle switch、任何 exhaustiveness 检查M3TerminalSession 暴露 getter + onTelemetry 回调 P0
FR-B2.5 每 tab 渲染:上下文进度条 + $ 成本 + 模型 chip + PR 徽标(带 review_state 颜色),经 §SP10 共享仪表组件 P0
FR-B2.6 缩略图/项目卡片/manage 页复用同组小仪表 P1
FR-B2.7 5h/7d rate-limit 双 meterlines +/、effort 作 tooltip P1
FR-B2.8 解析对缺字段宽容(缺哪个不渲染哪个),陈旧遥测(>TTL置灰 P1
FR-B2.9 PR 徽标点击 = 复制/打开 PR URL同源 UI 内展示,外跳需用户显式点击) P2

B2.3 设计与数据流

Claude statusLine ──stdin JSON──► scripts/statusline.mjs
   ├ POST "$WEBTERM_STATUSLINE_URL" -H "X-Webterm-Session: $WEBTERM_SESSION" --data-binary @-
   └ echo 一行精简状态给 Claude (cost · ctx% · model)
   ▼ (loopback) POST /hook/status (isLoopback 复用)
   ▼ src/http/statusline.ts  parseStatusLine(sessionId, body) → StatusTelemetry | null  (纯, never throw, 仿 hook.ts:24)
   ▼ manager.handleStatusLine(id, telemetry) → 存 session.telemetry + broadcast {type:'telemetry',...}
   ▼ public/terminal-session.ts  case 'telemetry'(仿 status case :269-275→ onTelemetry
   ▼ public/tabs.ts per-tab 仪表(仿 refreshTab :468· 缩略图/卡片复用
  • spawn env 注入B2 唯一的 spawn 改动):在 src/session/session.ts:94-98 现有 WEBTERM_SESSION/WEBTERM_HOOK_URL 旁加 WEBTERM_STATUSLINE_URL: http://127.0.0.1:${cfg.port}/hook/statusweb-terminal 外该变量未设 → curl no-op。
  • 新类型(src/types.tsStatusTelemetry{contextUsedPct?,costUsd?,linesAdded?,linesRemoved?,model?,effort?,pr?{number,url,reviewState?},rate?{fiveHourPct?,sevenDayPct?},at}ServerMessage |= {type:'telemetry',telemetry}Session.telemetry: StatusTelemetry|nullLiveSessionInfo/ProjectSessionRef 可选带最新遥测。
  • 新文件src/http/statusline.tsscripts/statusline.mjs

B2.4 安全考量

  • loopback-only/hook/status 复用 isLoopback——外部 LAN 设备无法伪造遥测。
  • 未信任 JSONparseStatusLineunknown + 逐字段收窄,缺/脏字段安全降级never throw。
  • XSSmodel/PR URL 等走 textContentPR URL 不自动 window.open,仅用户显式点跳。
  • 体量express.json({limit:'64kb'})(同 /hook),只存"最新一条",无累积。

B2.5 配置

Env 默认 说明
STATUSLINE_TTL_MS 30000 多久未更新视为陈旧(前端置灰)
spawn envWEBTERM_STATUSLINE_URL 服务端按 cfg.port 注入 脚本 POST 目标,非用户配置

B2.6 暂不做:遥测历史/趋势图、跨会话成本汇总(只存最新一条)、服务端解析 statusLine 的 ANSI/格式(仍 byte-shuttle只摄入结构化 JSON、把遥测做成 push 告警(属 A1 范畴)。

B2.7 验收标准

  • AC-B2.1npm run setup-hooks~/.claude/settings.json 含 statusLine 段且幂等、--remove 干净移除。
  • AC-B2.2:跑会话后 tab 出现上下文进度条 + $ 成本 + 模型 chip。
  • AC-B2.3M3晚加入设备 attach 时立即收到当前遥测(与既有 status 回放一起补发)。
  • AC-B2.4:缺字段 JSON → 只渲染已有项不报错;陈旧(>TTL置灰。
  • AC-B2.5:非本机 POST /hook/status → 403。
  • AC-B2.6parseStatusLine 纯函数单测 ≥80%(满/缺/脏/空);ServerMessage 序列化/反序列化往返测试覆盖 telemetry

B3. 从 UI 建 git worktree ⚠ 状态变更 (唯一写盘特性)

B3.1 用户故事

  • US-B3.1(核心):项目详情页点 New worktree,填新分支名、选 agent系统 git worktree add -b <branch> 建独立目录并在那里开会话——多 agent 真并行不互踩。
  • US-B3.2:新 worktree 立刻出现在列表,新 tab 标注分支名。
  • US-B3.3(仅记录)prune/promote/merge worktree——Band B 不做。

B3.2 功能需求

ID 需求 优先级
FR-B3.1 POST /projects/worktreeOrigin/CSRF 守卫{path, branch, base?};执行 git worktree add -b <branch> <computedDir> [<base>] P0
FR-B3.2 严格校验:path 绝对+isDirectory+isGitbranch 过 git ref 规则子集(拒控制字符/../前导 -/空白/~^:?*[/@{computedDir 必须落在受控 base 内 P0
FR-B3.3 安全 execexecFile('git', [...]) 无 shell + timeout + 数量上限;失败返回结构化 error不泄完整 stderr P0
FR-B3.4 新目录 = <WORKTREE_ROOT 或 <repo>-worktrees>/<sanitized-branch>(可配 base P0
FR-B3.5 成功返回新 path+branch;前端调既有 openProject(newPath, label, cmd) 在该目录开会话 P0
FR-B3.6 详情页表单:分支名 + base 选择 + agent 选择claude/codex建好刷新 worktree 列表 P1
FR-B3.7 防重:分支/目录已存在 → 明确报错并提示进入已有 worktree P1
FR-B3.8 移动端友好表单(大点击区、键栏不挡) P1
FR-B3.9 从现有分支建(不带 -b P2

B3.3 设计与数据流

详情页 ──  New worktree (branch, base?, agent) ──► POST /projects/worktree {path, branch, base?}
   ▼ requireAllowedOrigin (src/server.ts:215) + express.json({limit:'4kb'})   ← 仿 /open-in-editor :247
   ▼ src/http/worktrees.ts  createWorktree(repoPath, branch, opts)
   ├ validateBranchName(branch)                         (纯, 单测)
   ├ computeWorktreeDir(repoPath, branch, cfg.worktreeRoot)  → 受控、防越界 (见安全 M2)
   ├ 入口校验 (repoPath 绝对+isDirectory+hasGitEntry)
   └ execFileAsync('git', ['worktree','add','-b',branch, dir, base?])   ← 无 shell, timeout
   ▼ { ok:true, path:dir, branch } | { ok:false, status, error }
   ▼ public/projects.ts 表单提交 → 成功则 hooks.onOpenProject(dir, `${repo}:${branch}`, cmd)
   ▼ TabApp.openProject(dir, label, cmd)(终端侧零改动)
  • 新类型/函数CreateWorktreeResult{ok,path?,branch?,status?,error?}src/types.tsvalidateBranchName/computeWorktreeDir(纯,单测)/createWorktree(扩展既有 src/http/worktrees.ts,与 listWorktrees 同文件)。

B3.4 安全考量(本特性最重

  • CSRF/Origin:写端点必须 requireAllowedOrigin(否则恶意页可无预检 POST 建分支/写盘)——同 DELETE /live-sessions/open-in-editor
  • 命令注入execFile 数组参数,绝不 shell 拼接git 选项前置、用户值在 -- 后。
  • 分支名注入/越界validateBranchName../越界 //控制字符/前导 -(防当 flag/空白/~^:?*[/@{,遵 git ref 规则子集。
  • 路径越界关键M2computeWorktreeDirpath.resolve对两端先 fs.realpath 再做前缀 startsWith 包含校验(防 symlink 化的 WORKTREE_ROOT 或 repo 绕过字符串前缀sanitize 分支为目录段(/-),杜绝写到任意位置。baseFR-B3.9)若接受须 git rev-parse --verify
  • DoS/资源timeout + 单次只建一个 + 数量上限error 文案不回传完整 git stderr。
  • 审计sanitizeForLogsrc/server.ts:80-86)记录 who/whatbranch/path 截断、去控制字符)。

B3.5 配置

Env 默认 说明
WORKTREE_ENABLED 1 总开关(保守部署可关此写能力)
WORKTREE_ROOT <repo>-worktrees(同级) 新 worktree 落地根;强制所有 worktree 落此前缀内
WORKTREE_TIMEOUT_MS 10000 git worktree add 超时

B3.6 暂不做prune/promote/merge/remove worktree仅记录、worktree 看板、远程分支跟踪/--track/PR 自动开。

B3.7 验收标准

  • AC-B3.1:填新分支名 → 在 WORKTREE_ROOT 下建出目录,git worktree list 可见,新 tab 在该目录跑起 agent。
  • AC-B3.2:缺/伪造 Origin → 403非 git path → 4xx非法分支名../x-rf、控制字符)→ 拒绝且不执行 git。
  • AC-B3.3:分支/目录已存在 → 明确错误,无半成品残留。
  • AC-B3.4M2symlink 化的 WORKTREE_ROOT/repo 不能绕过包含校验realpath 后仍被拒)。
  • AC-B3.5:两个 worktree 各开一个 agent 互不踩工作树(手动并行正确性)。
  • AC-B3.6validateBranchName/computeWorktreeDir 纯函数单测 ≥80%(含越界/注入/symlink+ 端点集成测试(真起 server、临时 repo

B4. plan-mode / 权限模式中继 (Permission-mode Relay)

⚠ 研究优先H5阻断式前提--permission-mode 取值(default/acceptEdits/plan/bypassPermissions、ExitPlanMode 是否经 /hook/permission 到达、以及 permDecision 的 decision JSON 能否携带"更新后的权限模式"必须先对照 Claude Code 官方文档/源码确认——当前 permDecisionsrc/server.ts:61-63)只发 {hookSpecificOutput:{hookEventName:'PermissionRequest',decision:{behavior}}}无证据能携带 mode。研究未确认 → 任务返回 [!] BLOCKED,不得猜测形状。"auto" 暂映射 bypassPermissionsPRD 标注待核。

B4.1 用户故事

  • US-B4.1(核心)开会话时选权限模式default/acceptEdits/plan/auto--permission-mode 传给 CLI。
  • US-B4.2plan 模式出计划要退出 planExitPlanMode手机弹三选一门approve+auto接受编辑直接干/ approve+review逐项确认/ keep planning继续规划,复用既有 held-permission 通道。

B4.2 功能需求

ID 需求 优先级
FR-B4.1 启动会话可选 permission mode映射 CLI --permission-mode <mode> P0
FR-B4.2 openProject/launcher 的 cmd 从 'claude\r' 升级为 claude --permission-mode <mode>\r(仅当选了非默认) P0
FR-B4.3 plan-gate 识别:/hook/permission 收到 tool_name==='ExitPlanMode'(待研究确认)时标记 plan gatepending 带类型标志 P0
FR-B4.4 协议扩展:ServerMessage.statusgate?:'tool'|'plan'ClientMessage approve 携带决策approve+auto / approve+review / keep planning P0
FR-B4.5 审批栏(updateApprovalBarplan gate 时渲染三按钮;普通 tool gate 仍两按钮 P0
FR-B4.6 决策→ClaudepermDecision 扩展为写回带目标 permission mode 的 decision JSON形状待研究确认plan 批准后切 acceptEdits/defaultkeep planning=deny/留 plan P0
FR-B4.7 模式选择持久化localStorage后台 tab 命中 plan gate 触发既有通知(依赖 A1M5 P1
FR-B4.8 "auto"(绕过权限)需 UI 二次确认 + 视觉警示(高危),可 env 全局禁用 P1
FR-B4.9 statusLineB2若上报 permission mode则 tab chip 显示当前模式 P2

B4.3 设计与数据流

启动 (launcher/卡片) ── 选 mode ──► cmd = `claude --permission-mode <mode>\r`(复用 openProject 链路)
                                  ── plan 运行 ──►
Claude 出计划 → ExitPlanMode → PermissionRequest hook (held)
   ▼ POST /hook/permission (isLoopback, :277)
   ├ tool_name==='ExitPlanMode'(待核)→ gate='plan'park 进 pendingApprovals (:299)
   └ manager.handleHookEvent(id,'waiting',tool,pending=true, gate='plan') 广播
   ▼ public/terminal-session.ts status case 记 gate仿 :269-275
   ▼ public/tabs.ts updateApprovalBar (:193) — plan 时 3 按钮:
   ├ approve+auto    → ws {type:'approve', mode:'acceptEdits'} → permDecision(allow, mode)
   ├ approve+review  → ws {type:'approve', mode:'default'}     → permDecision(allow, mode)
   └ keep planning   → ws {type:'reject'}                      → permDecision(deny)(留 plan
   ▼ resolvePending 写回 decision JSON 给 curl→Claude (:116-122, :61-63)
  • 新类型/协议(src/types.tstype PermissionMode = 'default'|'acceptEdits'|'plan'|'auto'ClientMessage approve |= {type:'approve', mode?:PermissionMode}ServerMessage.statusgate?:'tool'|'plan'permDecision(behavior, mode?)形状待核)。

B4.4 安全考量

  • loopback-onlyplan gate 仍走 /hook/permissionisLoopback),决策只能由本机 Claude 取走。
  • 多设备一致性:保持"最后一个观看者离开才释放 hold"src/server.ts:472-474plan gate 同理。
  • "auto"/bypassPermissions 高危等于无人值守全自动改盘——UI 二次确认 + 显著警示 + ALLOW_AUTO_MODE 全局可禁用。
  • 未信任决策输入ws 的 modeparseClientMessage 边界白名单校验仅四枚举非法降级为最保守default 或 deny
  • 超时回退不变plan gate 超时回退到 Claude 自身交互提示(cfg.permTimeoutMs),不卡死。

B4.5 配置

Env 默认 说明
DEFAULT_PERMISSION_MODE default 未选时启动会话用的模式
ALLOW_AUTO_MODE 0 是否允许 "auto"bypassPermissions默认禁高危全自动
(复用)PERM_TIMEOUT_MS 300000 plan/tool gate hold 超时,沿用既有(src/config.ts:32)。held-curl --max-time 必须 > 此值L5

B4.6 暂不做:自定义 per-tool 细粒度 allow 规则集、会话运行中动态切模式(除 plan gate 结果切换外)、非 Claude agent 的权限模式hook/plan 目前 Claude 专属,标注未来中立化)。

B4.7 验收标准

  • AC-B4.1:选 plan 模式开会话 → 实际以 claude --permission-mode plan 启动。
  • AC-B4.2plan 出计划触发门 → 手机审批栏出现按钮approve+auto 后以 acceptEdits 继续keep planning 则留 plan。
  • AC-B4.3:普通 tool gate 仍两按钮(无回归);多设备下关一台不取消他人门;超时回退到 Claude 自身提示。
  • AC-B4.4ALLOW_AUTO_MODE=0 时 UI 不提供/禁用 auto非法 mode ws 帧被边界拒绝并安全降级。
  • AC-B4.5permDecision(mode)、cmd 构造、parseClientMessageapprove.mode 的校验均有单测;/hook/permission plan-gate 路径有集成测试。
  • AC-B4.6H5:研究 spike 已确认 --permission-mode 值集、ExitPlanMode 投递路径、decision-JSON 携带 mode 的形状(或记录无法携带、改走替代方案)后方可实现。

5. 合并安全章节 (Consolidated Security)

整体守 byte-shuttle/LAN 哲学(带外 JSON、git 子进程、前端-only A2/A3、无服务端 ANSI 解析)。逐风险归并:

风险类 涉及特性 缓解(落到代码)
CSWSH/CSRF状态变更 A1(/push/subscribe,/hook/decision)、B3(/projects/worktree) 全挂 requireAllowedOriginsrc/server.ts:215-222)。/hook/decision 是唯一远程非-loopback 的敏感写端点,额外要能力 token
loopback 伪造 A1 hold 门、A4 hook 摄入、B2 /hook/status、B4 plan gate 摄入端点全过 isLoopback:70-78statusLine/hook/permission 进程恒在本机
能力 token最小授权非 app auth A1 /hook/decision、A5/B4 经 A1 token 绑定到 session+当前 pending 决策、resolve/超时即失效M1失配/陈旧 → 403限流
安全上下文不可用 A1 Web Push !isSecureContext 检测 + UI 文案;Band A1 需 HTTPSTailscale Serve/TLS;裸 LAN 回退到 ntfy/Pushover 桥C2
秘密泄露 A1 VAPIDenv、不入日志、缺失降级、A1 ntfy tokenspawn env 引用,不入 settings.json/argvH3 env-only + 启动校验 + 600 文件权限;订阅库绝不经 GET 暴露
命令注入 B1 diff、B3 worktree execFile('git', [...]) 无 shell + -- 终结选项;用户值作独立 argv
路径越界 B1、B3 绝对+isDirectory+hasGitEntry 三连B3 额外 realpath 两端 + 前缀包含M2+ 分支 sanitize 为目录段
分支名/ref 注入 B3、B1(base) validateBranchNamegit ref 规则子集);basegit rev-parse --verify 白名单
XSS未信任工具/文件内容) B1 diff、B2 遥测、A3 标签、A4 时间线 一律 textContent/el(),禁 innerHTML 渲染外部内容CSP script-src 'self' 兜底SP7
未信任 JSON A4 hook body、B2 statusLine、B4 mode unknown + 逐字段收窄、never throwwhitelist 事件名、截断长度、sanitize 控制字符
DoS / 资源 A1(PUSH_MAX_SUBS)、A4(TIMELINE_MAX 环)、A5(每回合一告警)、B1(timeout/maxBuffer/文件封顶)、B2(只存最新一条)、B3(timeout/数量上限) 见各特性配置
告警风暴 A5、A1 tag 去重 + 每 stuck 回合至多一次A5-FR2
供应链 A1 web-push 新运行时依赖锁版本、纳入安全清单L1
隐私(音频外流) A2 文档明示 Chrome Web Speech 流向 Google服务端永不接触音频AC-A2.5L2
timeout 配置一致性 A1/B4 hold held-curl --max-timesetup-hooks.mjs:36 现为 350必须 > cfg.permTimeoutMs(现 300纳入 config 校验,避免裸 350 字面量L5

6. 合并配置 env 表 (Consolidated Config)

命名约定SP8 / L4服务端 cfg 读 = 无前缀spawn 注入 / hook 脚本读 = WEBTERM_ 前缀。全部经 src/config.tsparse* 帮手解析秘密缺失优雅降级never 硬编码。

服务端 cfg env无前缀

Env 默认 特性
VAPID_PUBLIC_KEY / VAPID_PRIVATE_KEY / VAPID_SUBJECT 未设→push 关 / —(秘密)/ mailto:admin@localhost A1
PUSH_STORE_PATH <homeDir>/.web-terminal/push-subs.json A1
PUSH_MAX_SUBS 50 A1
NOTIFY_DONE 1 A1
NOTIFY_DND 0 A1
DECISION_TOKEN_TTL_MS =PERM_TIMEOUT_MS A1
TIMELINE_MAX 200 A4
TIMELINE_ENABLED 1 A4
STUCK_TTL 600(秒) A5
STUCK_ALERT 1 A5
DIFF_TIMEOUT_MS 2000 B1
DIFF_MAX_BYTES 2097152 B1
DIFF_MAX_FILES 300 B1
STATUSLINE_TTL_MS 30000 B2
WORKTREE_ENABLED 1 B3
WORKTREE_ROOT <repo>-worktrees B3
WORKTREE_TIMEOUT_MS 10000 B3
DEFAULT_PERMISSION_MODE default B4
ALLOW_AUTO_MODE 0 B4
(复用,校验 --max-time > 之)PERM_TIMEOUT_MS 300000 A1/B4 (L5)

spawn 注入 / hook 脚本 envWEBTERM_ 前缀)

Env 来源 特性
WEBTERM_STATUSLINE_URL spawn 按 cfg.port 注入(session.ts B2
WEBTERM_NTFY_URL / WEBTERM_NTFY_TOPIC / WEBTERM_NTFY_TOKEN spawn/hook envtoken 不入 settings.jsonH3 A1
WEBTERM_PUSHOVER_TOKEN / WEBTERM_PUSHOVER_USER spawn/hook env A1

7. 优先级与跨特性依赖排程 (Prioritization & Dependency Ordering)

C3 修正(最重要的排程约束):原 Band A、Band B 两张任务表都各自宣称独占 src/types.tssrc/server.tssrc/session/manager.tspublic/tabs.tspublic/terminal-session.tsscripts/setup-hooks.mjs——违反 Owns: disjoint 不变式。本节合并为一个跨 Band 波次计划,每个共享文件只有一个 owner 任务;其余为 disjoint 新文件,可并行。沿用 v0.6 做法(docs/FEATURE_PROJECT_MANAGER.md §7)派给 module-builder,建议 isolation: worktreeTDD ≥80%。

7.1 优先级与"为什么这个顺序"

  1. R0 研究 spike阻断H5:核实 statusLine schemaB2--permission-mode 值集 + ExitPlanMode 投递 + decision-JSON 能否带 modeB4未确认则 B2/B4 的类型不得在 T-types 冻结,相关任务返回 [!] BLOCKED
  2. A1 通知服务先行M5A5、B4-FR7 都消费 A1 的 notifyService/push 通道——provider 必须先落地,排程不得把消费者排在 provider 前。
  3. B1只读 diff:纯复用、低风险、高价值,可与 A 段并行早做。
  4. A2/A3前端-only:独立、低风险,任意时段可落。
  5. A4时间线:依赖对 hook 摄入的扩展H2
  6. B3worktree:唯一写盘、安全最重,但与通知链独立,可并行推进。
  7. B2遥测:受 R0 阻断;类型确认后并入。
  8. B4plan 中继):受 R0 阻断 + 依赖 A1背景 tab plan gate 通知M5

7.2 共享文件 → 单一 ownerC3 核心)

共享文件 唯一 owner 任务 合并了哪些特性的改动
src/types.ts T-types 全部新共享类型A1 token/订阅、A4 TimelineEvent、B1 DiffResult、B2 StatusTelemetry+ServerMessage、B3 CreateWorktreeResult、B4 PermissionMode+协议扩展、各 Config/Session 字段)
src/config.ts T-config 全部新 env 解析 + L5 的 --max-time>permTimeoutMs 校验
src/session/manager.ts T-manager A4 时间线 append、B2 telemetry 存+广播、A5 sweepStuck()(H4)、B4 gate flag、manager↔notifyService 接线(M5)
src/session/session.ts T-spawn-env B2 WEBTERM_STATUSLINE_URL + A1 WEBTERM_NTFY_* 注入
src/http/hook.ts + /hook 路由 T-hook-intake A4 扩展摄入(保留时间戳/tool_inputH2、B4 gate 识别协调
src/server.ts T-server-wire A1 /push/*+/hook/decision+改 hold 门(C1)、A4 /…/events、A5 reaper 加 sweep 回调、B1 /projects/diff、B2 /hook/status、B3 /projects/worktree、B4 扩 /hook/permission gate + approve mode
scripts/setup-hooks.mjs T-hooks-installer A4 补 PostToolUse(H1)、A1 ntfy/Pushover 桥(env tokenH3)、B2 statusLine 段、L5 --max-time 取自/校验于 config
public/tabs.ts T-tabs B2 仪表、A5 stuck 徽标、B4 三按钮审批+mode 选择+cmd、A2/A3/A4 面板挂载点、§SP10 共享状态/仪表组件 + 单一 refreshTab owner(M4)
public/terminal-session.ts T-termsession B2 telemetry 捕获+onTelemetry、B4 gate 捕获
public/projects.ts T-projects-ui B1 diff 入口、B3 New-worktree 表单
public/sw.js T-sw A1 push/notificationclick(保留不拦 /term//hook

7.3 Disjoint 新文件任务(可并行)

任务 Owns新文件 特性
N-push src/push/push-service.ts + src/push/subscription-store.ts (+ test) A1
N-timeline src/session/timeline.ts(有界环/标签/sanitize(+ test) A4
N-diff-be src/http/diff.ts (+ test) B1
N-statusline-be src/http/statusline.ts (+ test) B2
N-worktree src/http/worktrees.ts(加 createWorktree/validateBranchName/computeWorktreeDir,与 listWorktrees 同文件)(+ test) B3
N-statusline-script scripts/statusline.mjs B2
N-push-ui public/push.ts(订阅流 + 🔔 + 决策 fetch A1
N-voice public/voice.ts A2
N-quickreply public/quick-reply.ts A3
N-timeline-ui public/timeline.ts A4
N-diff-ui public/diff.ts B1

7.4 波次 (Waves)

  • W-1阻断研究R0B2/B4 schema 确认)。先于 T-types 对 B2/B4 字段的冻结。
  • W0协调点T-types → 然后 T-configT-spawn-env
  • W1并行新文件 + 纯核心)N-pushN-timelineN-diff-beN-statusline-be(待 R0N-worktreeN-statusline-scriptT-hook-intake、以及前端 N-voiceN-quickreplyN-diff-uiN-timeline-uiN-push-uiT-sw
  • W2后端汇聚单 owner串行避免 clobberT-manager(依赖 N-push/N-timeline/统计核心)→ T-server-wire(依赖全部 be 任务 + T-managerT-hooks-installer
  • W3前端汇聚单 ownerT-termsessionT-tabs(含 SP10 共享组件)、T-projects-ui
  • W4验证报告-only:集成/E2E真起 server、临时 repo、HTTPS 上下文模拟 push 注册),按 §8 汇总验收;缺陷回路给对应 owner不跨 lane 改文件)。

7.5 显式依赖边

  • A5 → A1notifyServiceB4-FR7 → A1(背景通知);B2/B4 → R0schemaT-server-wire → {N-push, N-diff-be, N-statusline-be, N-worktree, T-hook-intake, T-manager}T-tabs/T-termsession/T-projects-ui → {T-server-wire + 各 N-*-ui}T-config/T-manager/...→ T-types

8. 验收标准汇总 (Acceptance-Criteria Summary)

特性 关键验收(含本次评审修正点)
A1 零 tab 走开时锁屏 Allow/Deny 即解决 held 请求(C1, AC-A1.2);不安全上下文隐藏 🔔 并提示需 HTTPS(C2, AC-A1.5)VAPID 未设 503/隐藏不崩ntfy token 不入 settings.json/argv(H3, AC-A1.7);陈旧/外来 Origin/坏 token → 403(M1, AC-A1.8)DONE 低优通知
A2 按住 🎤 说话→打进终端;软键盘不弹;无 API 隐藏不崩;无音频抵达服务端(L2)
A3 内置 chips + 可持久调色板,同键栏路径发送;标签惰性文本(无注入)
A4 离散人话时间线;/…/eventsTIMELINE_MAX 界;安装器含 PostToolUse(H1)hook body sanitize 后显示(SP7)TIMELINE_ENABLED=0
A5 静默达 STUCK_TTL 经 A1 恰发一次;恢复后重新武装;扫描覆盖 attached 会话且无新 timer(H4)0 禁用
B1 结构化 diff + numstat 一致working/staged非 git→404超大→truncated 不 OOM<script> 作文本(SP7)纯解析单测≥80%untracked 经 porcelain 标注(L3)
B2 tab 仪表;晚加入设备 attach 立即收到遥测(M3, AC-B2.3);缺字段宽容/陈旧置灰非本机→403telemetry 序列化往返测试R0 已确认字段(H5)
B3 WORKTREE_ROOT 建出并开 agent缺/伪造 Origin→403非法分支名拒绝不执行realpath 后仍拒 symlink 绕过(M2);并行不互踩;纯函数+集成测试
B4 plan 模式启动plan 门三按钮且 approve+auto 切 acceptEditstool gate 仍两按钮无回归;多设备一致;ALLOW_AUTO_MODE=0 禁 auto非法 mode 边界拒绝R0 已确认 schema(H5)
跨特性 共享文件单 owner 无 clobber(C3)env 命名约定(L4)held-curl --max-time>permTimeoutMs 经 config 校验(L5);共享状态/仪表组件无 DOM 争用(M4)

全局门槛所有纯核心diff/statusLine/timeline 解析、branch/dir 校验、push payload、label 派生)单测 ≥80%;端点经集成测试(真起 serverA1 push 在模拟 TLS/安全上下文下注册成功。


9. 一句话总结 (One-line Summary)

v0.7 走开即工作台A 段靠"hook 触发 → 出站推送 → 锁屏决策回 held 通道"闭合走开循环核心修正hold 门要在零客户端但有推送订阅时也 hold且 push 需 HTTPSB 段靠"只读 git-execdiff+ 受控写 git-execworktree+ loopback JSON 摄入statusLine+ held-permission 新 gateplan"在终端之上长出工作台。服务端仍不解析一个终端字节9 个特性合并为一个单-owner-per-shared-file 的波次计划B2/B4 受一个阻断式 schema 研究 spike 把关。