Files
web-terminal/docs/PROGRESS_LOG.md
Yaojia Wang aa1912b962 feat(relay): Phase1 waves A2-E — server entry, shared-store data plane, agent runtime, deploy artifacts
RELAY-PHASE1 Wave A2/B/C/D/E (12-agent workflow, all tsc-clean, 314/314 tests pass):
- A2: control-plane server.ts entry + boot/redis.ts revocation-bus wiring + start script.
- B1: relay-run shared-store EnforceDeps (relay-auth ports over the SAME Postgres+Redis as P3).
- B2: registry-backed MtlsVerifier (verifyAgentCert, fail-closed, INV14).
- B3: store-backed RouteResolver (subdomain->hostId).
- B4: Redis relay:revocations subscriber -> tunnel teardown (INV12).
- B5: main-phase1.ts production entry (public bind, real TLS, async-mTLS prefetch bridge) + staging /auth/mint.
- B6 (PARTIAL): relay-web operator login + browser DPoP; proof offered via term.dpop.<b64u> subprotocol.
- C: agent dist/cli.js build (esbuild) + runTunnel run-loop + CliDeps.
- D1: same-origin static serve of relay-web/public from the browser WSS.
- E: systemd units + gen-ca/gen-capability-key/issue-tls-cert scripts + deploy/RUNBOOK.md.
Adversarial review: all hard invariants PASS. Follow-ups (B7): close DPoP-subprotocol read on
browser-server (blocks browser connect); rate-limit /auth/mint (F1); wire activeSessionCount (F2);
scrub error logs (F5). Excludes unrelated public/style.css (concurrent iOS job).
2026-07-06 16:13:34 +02:00

666 lines
114 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Web Terminal 实施进度日志 (Progress Log)
> **用途**: 记录 [PLAN.md](./PLAN.md) 各任务的完成情况,作为**跨会话记忆文件**。
> 新会话开工前**必读本文件**。只记**已发生的事实**(含失败、跳过),不预填未来,不写愿望。
> **谁来写(G1)**: 本文件**仅由 orchestrator(主会话)写入** —— 它不在任何任务的 `Owns:` 里,
> 防止并行 subagent 并发写冲突。被派的 subagent **不写本文件**,而是把日志条目作为返回结果交回,
> 由 orchestrator 在每批 agent 返回后**立即追加**(不要攒到最后)。
> 完整维护规则见 [CLAUDE.md → Development Workflow](../CLAUDE.md)。
---
## 状态图例
| 标记 | 含义 |
|------|------|
| `[ ]` | TODO — 未开始 |
| `[~]` | IN PROGRESS — 进行中 |
| `[x]` | DONE — 完成且已验证 |
| `[!]` | BLOCKED — 受阻(须在详细日志写明阻塞原因) |
---
## 当前焦点 (Current Focus)
> 新会话读到的第一块。保持准确,只描述"此刻"。
### 🚧 RELAY-PHASE1 — 把原生 rendezvous-relay 部署到单台 VPS(8.138.1.192,阿里云;2026-07-06)
- **目标**: DEPLOY_RELAY §4 的 **Phase 1「1b 全量持久化 staging」**。VPS 上 Docker 自建 Postgres+Redis;已备案域名走 443 + Let's Encrypt;agent 跑在操作者本机向外拨号。完整文件级方案见 [PLAN_RELAY_PHASE1.md](./PLAN_RELAY_PHASE1.md)。
- **进度**: **Wave A/B/C/D/E 代码全部完成并通过 Verify** —— 一个后台 Workflow(12 agents,0 error)跑完 A2、B1B5、B6、C、D1、E。Verify 门槛:**四个包 tsc 全干净、可构建包全 build 成功、314/314 测试通过**(control-plane 103 / relay-run 74 / agent 137 + relay-web 118)。对抗式安全评审:**所有硬不变量 PASS**(INV2 opaque splice、Origin/CSWSH 精确匹配、mTLS INV14 registry 门控、token-mint 密码学、撤销 INV12、jti 单次、参数化 SQL)。
- **各任务**: A2 P3 server 入口+Redis 总线 `[x]`;B1 共享 store EnforceDeps `[x]`;B2 registry-backed MtlsVerifier `[x]`(异步阻抗由 B5 的 `bridgeAsyncMtls` per-DER 预取桥无锁解决);B3 store RouteResolver `[x]`;B4 撤销订阅 `[x]`;B5 `main-phase1.ts` 组合入口 + staging `/auth/mint` 签发 `[x]`;C agent `dist/cli.js` 构建+`runTunnel` 运行入口 `[x]`;D1 同源托管 relay-web `[x]`;E systemd+脚本+RUNBOOK `[x]`;**B6 relay-web 登录/DPoP `[~] PARTIAL`**。
- **两个待收尾(B7)**: ① **功能阻塞** — 浏览器 WS 无法设请求头,B6 把 DPoP proof 放进 `term.dpop.<b64u>` 子协议,**relay 侧(browser-server.ts)必须从子协议读取并喂给 `UpgradeRequest.dpop`**,否则真实浏览器一律被 DPoP 拒(401)。② **F1 安全** — 公网 :443 上的 `/auth/mint` 无限速/锁定(评审最大攻击面);顺带 F2(`activeSessionCount` 硬编码 0,并发上限失效)、F5(裸 error 日志或泄露 DSN)。F3/F4(撤销后 agent 空转重连、slowloris)为 LOW,记入 Phase 2 backlog。
- **远端(需 VPS 执行,见 `deploy/RUNBOOK.md`)**: DNS、Let's Encrypt 签证、私有 enrollment CA + capability keypair 生成、阿里云安全组放行 443/AGENT_PORT、systemd 起两个服务、端到端 enroll→dial→浏览器点进 shell。
- **未做/推迟到 Phase 2**: 真 KMS、F6 replay、WebAuthn step-up(staging 用 `NO_STEPUP_POLICY`)、通配多租户、F3/F4。
- **未做/推迟到 Phase 2**: 真 KMS(dev signer 暂用)、F6 replay(`loadReplay` fail-closed)、WebAuthn step-up(staging 用 `NO_STEPUP_POLICY`)、通配多租户。
#### [x] A1 — Postgres store 适配器(P3)+ 迁移运行器(2026-07-06)
- **交付**: `createPgStores(query): Stores` 把 9 个 store 端口映射为参数化 SQL(零字符串拼接);`runMigrations(query)` 按文件名顺序跑 `db/migrations/*.sql`(幂等)。
- **文件(全在 Owns 内)**: `control-plane/src/store/pg.ts``src/db/migrate.ts``db/migrations/0002_routes.sql`(新增 — 0001 无 routes 表)、`test/store/pg.test.ts`
- **语义对齐 memory.ts**: INV8 accounts/hosts `swapStatus` 用单条 **writable CTE**(UPDATE 指针 + INSERT 版本行)原子完成,无显式事务;重复插入捕获 pg `23505``throw duplicate`;`casRedeem` 单赢家 `UPDATE...WHERE redeemed_at IS NULL RETURNING`;RouteStore 存 `expires_at`,`expires_at<=now()` 视为不存在(fail-closed INV7)+ 惰性 DELETE。
- **验证(实测)**: `npx vitest run test/store/pg.test.ts`**23/23 通过**(自起 `postgres:16` 一次性容器 :5433,afterAll 自动 `docker rm -f`);`pg.ts` 覆盖 **96.72% stmts / 100% funcs**
- **偏差**: ① `0002_routes.sql` 新增;② SubdomainStore 无独立预留表,按 `hosts.subdomain UNIQUE` 归属裁决;③ migrate 按 `;` 切分逐条执行(参数化路径拒绝多语句)。**阻塞**: 无。
#### [x] A3 — 真能力验证器(relay-auth verifyCapabilityToken)+ 同步→异步 seam(2026-07-06)
- **交付**: 抛异常的 `refuseAllVerifier` stub 换成委托 P5 异步 `verifyCapabilityToken` 的真验证器;`await` 贯通整个 authorizer 路径。默认仍 fail-closed(异步化后 reject→401)。
- **文件**: `src/api/authz.ts`(`CapabilityVerifier.verify``Promise`)、`src/api/provision.ts`(5 个路由 call site 改 `await principal(req)`)、`src/boot/verifier.ts`(新;`configureCapabilityVerifyKey``env.capabilitySignPubkey` 32 字节导入 WebCrypto verify key 后调 relay-auth `configureVerifyKey`,桥接 `CAPABILITY_SIGN_PUBKEY_B64` vs `RELAY_AUTH_VERIFY_PUBKEY` 命名差)、`src/main.ts``package.json`(加 `relay-auth: file:../relay-auth`)、`test/verifier.test.ts`(新)+ 更新 api 测试 stub 为异步。
- **验证(实测)**: `npx tsc --noEmit` 全项目干净;`npx vitest run` **15 files / 101 tests pass**
- **偏差**: 手建 `control-plane/node_modules/relay-auth` 符号链接以配合新 file: 依赖(`npm install` 会重建)。**阻塞**: 无。
### ✅ 修复:底部快捷键栏遮挡终端内容(iOS 安全区,2026-07-06,main)
- **现象(用户截图,iPhone)**: 底部快捷键栏(Esc/Esc²/⇧Tab…)压住终端最后一行(Claude Code "bypass permissions" 提示行只露上半)。
- **根因(`public/`)**: `index.html` 设了 `viewport-fit=cover`(布局铺满物理屏、延伸进刘海/Home 指示条),但 `style.css` **全程没有任何 `env(safe-area-inset-*)` 补偿**。于是 `#keybar`(`fixed; bottom:0; height:--keybar-h`)整条压进 Home 指示条区,而 `#term` 只预留了裸 `--keybar-h`,末行与键栏相撞。教科书级 cover-无-safe-area 缺陷。
- **修复(纯 CSS,零 JS)**: `:root` 新增 `--safe-t/--safe-b = env(safe-area-inset-top/bottom, 0px)`,穿过所有贴边固定元素:`#tabbar` 顶部让出刘海(border-box + `padding-top:--safe-t`)、`#term` inset 改 `calc(--tabbar-h+--safe-t) … calc(--keybar-h+--safe-b)``#keybar` 高度 `calc(--keybar-h+--safe-b)` + `padding-bottom:--safe-b`(芯片留在顶部 --keybar-h,Home 条上方)、`#approvalbar``body.home-open #term` 底部、`#searchbox`/`#settingspanel` 顶部同步。inset=0 时全部塌回原值 → 桌面/无安全区平台**零回归**。
- **验证(headless,390×844,注入 `--safe-t:59px --safe-b:34px` 模拟 iPhone)**: 开一个真实会话后量测 `term_bottom==keybar_top==764`(gap=0,无重叠)、`tabbar_bottom==99`(=40+59,让出刘海);截图确认芯片位于 Home 指示条上方、终端不再被遮。`npm run build:web` 通过。
- **待办**: 未提交;未在真机 iPhone 上复验(仅 headless 模拟安全区)。
### ✅ 修复:项目面板把父文件夹当项目 & 会话点亮所有祖先项目(2026-07-06,分支 `feat/ios-client`)
- **现象(用户截图)**: 只在 `web-terminal` 跑了一个会话,但 "Active now" 同时显示 `web-terminal`/`Documents`/`yiukai` 三张卡,且父文件夹本身被列为项目。
- **根因(`src/http/projects.ts`)**: ① `belongsTo` 纯前缀匹配 → 会话按 cwd 归属到**每一个**祖先项目;② 历史合并(`mergeHistory`)把曾经跑过会话的 cwd(如 `~``~/Documents`)原样列为项目。
- **修复(TDD,先红后绿)**: ① 新增 `assignSessions` —— 每个 live 会话只归属**最深**的包含项目(`buildProjects` 改用;`buildProjectDetail` 保留前缀匹配,单项目详情页语义不变);② 新增 `dropParentFolders` —— 丢弃"非 git 且包含其他已列项目"的父文件夹条目(discovery 阶段过滤,进缓存)。
- **验证**: 新增 3 测(最深归属/父文件夹剔除/独立非 git 历史项目保留);`test/projects.test.ts` 29/29 绿;全量 `npm test` 53 files / **1473** 全绿;`tsc --noEmit` 干净。前端零改动("Active now" 分组消费 `sessions[]`,服务器修正后自然收敛)。
### ✅ 全端应用图标 "Orbit" + 托盘模板图(完成 — 2026-07-06,main,commit 86d100a)
用户选定 "Orbit"(节点绕方块光标核环绕=agent 自主替你运行),品牌沿用桌面 `--accent #E3A64A` 琥珀金 + 暖近黑 `#100F0D`。矢量母版三份(App 圆角暖底 / iOS 满幅不透明 / 托盘纯黑透明加粗),`rsvg-convert`(`brew install librsvg`)栅格化铺各端:
- **iOS/iPad**: `Assets.xcassets/AppIcon.appiconset`(单尺寸 1024 不透明,Xcode 自动生成全套;此前**无图标**)。构建验证 `AppIcon60x60@2x`/`76x76@2x~ipad` 入 Assets.car;模拟器主屏实拍确认。
- **桌面 App**: `desktop/resources/icon.png`(圆角 1024)→ `npm run dist:mac` 重打包出新 icns + 156MB DMG(`dist-app/Web Terminal-0.1.0-arm64.dmg`),顺带带上 projects 修复。装机需 `xattr -cr` 清 quarantine(ad-hoc 签名 Gatekeeper)。
- **桌面托盘**: `trayTemplate.png`/`@2x` 换成正确的 macOS **模板图**(黑形状+透明底,系统按亮/暗菜单栏自动染色)——根治"一坨白"。
- **web/PWA**: `public/icon.svg` 矢量。**待办**: 分支/main 未推送;图标对比页(5 创新+5 经典)在 job scratchpad 未入库。
### ✅ iOS 客户端全面 UX/UI 打磨(完成 — 2026-07-05,分支 `feat/ios-client`,commit 660a404;精致原生方向)
用户定方向「精致原生 + 可重构」(靛紫 #7C8CFF 强调、语义状态色、SF Mono 数字、系统材质)。编排 = 一个 ultracode `Workflow`(9 agents,4 阶段):3 路审计(易用性/视觉/无障碍,各 boot 两 sim 取证)→ **冻结 DesignSystem**(协调点,防各 agent 各挑配色)→ 4 组文件互斥并行套用 → 验收。
- **审计**: 3 视角共产出 ~40 findings,共识根因=「无设计系统」——全 App stock 系统蓝、状态仅靠颜色(违反色+形,idle 蓝撞 unread 蓝)、gate 决策按钮 <44pt间距/圆角魔数无刻度数字非 tabular缩略图纯黑块gate 中英混排reduceMotion 未处理
- **冻结 DesignSystem**(`ios/App/WebTerm/DesignSystem/**` 4 文件): Tokens(自适应 accent 靛紫语义 status 间距 2-24 刻度圆角 sm8/md12/lg16Motion gated by reduceMotionHaptics)、Typography(SF ramp + mono tabular)、StatusStyle(status+SF Symbol+中文,单一真源,+)、Primitives(StatusBadge/TelemetryChip/Card/SectionHeader/DSButtonStyle 可复用)。 `.tint(DS.Palette.accent)` 单点注入
- **4 组套用( DONE,纯视觉零逻辑改动)**: G1 列表行重构+状态徽标+telemetry 芯片+缩略图占位;G2 终端 gate (≥44pt approve/reject)+SwiftTerm accent 主题+KeyBar/reconnect/quick-reply/digest;G3 配对 hero+警告分层视觉化+Projects 卡片+Timeline/Diff;G4 导航 chrome+iPad 分栏占位(accent 图标)+隐私遮罩+动效+`ContinueLastBanner`/`ProjectsToolbarItem` 抽共享组件gate 文案落中文
- **验收 PASS_WITH_FINDINGS,设计评分 8.5/10,1 finding**: TimelineSheet class色仍裸色(全改动里唯一越界)→ orchestrator `DS.Palette.timelineTool/timelineUser` token + TimelineSheet 全走 DS + 更新钉旧裸色的测试
- **验证(亲验)**: iPhone 16 **290** + iPad Pro 11 **290** 全绿( finding );+集成绿;iPad 分栏占位截图确认靛紫强调色生效。**未推送**。**待用户目视**: 会话列表行/gate /配对页真机观感(截图受通知授权弹窗遮挡,建议真机或点过授权后看)。iPad 侧栏 List(selection:) 高亮审计标记为列表组 Owns 后续 pass」——当前 onOpen 已驱动切换,视觉选中高亮可再补
### ✅ iOS 客户端 iPad 适配(完成 — 2026-07-05,分支 `feat/ios-client`;自适应布局,非分叉)
`docs/PLAN_IOS_IPAD.md`(5 人日)编排:W0 orchestrator 亲做 一个 ultracode `Workflow`(4 agents: T-iPad-2 核心串行 T-iPad-3 T-iPad-4 T-iPad-5 验收)。
- **[x] T-iPad-1 device family(orchestrator 亲做,commit 77502ec)**: `TARGETED_DEVICE_FAMILY` target `1``1,2`;iPad `~ipad` 四方向(iPhone 不动);ios.yml iPad Pro 11 测试腿实测:产物 `UIDeviceFamily [1,2]`iPad 原生满屏(不再 iPhone 黑边)。
- **[x] T-iPad-2 AdaptiveRootView + NavigationSplitView + LayoutPolicy(核心)**: `LayoutPolicy.mode(hSizeClass)` 唯一 size-class 决策点(regularsplit/compactstack,仿 PrivacyShadePolicy);`AdaptiveRootView` 把横切布线(**隐私遮罩 ZStack 顶层两分支共享**/scenePhase/deepLink/sheets)上提唯一一处;`StackRootView`= RootView body **原样搬迁**(iPhone 字节级零回归);`SplitRootView`= SessionListScreen(复用不改)+ TerminalContainerView(复用不改, `.id`);AppCoordinator SidebarItem 路由桥(非平行生命周期)。
- **[x] T-iPad-3 KeyBar 自适应 + 指针右键菜单**: `KeyBarVisibility` 纯谓词(硬件键盘在场默认隐);`TerminalContextMenu`(iPad idiom,复制选区/开新会话/结束会话,一一路由既有通道);kill APIClient Origin 单测
- **[x] T-iPad-4 Projects 大屏化**: `ProjectsGridLayout.columnCount`(iPhone 1/iPad 按宽 1-3);iPad 多列网格复用同款 row/分组/prefs 逻辑零改;Timeline 已有 detent 两端合理。**决策**: Projects iPad form sheet 呈现,其内 hSizeClass compact 改用 `userInterfaceIdiom` 判据( size class 正交)。
- **[x] T-iPad-5 验收 PASS_WITH_FINDINGS orchestrator 修复 4/4**: **iPhone 零回归硬门守住**(277 App+261 +10 集成);iPad 分栏工作; CRITICAL/HIGH/安全回归修复:MED kill 菜单项无法渲染 `onKillSession` 贯穿 TerminalContainerView两根视图 + `AppCoordinator.killCurrentSession()`(复用 kill 通道);②MED split 未消费 route `AdaptiveRootView` `.loading/.pairing` 强制走 StackRootView(iPad 首启配对不被困在空态);③LOW 继续上次横幅补进 split sidebar(截图确认);④LOW iPad happy-path XCUITest **接受推迟**(split 选中逻辑已由 SidebarSelectionTests 9 测真 engine 覆盖,stack happy-path 未变,XCUITest 单跑 7-11min 且脆,成本不划算)。**验证(亲验)**: iPhone 16 **277** + iPad Pro 11 **278**(+kill 桥测试)全绿;iPad 分栏 sidebar+detail 截图确认
- **合计**: iPad 适配零改 `ios/Packages/**`/`src/`/`public/`;纯逻辑包设备无关直接复用。**待用户**: 真机 iPad(分栏手势/硬件键盘/指针右键/Stage Manager)DEFERRED;多窗口(拖会话开新窗并排两终端)另立下一期(本期单场景)。
### ✅ iOS 客户端 P1 实施(完成 — 2026-07-05,分支 `feat/ios-client`,P0+P1 共 14 commits,未推送/未合并)
- **[x] P1-A(W6+W7 首发: T-iOS-37 T-iOS-20 T-iOS-38,3 builders + verify 6/6 PASS;`/push/apns-token` 线形状由 orchestrator 预冻结,两侧并行编码零失配——verify 逐项对照 method/path/body key/hex 规则/状态码/守卫/限额全 match)**:
- **T-iOS-37 server lastOutputAt**: `LiveSessionInfo.lastOutputAt?: number` 可选增量 + `manager.list()` 一行映射 + 4 ;web grep 零消费者,tsc+全量双证兼容
- **T-iOS-20 server APNs**: `src/push/apns.ts`(576 )——env 三件套 all-or-disabled(缺失路由不挂载,启动不 crash,密钥材料零日志)、token store 照抄 subscription-store 约定(幂等/上限/0600)、**手写 ES256 JWT(node:crypto ieee-p1363,零新依赖)**、NEEDS-INPUT(priority 10+WEBTERM_GATE+capability token)/DONE(priority 5)、payload 最小化结构性保证(构造函数只读 meta.id,cwd/命令内容不可能出现)、410/BadDeviceToken 逐出`combineNotifyServices` web-push 并联互不阻塞;`hasPushTarget` 扩展( APNs token 也能 hold gate)。**65 **含双 e2e(本地 h2c APNs:held gate双通道同发同 token→/hook/decision 放行)。新增 `APNS_HOST` env(sandbox 切换+测试零网络)。**npm test 53 files/1470 全绿,tsc 干净**。
- **T-iOS-38 APIClient P1 契约**: APNs token builders(hex 校验镜像服务器联网前拒绝显式 ASCII 字符集防全角)、projects/detail(**证实线上无 namespace 字段**—— web 端分组概念;lossy 解码;percent-encode 单点+ + 规避)、prefs(**未知顶层键往返逐字节存活**—— iOS 覆写 web 偏好,关键陷阱已测;JSONValue 独立 integer case 4242.0)、公开四层 HostNetworkTier(W3 dedup 欠账,PairingVM 后续 pass 可切换)。76/76 绿,覆盖 95.26%。
- 遗留(LOW/cosmetic): Swift doc comment server 行号引用漂移;PairingVM 分类去重待后续 pass
- **[x] P1-B UI 九连链(222425272623292821 DONE,10 agents;verify BLOCKEDorchestrator 修复后放行)**:
- **T-iOS-22 DeepLinkRouter**: 全字段白名单(scheme/action/path /query 键精确/重复键拒)、UUID 复用冻结 Validation(v4);冷启动单槽 stash;`route(from:)` push-tap 复用;非法链接计数不回显内容(防日志注入)。21
- **T-iOS-24 Timeline sheet**: 呈现顺序逐行镜像 web render()(slice 50reverse);`[]`=disabled空态非错误;复用 AwayDigestView 既有 onExpand(T-iOS-14 零改动);label Text(verbatim:)。14
- **T-iOS-25 Quick-reply**: 内置 6 chips 逐项镜像 quick-reply.ts(Esc/Enter KeyByteMap 零手写转义);显示条件=持牌 gate(流上唯一 waiting 信号)&&!readOnly;UserDefaults lossy 载入16
- **T-iOS-27 Diff 查看器**: App DiffFetcher(RO Origin;并注记归 T-iOS-38 后续收编);hunks 着色/staged 切换/truncated 横幅/lazy list;diff 字节 verbatim 渲染
- **T-iOS-26 Projects**: 分组逐条镜像 projects.ts ** key web 逐字节一致**(collapsed /prefs 跨端共享,key 漂移=两端互丢折叠);**prefs clobber 防线**——GET 失败toggle 仅本地绝不 PUT(空底盘 PUT=清库)、PUT 采纳服务器 echo;`claude\r` bootstrap 镜像 tabs.ts:679
- **T-iOS-23 切换器**: UnreadLedger(单调水位,严格 >)+TitleSanitizer(C0/C1/DEL+bidi/零宽全表,字素簇截断 256)入 SessionCore(+15 包测);LiveSessionInfo.lastOutputAt 容错解码(授权增量);列表边界二次净化。
- **T-iOS-29 杂项**: new-in-cwd(cwd 不可信,非绝对路径按未知;**bootstrap 绝不复注入**——开新 shell≠起 claude,专项断言);exit 横幅"开新会话";**加建修复潜在 bug**: RootView 给容器补 `.id(controller.id)`(呈现中换 controller 不换 SwiftTerm 视图→终端假死)。
- **T-iOS-28 缩略图**: 离屏 SwiftTerm 渲染管线(LRU 32/并发闸 2/预览 256KiB 上限/网格钳制),缓存键 (sessionId,lastOutputAt),CADisplayLink 注销不泄漏。
- **T-iOS-21 PushRegistrar+锁屏**: WEBTERM_GATE category(Allow=.authenticationRequired、Deny 免认证 fail-safe、均无 .foreground,形状测试钉死);device token 对每 host 注册+记账重试;NotificationActionHandler(sessionId 复用 DeepLinkRouter 白名单、token 校验后仅入参绝不落盘、beginBackgroundTask 包 POST、403→本地通知兜底)。锁屏真机行为 DEFERRED(需付费账号)。
- **verify(独立 agent)**: 包 261(SessionCore 92/APIClient 77/…)+App 247+集成 10 全绿;7 项语义抽查全 PASS(含恶意标题/putPrefs 唯一调用点走保留路径/token 零持久化 grep);Owns 审计干净;**但抓到 CRITICAL——生产模式启动即崩**: @MainActor UNNotificationCenterAdapter 的 completion 闭包继承主 actor 隔离,UN center 在后台队列回调→Swift 6 executor 断言 trap(测试全绿盖不住:push 接线在测试宿主下被跳过——**测试盲区实证**)。**orchestrator 修复**: 三处闭包加 `@Sendable` 字面量标注(不继承隔离);重建+实机启动验证:进程存活、崩溃报告零新增、通知授权弹窗弹出(=走过原 trap 点)、弹窗期间隐私遮罩正确覆盖(scenePhase .inactive)。
- 遗留(LOW,归 P1-C 复核): deep link 未断言 userinfo/port(`webterminal://user:pass@open` 仍路由;id 均 v4 校验+host 只经 store 解析,影响可忽略);Diff fetcher 收编 APIClient 待后续 pass。
- **[x] P1-C 验收+安全复核(T-iOS-30,2 reviewer ∥,report-only)—— 双双 PASS,零 findings**:
- **Part A 功能验收**: 亲跑 261 包测+247 App 测+10 集成全绿;**启动崩溃修复复核 PASS**(3 次实机启动零崩溃,通知授权弹窗弹出=走过原 trap 点,崩溃报告计数停在修复前的 2);**`simctl push` 实测**注入 WEBTERM_GATE payload → 通知横幅真实渲染("Needs input / Session 3d34818d",目视确认最小化:仅状态词+8 位前缀,无 cwd/命令/token);F-iOS-15 服务端切片实测(双会话 `lastOutputAt` 差分正确);**意外收获**——对**你的真实服务器**(127.0.0.1:3000,pre-T-iOS-37 旧构建)实测旧版兼容降级:App 直接渲染真实会话列表(stuck 红点/`未知目录` 标题回退/`0 台设备在看 · 51×50` 与服务器精确一致/无 unread 点=正确降级)。真机项(锁屏 tap/Face ID、unread 点亮目视、OSC 标题目视)DEFERRED 附手工步骤。
- **Part B 安全复核(对抗式)**: APNs payload 最小化 post-P1-B 仍成立(OSC 标题不进 alert,grep 证实);**capability token 全链路 CLEAN**——issue(server randomUUID+TTL)→payload(仅 gate)→handler 校验后仅入参 hookDecision→**零落盘**(grep Push/ 及全 App 无 UserDefaults/Keychain/file 写;唯一持久 token 是内存态 device token)→单次消费(server resolvePending 一次性)→**两侧日志零 token**;deep-link 50+ fuzz 全部安全(userinfo/port LOW 判定 inert——从不用于构造请求、id 仍 v4 校验,继续接受);锁屏 Allow `.authenticationRequired` 形状测试绿;prefs 唯一写入点仍守空底盘;P1-B 新攻击面(缩略图字节只进 SwiftTerm、diff verbatim、标题净化覆盖全渲染点)扫盘无泄漏;残余风险台账完整。
- **P1 收官统计**: 累计 **518 项自动化**(261 包测 + 247 App 测 + 10 集成)+ 服务器 1470 测全绿;新增能力=远程 APNs 推送(锁屏 Allow/Deny+Face ID)、deep link、Projects 项目列表(分组/收藏/在仓库起 Claude)、多会话切换器(unread+OSC 标题净化)、Timeline/Diff/Quick-reply/缩略图;服务器仅 §0.3 声明的两处增量触点(lastOutputAt + APNs)。**待用户**: ①真机走查(T-iOS-30 报告手工清单);②Apple 付费账号(APNs 真机端到端+TestFlight);③推送分支跑首轮 GH Actions(ui-test 腿/ios17 runtime);④P2(语音 PTT/worktree/终端内搜索/主题/web ?join= 互通)是否开工——P2 任务需先按 P0 规格扩写 RED 清单再分派(§7 P2 注)。
### ✅ iOS 客户端 P0 实施(完成 — 2026-07-05,分支 `feat/ios-client`,7 commits,未推送/未合并)
按 PLAN_IOS_CLIENT §8 批次推进;编排 = 每波一个 ultracode `Workflow`(builder 按任务派发 + 独立 verify agent 复跑验收)。
- **[x] W0 基础(T-iOS-1/2/3/4,5 agents,verify 6/6 PASS)**:
- **T-iOS-1 脚手架**: `ios/` 全结构(project.yml/XcodeGen、5 包空壳、WebTermApp 空窗、`.github/workflows/ios.yml`);Info.plist 经 plutil 核对 = §5.2 逐字(五段 CIDR,**全程零 NSAllowsArbitraryLoads**);bundle id `com.yaojia.webterm`。**偏差**: "default MainActor isolation" 属 Swift 6.2+,本机 6.1 → Swift 6 语言模式 + strict concurrency + 显式 @MainActor 替代(已注 project.yml)。**环境事实**: 本机 Xcode 16.3 原本无 iOS 模拟器 runtime → `xcodebuild -downloadPlatform iOS` 装 iOS 18.4(22E238)后模拟器构建绿。
- **T-iOS-2 Day-1 双 spike**: **平台事实定音——URLSessionWebSocketTask 可发自定义 Origin(①无 Origin→401 ②精确匹配→101+attached ④端口失配→401),不切 Starscream**;③ 1.5MB 回放在默认 1MiB 上限失败(withKnownIssue 记录)、16MiB 成功,ESC/C0 对抗(转义≈3.6MB)成功。**勘误(已写回 PLAN §1/§3.2/T-iOS-9)**: 超限 errno 实测 NSPOSIXErrorDomain **40=EMSGSIZE**(计划原文误标 ENOBUFS=55),T-iOS-9 归类以 40 为主、55 兜底。ServerHarness 自举真服务器(tsx spawn+death-pipe watchdog,无孤儿进程)。真机 smoke 4 项 DEFERRED(无真机),SpikeTerminalScreen 已编入 App target 待 T-iOS-18。
- **T-iOS-3 WireProtocol(冻结)**: §3.1 全类型 + Tunables §3.2.1 全表;**59 tests 全绿、覆盖率 100%**(llvm-cov 226/226);300 轮 roundtrip property + 500 轮 fuzz(decodeServer 永不 crash);**跨实现实测**: Swift encoder 输出经 tsx 喂给真 `src/protocol.ts` parseClientMessage → 13 帧+mode 顶层恢复全 PASS。自此冻结。
- **T-iOS-4 TestSupport**: FakeTransport(actor,scripted failures+按连接记帧)/FakeClock(class+锁,`waitForSleepers` 确定性栅栏,零真实等待)/FakeHTTPTransport(记录 headers 供 Origin-iff-G 断言);3 smoke 全绿。
- **verify(独立 agent 复跑)**: 模拟器构建 exit 0;59+3 tests;覆盖率 100%;spike 6 tests+1 known issue;`git status` 零越界。
- **[x] W1 叶子包(T-iOS-5/6/7/8,4 builders;编排 [5→6 串行] ∥ 7 ∥ 8——5/6 同在 SessionCore 包,主树并行会互坏编译)**:
- **T-iOS-5 ReconnectMachine+PingScheduler**: 纯 reducer,§3.2 签名逐字;backoff 1s→…→30s 封顶镜像 terminal-session.ts(连上才归零,retry/foregrounded 不重置阶梯——防狂按打穿);Ping 25s/连续 2 miss→connectionLost/取消与断线显式区分。16 测,0 真实等待,文件级覆盖 100%/95%。
- **T-iOS-6 GateState+AwayDigest**: epoch 语义逐行镜像 terminal-session.ts:306-311(上升沿 +1、持续帧刷新 detail、下降沿不重置计数器);`canDecide(epoch:)` 防误批;affordance 映射逐字对齐 tabs.ts:345-347(绝不发 raw auto);digest 词表与 `TimelineEvent.knownClasses` 有相等性绊线测试;防御:乱序容忍、Date→ms clamp 不 trap。25 测,两文件 100%。
- **T-iOS-7 HostRegistry**: §3.3 契约;KeychainHostStore 经 SecItemShim 缝(swift test 打 fake;真 shim kSecUseDataProtectionKeychain+AfterFirstUnlockThisDeviceOnly,属性字典单点构造+逐字单测);InMemoryHostStore 入 Sources 供 App-VM 测试。30 测,own-Sources 覆盖 88.1%。
- **T-iOS-8 APIClient+探针**: 44 测先全绿后 **`[!] BLOCKED` 正确上报冻结契约自相矛盾**——§3.4 `runPairingProbe → Result<Host,…>` 中 Host 是 §3.3 HostRegistry 类型,而 APIClient 依赖边仅 WireProtocol(§1 零耦合),签名无法编译(两轮 review 均漏)。**裁定(orchestrator,三选一取 c)**: 探针职责=验证 endpoint → 返回 `Result<HostEndpoint,…>`,Host{id,name} 由 T-iOS-12 VM 构造(id/name 本非探针所知);同时按法定路径加 `Tunables.pairingProbeTimeout`(10s,§3.2.1 新增行+绊线断言)。裁定已写回 PLAN §3.4/§3.2.1/T-iOS-8,公开包装+冒烟测试由 orchestrator 落地。Origin **iff**-G 入测试名;探针②成功后必带 Origin kill 不留孤儿;403 kill→originRejected(HTTP 侧 G 守卫也是配对必验项)。45 测,own-Sources 覆盖 98.1%。
- **W1 验证(orchestrator 亲验)**: 5 包 178 tests 全绿(59+3+41+30+45);覆盖率(own-Sources 口径)SessionCore 99.1%/HostRegistry 88.1%/APIClient 98.1%,全过 80% 门;`git status` 全部在 Owns 内零越界。**发现归 T-iOS-16**: §9 覆盖率命令的 ignore regex 不排静态链入的依赖包源码,CI 接线时须按包过滤(本次已按正确口径亲算)。
- **[x] W2 连接核心(T-iOS-9→10 串行;首次派发 T-iOS-9 agent 死于 API 连接中断——现场零文件、直接 resume 重跑,非任务失败)**:
- **T-iOS-9 URLSessionTermTransport**: 冻结 TransportConnection 不动,ping 经 SessionCore 内部(非冻结)`PingableTermTransport/ConnectionPinger` 扩展点接入;pong 时限复用 `Tunables.pingInterval`(无新常量);EMSGSIZE 归类 40 主/55 兜底→类型化 `.replayTooLarge`;connect 失败原样重抛底层错误(保住 PairingError.classify);状态 delegate 驱动,close 三态可区分;测试服务器 = 裸 TCP 手写 RFC6455 framing(可逐字验 Origin/注入 binary/超帧)。11 测,文件覆盖 97.83%,iOS triple 编译过。
- **T-iOS-10 SessionEngine actor**: 单 Task 生命周期循环 + generation 计数防陈旧任务复活;首帧铁律 attach→lastKnownDims resize→排队帧按序 flush;终态(exit / replayTooLarge)绝不进 backoff;digest 断点原子消费恰好一次;gate 决策 canDecide 兜底、断线期决策丢弃不入队(重连后 gate 可能换代);防御:非 v4 UUID/相对 cwd 剥离(服务器会静默丢帧致挂死)。**决策**: 初始 resize 依赖首次 sizeChanged/notifyForegrounded(冻结 open 无 dims 参数,SwiftTerm 首次布局即触发,已注 doc);digest limit 传 Int.max(截断属 UI,T-iOS-14)。20 测,SessionEngine.swift 94.98%(480 行,<800 硬上限,内聚裁量已记)。
- **W2 验证(独立 agent,5/5 PASS)**: SessionCore 72 (41 W1 完好+31 );四包回归全绿(**累计 209 tests**);own-Sources 覆盖 96.68%;冻结契约零改动;4 项语义抽查(Origin 单点 :169/16MiB 来自 Tunables/replayTooLarge 断重连路径逐行核/decodeServer 唯一入口 nil-drop)全数在源码落实
- **[x] W3 UI+提前项(7 agents: [T-iOS-11121314] 串行链 T-iOS-16 T-iOS-17;协调点 = orchestrator 预建 WebTermTests 单测 bundle(project.yml+5 包依赖,模拟器实测过)并单独提交)**:
- **T-iOS-11 Terminal+KeyBar**: KeyByteMap(SessionCore 纯数据)逐字节复刻 keybar.ts 17 (⏎=\r \n 专项断言);TerminalViewModel 消费 engine.events,replayTooLarge不可重试态+话术逐字断言;**决策**: 硬件 UIKeyCommand 仅注册 Esc/⇧Tab/Ctrl 和弦,箭头不注册(固定 CSI 会破 DECCKM 应用光标模式,vim/htop 错乱)——有排除测试;OSC52 剪贴板拒绝链接仅 http(s)。
- **T-iOS-12 Pairing**: Phase 状态机;扫码confirmingHost,**确认前零网络**(recordedRequests/connectAttempts 空断言);公网需显式勾选确认5.4 四层警告 13 组参数化3.4 裁定落地(VM 构造 Host store);host 分级 VM 内重实现(APIClient internal 粒度不足,重复已标注归 T-iOS-38 去重);真机 DataScanner 编译隔离,generic/platform=iOS 构建过
- **T-iOS-13 SessionList**: 轮询 Tunables.listPollInterval,离开断言 0 sleeper+10 周期零请求(无泄漏);**发现**: LiveSessionInfo pending 字段(已核 manager.list())→ 徽标经 setPendingApproval overlay T-iOS-15 gate 事件接入;kill 乐观移除+404=成功/403 回滚;staleness 严格 > 镜像 web;PR 链接 https-only(SEC-L5 镜像)。
- **T-iOS-14 Gate/Digest UI**: GateViewModel 独立 VM(events 与 engine 分离注入,T-iOS-15 fan-out 零改动);三选一映射唯一事实源=SessionCore Affordance.clientMessage(acceptEdits/default/reject,App 层零 allowAutoMode 逻辑);tap-epoch+affordance 双守卫;haptic 每 epoch 恰一次(HapticSignaling 注入);digest 自动淡出 FakeClock 驱动;文案逐字镜像 tabs.ts:326-350。
- **T-iOS-16 集成 CI**: 吸收 spike 重构 harness;10 测对真自举 Node 服务器(echo/stty resize 1..1000/16MiB+ESC/C0 对抗回放/JOIN mirror/401/401/403/kill→WS close 而非 exit 帧/自然 exit 广播);ios.yml=包矩阵+coverage-gate.sh(own-Sources 过滤,修正 §9 已知缺陷)+App 测试+集成 job;覆盖率门红→绿演示实录;CI 平台未跑(如实注明)。
- **T-iOS-17 ntfy 文档**: ios/README.md;只读验证(引 setup-hooks.mjs file:line),**未触碰用户真实 hook 配置**(orchestrator 安全约束);payload 最小化逐行核;真机端到端 DEFERRED。
- **W3 验证(独立 agent,全项 PASS)**: 包套件 59+3+77+30+45;App 55 测 TEST SUCCEEDED;集成 10 测+1 known issue(故意的 1MiB 复现);语义抽查 5/5(KeyByteMap 逐字节 16 项、三选一字面量+零 allowAutoMode、零网络断言实测在 :60、轮询走 Tunables、CI 覆盖率 own-Sources 过滤);Owns 审计零越界。**累计 224 单测 + 10 集成**。
- **[x] W4 汇合(T-iOS-15,1 builder + verify 8/8 PASS)**:
- **T-iOS-15 App 接线**: `Wiring/` 9 文件——AppEnvironment 生产 DI(真 Keychain/真探针/URLSessionHTTPTransport 零逻辑透传,Origin 规则全留 APIClient)、EventFanOut(单消费者 engine.events → TerminalVM+GateVM+SessionActivityBridge 三分支保序广播)、bridge(.adopted→存 lastSessionId/.gate→列表 ⚠ overlay/.exited→清两者,防「继续上次」指向死会话)、TerminalSessionController(.background→close 干净 detach;suspend 后 .active→重建栈+open,generation 换 SwiftUI identity 触发首次 sizeChanged 夺回全屏)、**PrivacyShade 严格 `scenePhase != .active`**(三相位测试)、冷启动路由纯函数;WebTermApp 薄 @main;**删除 SpikeTerminalScreen**(grep 零残留)。
- **两项自动化走查代理**: ①LiveServerSmokeTests——签名宿主内真生产 DI 图端到端(探针→host 入库→liveSessions→attach→echo 回显→close→kill 清理);②KeychainHostStoreLiveTests——真 Keychain 往返+`SecItemCopyMatching` 断言 `kSecAttrAccessible==AfterFirstUnlockThisDeviceOnly`+非 synchronizable(T-iOS-7 递延项闭合)。模拟器 boot 截屏:配对页正常渲染。
- **环境硬墙(新发现,已记 harness)**: repo 在 `~/Documents`(TCC 保护)时,模拟器 App 语境 posix_spawn 的 node 读 repo 文件被 tccd 无限阻塞(sample 实测卡 `uv_cwd→__open_nocancel`;平台二进制豁免)→ SimServerHarness 双模式:A 自举(CI/非 TCC 路径)/B `WEBTERM_SERVER_URL` 外部注入;TCC 下 0.001s 快速失败+指引。**本机跑法**: 起服务器后 `xcrun simctl spawn booted launchctl setenv WEBTERM_SERVER_URL …` 再 xcodebuild test(命令行 TEST_RUNNER_ 前缀实测传不进宿主 App 测试进程);裸跑 smoke 红=有意显式失败非回归。
- **偏差闭合(orchestrator 代 T-iOS-11 owner 修)**: W4 曾记「alive-engine `.active` 未调 notifyForegrounded(dims:)——W3 无 dims 钩子」→ TerminalViewModel 增 `lastSentDims`(仅记有效 resize,invalid 不覆盖,有测试)+ controller `.active` 分支接 `notifyForegrounded(dims:)`(无有效 dims 时跳过,SwiftTerm 首次布局兜底)。**76 App 测全绿**(75+1)。
- **verify(独立 agent,8/8)**: 214 包测+76 App 测+10 集成;spike 删净;遮罩规则/生命周期双向 close 路径/组装安全(G 全走 APIClient、零 ad-hoc URLSession、零 ArbitraryLoads、Origin 零手拼)逐项在源码核实;Owns 零越界。
- **[x] W5 验收 + findings 修复(2 reviewer ∥ + 1 fix builder;均 PASS_WITH_FINDINGS→已闭合)**:
- **T-iOS-18 F 走查**: 全部机器可执行项本轮亲自重跑(集成 10/App 76/包 214 + 模拟器冷启动·后台·重启截屏);F-iOS-1..13 逐条 EXECUTED/EVIDENCED(指认测试名)/DEFERRED(真机)(QR 扫码/IME/震动/切换器遮罩目检/ntfy 端到端,附手工步骤清单)。
- **T-iOS-19 安全核对**: 对产物审——Origin 写入点全树恰两处且单点派生;G/RO 分界;**拆构建产物**验五段 CIDR+零 ArbitraryLoads+双 usage description;Keychain 属性;P0 不该在的面(deep link/麦克风)确认缺席;残余风险(isCaptured 跳过/ws 明文模型/epoch 仅客户端)记录完整。
- **findings(0 CRITICAL/HIGH; 1 MED+3 LOW,全部闭合)**: ①MED §9 XCUITest happy path 缺失 → orchestrator 建 WebTermUITests target/scheme,fix agent 落地唯一用例:**配对(手输)→列表→attach→KeyBar ^L→注入 held gate(POST /hook/permission)→点 Approve→断言服务器真放行 behavior==allow**,服务器为 oracle(XCUITest 读不了 SwiftTerm 字形);幂等三分支(全新/已配对/空态)全被实走;**两次真实跑通**(439s/677s,EXIT=0)。②LOW CI 缺 iOS17 下限腿 → ios.yml 增 ios17-floor job(runtime 缺失大声跳过/存在必判,CI-only 本机未验)。③LOW URLSession.shared 磁盘缓存可落盘 preview 终端字节 → 生产 HTTPTransport 默认 .ephemeral。④LOW XcodeGen target 级默认覆盖 project 级 TARGETED_DEVICE_FAMILY(实际打包 [1,2]) → 移 target 级,产物已验 [1]。
- **XCUITest 实证发现(已记 ios.yml/用例注释)**: `TEST_RUNNER_` env 只在 xcodebuild **进程环境**里才透传 runner(命令行 KEY=VALUE 是 build setting 到不了);**`CODE_SIGNING_ALLOWED=NO` 打断真 Keychain(-34018)**→ 顺手修掉既有 app-tests 腿同 flag(否则 KeychainHostStoreLiveTests 在 GH runner 必红);XCUITest typeText 够不着 SwiftTerm(无 AX 键盘焦点)→ 输入断言走 KeyBar ^L+清屏应答;SwiftTerm 光标闪烁烧 XCUITest 60s idle-wait/事件(单跑 7-11min,已压到 3 次终端交互)。
- **遗留观察(report-only,非阻塞)**: 配对探针偶发残留 clientCount=0 会话(killProbeSession 竞态,run 8 一例)——归 T-iOS-8 owner 复核;真 GH Actions 首跑待推送后确认(ui-test 腿/ios17 runtime 镜像现状)。
- **P0 收官统计**: 306 项自动化(214 包测 + 76 App 测 + 10 集成 + 1 XCUITest[计 1,参数化更多]) 全绿;覆盖率 own-Sources SessionCore 96.7%/HostRegistry 88.1%/APIClient 98.1%/WireProtocol 100%;19/19 任务完成(全部偏差有日志);服务器 src/ public/ **零改动**。**待用户**: ①真机走查清单(T-iOS-18 报告内);②推送分支跑首轮 GH Actions;③P1 开工前拍板:Apple 付费账号(APNs/TestFlight)与 bundle id/App 名(现 com.yaojia.webterm/WebTerm)。P1 首批 = T-iOS-20(APNs server) ∥ T-iOS-37(lastOutputAt) ∥ T-iOS-38(APIClient P1 契约)。**偏差(orchestrator 决策,W1 起生效)**: 并行 builder 不用 git worktree(Workflow worktree 合并回主树增加失败面),改为主树直跑——文件互斥 + 同包任务串行化,禁 agent 碰 git;每波末独立 verify + orchestrator commit 兜底。
### ✅ iOS 客户端实施计划 — `docs/PLAN_IOS_CLIENT.md`(规划完成 — 2026-07-04,main,未提交;纯文档,零代码改动)
**多 agent 编排 = 一个 ultracode `Workflow`(66 agents,7 阶段)**:5 读者并行(wire contract 逐 file:line / 功能矩阵 / relay 现状 / 模板规范 / iOS 平台联网调研)→ 3 架构师独立竞标(full-native / WKWebView-hybrid / phased)→ 3 评委多视角打分(UX/工程/安全,**phased 26.5 胜出**,native 23.5、hybrid 16.5,21 条 mustSteal 嫁接)→ 综合简报 → 写手出稿(843 行)→ **4 评审交叉验证**(对照 src/ 源码逐条核实 / iOS API 联网核查 / 安全 / 计划质量)→ 每条 finding 对抗式 verify(CRITICAL/HIGH 双票)→ fixer 应用。
- **方案**: Phased-Native「口袋驾驶舱」—— SwiftUI + SwiftTerm(MIT/SPM),4 个纯 SwiftPM 包(WireProtocol/SessionCore/HostRegistry/APIClient)+ 薄 App 胶水,`ios/` 顶层新包;前台会话单条活 WS + 其余 HTTP 轮询;iOS 17+/Swift 6/XcodeGen;**P0 服务器零触点**(通知复用既有 ntfy 桥 scripts/setup-hooks.mjs:227-238),P1 仅两处声明触点(APNs sender + `LiveSessionInfo.lastOutputAt`,后者为 TS 任务 T-iOS-37)。relay/E2E 明确推迟(已审计 TS 加密不得随手 Swift 重写)。
- **任务机**: 38 个任务(T-iOS-1…38),P0=19 任务 W0W5 ≈13 人日、P1 W6W8 ≈17.5、P2 W9W10 ≈8,总 ≈38.5 人日;每任务 Owns/Depends/测试先行 Steps/Accept/估算/分派模型。关键平台事实:原生客户端 **WS 握手必须显式带白名单内 `Origin` 头**(src/http/origin.ts:27 对 undefined 默认拒);`URLSessionWebSocketTask.maximumMessageSize` 需调大到 16 MiB(回放单帧 JSON \u 转义最坏 6×2MB)。
- **Review 结果**: 35 findings → **32 confirmed 全部修复**、3 rejected(含「ATS 不收 CIDR」被 verify 驳回 —— iOS 17+ 确实支持 CIDR exception domains)。修复亮点:plan-gate 三选一映射纠正为 `acceptEdits/default/reject`(对照 public/tabs.ts:345-347);「:443 破坏 Origin 匹配」为**臆造 gotcha,反向纠正**(new URL() 双侧归一化);ntfy 桥从「新建」改「复用既有」;CRITICAL 共享类型无主 → 全部收编进 WireProtocol(T-iOS-3);W7 Owns 重叠拆出 T-iOS-38;锁屏 Allow 加 `.authenticationRequired`(Face ID 确认才批 shell 命令);QR 扫码改「先确认再发包」防恶意配对。
- **验证(orchestrator 亲验)**: 文档 943 行落盘、11 节结构完整、fixer 27 组修复日志逐条对账;`git status` 仅新增该 .md,src/ 零改动。**未提交**。下一步:用户批准后按 §8 分派批次开工(W0 首日 spike 实测 Origin header)。
### ✅ 安全审计 relay-e2e + relay-auth — 上线前深度审计 + 修复(完成 — 2026-07-02,分支 `security/relay-auth-audit-fixes`,未提交)
上线门:自动生成的 E2E/鉴权核心须过安全专家审计(前次复审判 approve-with-changes)。**多 agent 编排 = 两个 ultracode `Workflow`**:①**审计**(99 agents)—— 11 维 finder 并行(两包)→ 每个 finding 3 lens 对抗式 verify(exploitability/crypto-spec/refuter 多数票)→ 第二轮「重复 review」深挖 → 每包 completeness critic → dedup+定级;②**修复循环**(10 agents)—— foundation(types)→ 3 组文件互斥并行修复(module fixers)→ **verify 循环至 tsc 干净 + 全绿** → 5 路对抗式 re-audit 确认每个 exploit 已闭合。
- **审计结论**: **relay-e2e = PASS 零改动**(所有候选在 verify 阶段被驳回:确定性 nonce 安全 —— 方向分离 + 真实 agent 侧 `createReplaySealer` 各自持独立单调 seq;strict-successor recv guard 无法导致 replay-accept)。**relay-auth = approve-with-changes,5 条确认**(2 HIGH / 1 MED / 2 LOW),全部在 human-auth/enforcement 层,全部包内可修。驳回但存档(不再复议):E2E replay nonce reuse、mTLS EKU、SPIFFE 域未 pin、reattach host-scope、TOTP 时序。完整报告见 `docs/REVIEW_RELAY_SECURITY.md`
- **修复**(均含回归测试): **F1 HIGH** step-up 因子降级 —— `needsStepUp` 现把新鲜度绑定到**精确方法**(`stepUpMethod`),钓来的 TOTP 不再满足 passkey;**F2 HIGH** step-up 在新建会话路径被完全跳过(生产 caller `term-relay/data-plane/upgrade.ts:105` 硬编码 `principal:null` → 生产环境 step-up 形同虚设)—— 改为**主机驱动 + fail-closed**(`policy.required` 时 principal 缺失/过期/错方法一律 403);**F3 MED** WebAuthn 断言未绑定已存凭据 —— 现校验 credentialId 且转发 pubkey 给 verifier;**F4 LOW** DPoP verify 未捕获抛出 → INV15 门未审计地崩(审计逃逸+DoS)—— `verifyDpopProof` 全体 try/catch→false + `coreAuthorize` 边界兜成干净审计 401 + issue 校验 cnfJkt 格式;**F5 LOW** 验证过的 signCount 被丢弃 → 克隆检测失效 —— `finishAuthentication` 现返回 `{principal,newSignCount}` 供持久化。
- **验证(orchestrator 亲验)**: relay-auth `tsc --noEmit` 干净 + **122 测试全绿**(基线 104,+18 回归);relay-e2e 未动、tsc 干净、76 全绿。re-audit **5/5 闭合**(置信 0.950.97,均有回归测试)。
- **遗留/behavioral note(非阻塞)**: F2 改为 fail-closed 后,**任何 `stepUpPolicyFor(host).required===true` 的主机,P1(term-relay)必须在 upgrade ctx 传入已认证 principal**,否则正确地被拒;当前 v0.9 默认 `NO_STEPUP_POLICY.required===false`,现网非 step-up 主机不受影响。P1 集成时须补此接线(见 [[relay-stepup-needs-principal-wiring]])。
- **第二轮独立 review(Fable 5,换模型增强多样性)+ F6 修复**: 第二个 `Workflow`(fix-regression 猎取 + 新角度 sweep + 对抗式 re-attack 被驳回项)判:**5 条修复 0 regression**、被驳回/D-i-D 项 re-attack **0 存活**(确认真不可利用),但**新增 1 条 HIGH——F6**:relay-e2e `replay-key.ts` 的可恢复 `K_content` 仅由 `(hostContentSecret, sessionId)` 决定(稳定),而真实消费者 `agent/src/e2e/replaySeal.ts``let seq=0n` 在每次重建(重启/重连)时归零 → 两代 sealer 在**相同 (key, nonce)** 下封不同明文 = 灾难性 AEAD 复用。**第一轮曾误驳此项**(verifier 看到"独立单调计数器"就类比 live 路径判安全,漏了 K_content 跨重启稳定);第二轮定向 re-attack + orchestrator 亲读 `replaySeal.ts` 证实为真。**修法(用户选 epoch-in-key,4 包)**:`ReplayKeyParams``epoch`;`deriveContentKey` 把 epoch 以 `sessionId‖0x1f‖epoch` 混入 HKDF salt;`createReplaySealer` 每代 `randomUUID()` 新 epoch 并暴露;`ReplaySource` 带 epoch、浏览器按之重推。**验证(亲验)**:4 包 tsc 干净 + 全绿(relay-contracts 81 / relay-e2e 78[+2 F6 回归] / agent 133 / relay-web 99);re-audit 判闭合(回归证:两代同 seq=0 nonce 但不同 key)。**残留(fail-closed 非复用)**:ring-buffer 传输(P1/P2)须持久化并按代下发 epoch —— 已记 TODO,当前 replay 路径尚未端到端接线(`manage-page.loadReplay` 仍是 throw stub),故 F6 此刻是潜伏漏洞。完整报告 `docs/REVIEW_RELAY_SECURITY.md`(含 F6 + RESOLUTION)。
- **动态 e2e 安全测试台(新增 `e2e/` 包)**: 跨包 harness `buildRelayWorld()` 把**真实** P5(auth)+P4(crypto)+P2(agent replay) 通过内存 seam + 不可信 relay「RelaySpy」攻击者视角接起来(仅 I/O 边界 fake,所有安全检查走生产代码路径)。全流程 issue cap→upgrade/authz→handshake→sealed session 过 relay→reattach→revoke。**21 测试全绿、tsc 干净(亲验)**。把 F1/F2/F3/F4/F6 从「静态+单测」升级为「动态攻击者实测」,另加 MITM/reflection/replay/reorder/INV2(live+replay)/cross-tenant/single-use。F5(signCount)为返回形状改动、由 relay-auth 单测覆盖;RelaySpy 代替 term-relay mux(真多进程/浏览器 e2e 需尚未建的 run tooling)。用符号链接免 `npm install`(`e2e/node_modules/*` → 各包源码)。覆盖表见 `docs/REVIEW_RELAY_SECURITY.md`。**未提交**,留待用户决定。
### ✅ 桌面客户端 v0.1 — Electron 一体化壳(代码完成 — 2026-07-01,分支 `feat/desktop-electron`,未提交)
Mac/Windows 桌面 App,**内嵌现有 Node 服务器 + node-pty**(all-in-one):主进程 `startEmbeddedServer` 复用 `src/server.ts``startServer(cfg)`/`loadConfig`(**服务器零改动** —— 它本就导出可编程启动且 import 无副作用),窗口 `loadURL('http://127.0.0.1:<port>/')` 复用**前端零改动**(前端严格同源,`location.host` 自动指向内嵌服务器;Origin 白名单默认含 localhost)。核心价值 = **原生通知**(轮询 `/live-sessions``computeNotifications` → 系统通知)+ 托盘常驻 + 深链 `terminalapp://` + 开机自启。
- **编排**: ultracode `Workflow` —— 脚手架(orchestrator 冻结 `desktop/src/types.ts` 契约 + package/tsconfig/esbuild/electron-builder)→ Build(4 builder 并行、文件互斥、对齐冻结签名)→ Review(correctness/security/typescript 三 lens 并行、schema 结构化 findings)。纯逻辑尽量抽出可单测;Electron glue(main/window/tray/menu/preload/embedded-server/logger)作为不可单测 wiring 排除出覆盖率(沿用既有 vitest.config 先例)。**无用 zod、无用 electron-store**(遵循项目手写校验 + 手写 JSON 持久化的最小依赖惯例)。
- **交付**(`desktop/src/` 17 文件 + `test/desktop/` 9 测试文件): 纯模块 `port`(pickFreePort)/`shell`(平台默认 shell,Win→powershell)/`server-config`(env 覆盖映射)/`deep-link`(解析+防注入)/`notify-policy`(通知策略)/`notifications`(批量 diff)/`live-poll`(边界校验 /live-sessions)/`prefs`(手写校验)/`settings-store`(JSON 持久化);glue `embedded-server`/`window`(加固:contextIsolation+sandbox+拒绝外域导航)/`menu`/`tray`/`preload`(最小 bridge)/`main`/`logger`
- **Review 结果**: **无 CRITICAL/HIGH 安全问题**(Electron 加固与不可信输入处理被评为扎实)。修复的真实 bug(orchestrator 直接改+补测): ① 冷启动深链丢失(缓冲 open-url/argv 到 ready 后派发) ② `createTray` 无守卫会崩启动(try/catch,失败无托盘继续) ③ 首轮轮询通知轰炸(首 tick 只建基线) ④ poller 捕获过期 prefs(每 tick 读 settings.get) ⑤ notify 'tool tool' 病句→'tool approval' ⑥ live-poll Windows 反斜杠路径 + 未过滤 exited ⑦ `NotificationsResult.next`→ReadonlyMap。
- **验证**: 桌面 `tsc --noEmit` 0 错;**全量 1401 tests 绿**(基线 1307 +94 桌面,零回归);覆盖率全局 91.65/85.39/92.44/93.48(阈值 80×4 通过)、`desktop/src` 97.56/95.04/100/97.81;esbuild 打包 `main.cjs`/`preload.cjs` 产出、electron/node-pty 保持 external、dist 走运行时动态 import。
- **本环境无法验证(留真机/CI,DESKTOP_PLAN P1 D5D7)**: `.exe`(Windows,需真机/无 wine)。**审批门通知源**(`/live-sessions` 不带 pendingApproval)留 P2/D8 接 hook 侧信道 —— 策略已实现且测试,当前以 status→'waiting' 为代理。方案文档 `docs/DESKTOP_PLAN.md`
- **✅ Mac 打包 + 启动验证(2026-07-01,本机 arm64,已实测)**: `electron-builder``.dmg`(arm64,深度 ad-hoc 签名)。**关键修复**:首版只打了 node-pty、漏了服务器其它 npm 依赖(express/ws/web-push),导致**仅原地能跑**(沿目录树借了仓库根 `node_modules`),移到 `/Applications` 即崩 `Cannot find package 'express'`。改 `electron-builder.yml`:把 `dist/` + `public/` + `node_modules`(过滤 electron/electron-builder/esbuild/typescript 等构建工具;node-pty 经 `npmRebuild` 为 Electron 43 ABI 重编译)**全部铺到磁盘 Resources**(Model 2,与 `embedded-server.ts``process.resourcesPath` 解析匹配),desktop/package.json 补 express/ws/web-push 依赖。**仓库外副本直接执行验证通过**:服务器起、`GET /` + `/build/main.js → 200``/live-sessions → []`、node-pty 加载 OK、无 `Cannot find` 报错。`main.ts` 默认不注册开机自启(消除 native "Operation not permitted" 噪音)。**Gatekeeper**:未签名 ad-hoc 在 Sequoia(darwin24)双击被拦 → `xattr -cr` 清 quarantine/provenance + 深度签名后 `open`/双击可正常启动(实测 PID 存活、200)。产物 `desktop/dist-app/Web Terminal-0.1.0-arm64.dmg`(~206M,含完整 node_modules)。**仅 arm64;Intel 需 `--x64`/`--universal`;Windows 需真机;对外分发需 Apple Developer 签名+公证。**
### ✅ VC 语音命令映射 — 语音直接 approve/reject 权限门(完成 — 2026-07-01,分支 `v0.6-projects`,未提交)
上下文门控:仅当活动 tab 有 held 权限门(`pendingApproval===true``gate==='tool'`)时,语音「确认/批准/yes…」→ 复用现有 `{type:'approve'}` WS 通道、「拒绝/取消/no…」→ `{type:'reject'}`;其余一律口述不变。**后端零改动**。安全设计:整句精确匹配(否定/口水词攻不破)+ 前导否定守卫 + reject 优先 + 置信度门(仅 approve)+ **可撤销确认窗口(1.5s,提交时复检门身份/连接,防 TOCTOU)** + stale-gate epoch。新增 `public/voice-commands.ts`(纯匹配器 100% 覆盖)+ `public/voice-confirm.ts`(确认窗口 100%),接线 `tabs/voice/terminal-session/main.ts`。**1307 tests 绿**,双 review(code+security)判 CRITICAL 已全修复并补测。规划 `docs/PLAN_VOICE_COMMANDS.md`,详见 Detailed Log 首条。**待办**:confidence fail-open 取舍留给用户;tabs.ts 968 行待拆分。
### ✅ v0.6 Projects 命名空间自动分组 + 跨设备 prefs(完成 — 2026-07-01,分支 `v0.6-projects`,未提交)
Projects 页去杂乱:按点分隔命名空间自动折叠成 section(`Billo.Platform ·8` / `Billo.Infrastructure ·2` / `Other`)+ 置顶 "Active now" + 无前缀时退化为扁平网格。收藏/折叠态改**服务端持久化**(`GET/PUT /prefs``~/.web-terminal-prefs.json`,离线回退 localStorage 镜像 + 迁移旧 `proj-favs`)跨设备同步。1227 tests 绿,真浏览器验证折叠跨 reload 从服务端载回。详见 Detailed Log 首条。
### ✅ v0.6 Project Manager 项目工作台(完成 — 2026-06-30,分支 `v0.6-projects`)
- **全部 6 任务完成并验证**(P1P6,多 agent 编排)。**398 测试全绿**(自 341 +57)· 双 typecheck 干净 · `build:web` OK · 覆盖率 90.98/81.95/89.28/93.16(阈值 80×4 通过)· **真浏览器 smoke 通过**
- **编排方式**: ultracode `Workflow` 动态编排——Build(P2 后端 ∥ P5 前端,`module-builder`)→ Wire(P4 端点 ∥ P6 接线)→ Review(`module-reviewer` 验收 + `typescript-reviewer` + `security-reviewer`,report-only 并行)→ 对每个 HIGH/CRITICAL 对抗式 Verify(`module-reviewer`)→ Fix(`module-builder` 仅改已确认项)。**P6 首跑因 transient `server_error` 挂掉(未落任何改动)→ stop + `resumeFromRunId` 恢复:P2/P4/P5 命中缓存秒回,P6 重跑成功。**
- **交付**:
- **P2** `src/http/projects.ts`(15 测试,93.37% 覆盖): `parseGitHead`(纯) + `buildProjects(cfg, liveSessions)`;BFS 扫 .git(深度上限、跳过 node_modules/dotdir/symlink、命中即停下钻)→ 读 `.git/HEAD` 分支 + `git status --porcelain` dirty(`execFile` 无 shell、2s 超时、并发 8)→ 合并 `~/.claude/projects` cwd(复用 `history.listSessions`)→ 按 cwd 前缀归并 live sessions(每次 fresh)→ TTL 缓存(仅缓存磁盘发现)+ **in-flight 去重**
- **P5** `public/projects.ts`(纯 helper 30+ 测试) + style 追加: `mountProjects`(镜像 `mountLauncher`);1:N 卡片(分支 chip / dirty 点 / session 行 / ` start claude here`)、搜索过滤、收藏置顶(localStorage);`.home-seg` 段控样式也在此定义。
- **P4** `src/server.ts`: `GET /projects`(只读、无 Origin 守卫、try/catch 兜底 `[]`)+ 5 集成测试(真起 server)。
- **P6** `public/tabs.ts`: `openProject`(同名 `#n` 后缀、`addEntry(null,label,cwd,'claude\r')`)+ `countOpenWithTitlePrefix` + 首页 **Sessions↔Projects 段控**(`updateHomeView`);7 新测试。
- **Review 12 findings → 4 HIGH 全确认并修复**: ① **A3 关键**:`openProject`/`newTabForResume` 缺回车 `\r`(claude 不自动执行)→ 补 `\r`;② 段控被定位面板盖住 → 面板 `inset:44px 0 0 0`;③ `mergeHistory` 串行 I/O → `mapWithConcurrency`;④ discover 并发缓存击穿 → in-flight promise 去重。
- **Orchestrator 追加加固**(MEDIUM/LOW,本人直接改 + 补测): 前端 `fetchProjects``normalizeProject`(API 响应边界校验,缺 `sessions` 不再 TypeError)+ `getFavs` 过滤非字符串(localStorage 防污);`config.parseProjectRoots` 相对路径 fail-fast(+ config 补 v0.6 解析测试,config.ts 95.87%→98%)。`style.css` 1143 行 > 800 上限属**既有债**(v0.6 前已超),esbuild 仍打单文件,留作后续拆分。
- **真浏览器 smoke**(`PROJECT_ROOTS=~/Documents/git`,83 真 repo): 首页段控 Sessions/Projects 切换 ✓;Projects 面板 83 卡片(`web-terminal` 显示 `branch:v0.6-projects` + dirty 正确,含 `feat/...` 斜杠分支)+ 搜索框 ✓;点 `web-terminal` 卡 → 开 tab 标签 `web-terminal`、首页隐藏、**终端出现 claude 启动界面**(A3 端到端)✓;console 仅既有 `/favicon.ico` 404 + apple-mobile 警告,无 v0.6 报错。
- **待办**: 真机/手机验收(同历史 F4/F7 遗留);可选 server.ts 抽 routes;`style.css` 拆分。
- **当前阶段**: **v0.3 全部完成**(H1H4 + M3/M6/M7 已并入 main;**O2 历史浏览** 在分支 `o2-history`,待合并)。
- ✅ Step1 前端快赢 · ✅ H2/H4 状态感知 · ✅ H3 远程批准 · ✅ H1 tmux 保活 · ✅ M3/M7/M6 · ✅ **O2 历史会话浏览/resume** · ✅ tech-debt 清理(@ts-ignore 去掉)· ✅ UI:终端不再打印 Connecting/Connected(靠标签点)。
-**现代化 UI 主题**(design-token 调色板/靛蓝强调色/圆角芯片标签+键栏/毛玻璃弹层)· ✅ **键栏功能字幕**(每键下方标作用)+ 新增 ^R/^L/^D(共 17 键)· ✅ **⌨ 快捷键速查弹层**(`public/shortcuts.ts`)。
-**v0.4 多设备共享会话**:放宽不变式 #5 —— 一个 session 可挂多个 WS 客户端(镜像;输出/退出/状态广播,谁都能打字)。`GET /live-sessions` + 打开应用自动把主机活动会话**显示为 tab**;`🔗` 分享二维码 / `?join=<id>` 加入指定会话。
-**镜像尺寸 = 最近活跃端优先(latest-writer-wins)**:共享 PTY 只能一个尺寸,之前取最小值导致宽屏桌面被 iPad 夹小(iPad 全屏、桌面留黑边)。改为"最近 fit/聚焦的设备决定尺寸",你正在用的设备永远全屏;attach/detach/`blur` 都不改尺寸,前端在 pane 显示 + 窗口聚焦时重发 dims(切设备即夺回全屏)。实测两端:200×50 → iPad 100×40 → 桌面 refit 200×50 → iPad blur 不变。后端纯客户端实测镜像输出 + 共享输入通过。
-**🗂 会话管理页**(`/manage.html` 独立页,非弹窗):**实时预览缩略图网格** —— 每张卡片用只读 xterm 渲染该 session 当前屏幕(像截图,缩放),一眼看出每个 session 在干嘛;`RingBuffer.tail()` + `GET /live-sessions/:id/preview`(不 attach、不影响观看数/回收),每 4s 刷新;可点缩略图/Open 进入、Kill / 批量 Kill。实测 3 个 session 缩略图(top / git log / ls)彩色可辨,Kill 3→2→0。
-**v0.5 首页 = 会话选择器(launcher)**:打开应用不再自动建/恢复 tab,而是落到选择器 —— 主机所有运行中 session 的实时缩略图网格,用户自己点开(回放完整 scrollback)或 ` New session`;关掉最后一个 tab 回到选择器。`public/launcher.ts`,复用 `.mg-*` 卡片样式。实测:首次加载 0 tab + 缩略图;Open→建 tab、选择器隐藏;关最后一个→选择器回来。
- ⬜ 仅剩:**O1 token 认证**(可选,默认关)+ **F4/F7 真机验收**(局域网/手机)。
- **225 测试全绿**;工具栏 🔍搜索 ⚙设置 ▦仪表盘 🕘历史 ⌨快捷键 🔗分享 🗂管理 📱QR。
- **下一步**: O1(可选)或真机验收。
- v0.2/v0.1 历史见下方条目。
- **Wave**: **W0W5 基本完成**。v0.1 功能完整、测试与真机浏览器验证通过。仅剩 2 项需用户真设备。
- **进度**: **20/21 任务**(T19 待真手机)· **187 自动化测试全绿** · 真 PTY 端到端验证 · **真浏览器 smoke 通过**
- **验收 F1F9**: ✅ F1/F2(浏览器 smoke:渲染+输入输出+全彩)· ✅ F3(resize 无错)· ⬜ **F4 需局域网真机** · ✅ F5/F6(集成 ⑤⑥ 回放 CJK/ANSI)· ⬜ **F7 需真手机**(软键盘栏)· ✅ F8(浏览器 exit→重连提示)· ✅ F9(集成+curl 401)。
- **下一步(交用户)**: 真手机/另一台电脑开 `http://<你IP>:3000` 实测 **F4**(局域网可用)与 **F7**(Esc/Shift+Tab/方向键等触摸键生效)。
- **阻塞**: 无(F4/F7 非阻塞,属需物理设备的人工验收)。
- **次要 tech-debt**: main.ts 1 处 `@ts-ignore`(CSS import);未来加 `*.css` d.ts。
- **最后更新**: 2026-06-20(并入 review 全面修复,见下方条目)
---
## 维护性修复条目 (Review Fixes)
### 2026-06-20 · 按 REVIEW_REPORT 逐项修复(安全/架构/质量/测试)
- **状态**: `[x]` DONE(typecheck 干净 · **341 测试全绿**(16 文件,自 228 起 +113)· `build:web` 通过 · 覆盖率 90.3/80.7/87.6/92.4 ≥80 阈值已在 `vitest.config.ts` 强制)。
- **来源**: 4 个 agent 并行 review 产出 `docs/REVIEW_REPORT.md`;用户决定修全部四档,`/sessions` 保持现状仅加说明,`clientDims`/`blur` 死状态彻底删除。计划见 `~/.claude/plans/piped-marinating-parrot.md`
- **🔴 关键**:
- 多设备审批竞态(`server.ts` ws.close):仅当最后一个 client 离开时才 `resolvePending` —— 关一个镜像不再取消另一台正在看的待批准。
- 会话无上限 DoS:新增 `Config.maxSessions`(默认 50,env `MAX_SESSIONS`)+ `manager.assertUnderSessionCap()`,超限抛错复用 M4 的 exit(-1) 路径。
- 信号处理器泄漏:`onSignal`/`onUncaught` 提具名引用,`close()``process.off` 全移除(去掉测试 `setMaxListeners(50)` 遮掩)。
- 前端 `initialInput` 700ms 定时器:跟踪 `initialInputTimer``dispose()` 清理、`disposed` 守卫、`INITIAL_INPUT_DELAY_MS` 常量。
- `tabs.ts addEntry``null as unknown as TerminalSession` 类型洞:先建 session 再建 entry。
- **🟠 应修**: 安全头中间件(无 helmet)+ `DELETE /live-sessions[/:id]` Origin/CSRF 守卫(403);`history.ts``fs/promises`(`listSessions` 异步,`/sessions` await);**彻底删除 `clientDims`+`blur`**(types/session/protocol/server/manager 注释/terminal-session.hide + 相关测试);WS 连接级限频(`maxMsgsPerSec` 默认 2000,超限丢帧不断连);`/sessions` 保持行为,加注释 + `TECH_DOC §7` 记录已接受风险。
- **🟡 测试**: 新增 `test/{tmux,preview-grid,terminal-session,tabs}.test.ts`(后两者 jsdom + mock WebSocket);扩 `history/config/manager/integration` 覆盖 killById/handleHookEvent/maxSessions/resolveUseTmux/live-sessions/preview-404/approve-reject/origin-guard/限频/信号泄漏回归。
- **⚪ 卫生**: `parsePositiveInt``parseNonNegativeInt`;ALLOWED_ORIGINS scheme 校验;`server.ts` 日志注入 sanitize + `isLoopback` 健壮化(127.0.0.0/8 + IPv4-mapped);运维常量(`PERM_TIMEOUT_MS`/`REAP_INTERVAL_MS`/`PREVIEW_BYTES`)入 Config;抽 `public/preview-grid.ts` 消除 launcher/manage 重复(类型复用 `LiveSessionInfo`);ARCHITECTURE §8 记录运行时句柄可变例外(H6)+ #5/#7/#8 更新;CLAUDE.md「auto-show as tabs」→ v0.5 launcher;「min across clients」陈旧注释清扫。
- **验证**: `npm run typecheck` PASS · `npx vitest run` 341/341 · `npm run build:web` OK · `npx vitest run --coverage` 退出 0(阈值 80×4)。
- **决策 / 偏离**: 覆盖率 `include` 收窄到 `src/**` + 四个有逻辑的前端模块,排除纯 DOM 接线/入口胶水(E2E 范畴),**未下调任何阈值**。未把审批状态机搬进 manager(报告 #10)—— close-handler 修法已正确最小,`server.ts` 抽 routes 留作可选后续。
- **遗留 / 待办**: 真机验收 F4/F7(同 v0.1 遗留);可选 server.ts 抽 `http/routes.ts`;胶水文件若要纳入覆盖率需补 E2E。
- **commit**: (本次提交)
---
## 任务总览 (Task Overview)
> 任务定义见 [PLAN.md](./PLAN.md);按 Wave 分组。`Steps` 完成数 = PLAN 中该任务的勾选项。
> **多 agent 并行**:同一 Wave 内任务互不依赖,可并行;跨 Wave 按依赖推进。
| 任务 | Wave | 模块 | 状态 | Steps (完成/总) | 完成日期 |
|------|------|------|------|------------------|----------|
| T1 | W0 | 脚手架 + 装齐依赖 | `[x]` | 5/5 | 2026-06-16 |
| T2 | W0 | `src/types.ts` 共享契约(冻结源) | `[x]` | 8/8 | 2026-06-16 |
| T3 | W0 | mock IPty 测试替身 | `[x]` | 2/2 | 2026-06-16 |
| T4 | W1 | `config.ts` | `[x]` | 5/5 | 2026-06-16 |
| T5 | W1 | `protocol.ts` | `[x]` | 4/4 | 2026-06-16 |
| T6 | W1 | `ring-buffer.ts` | `[x]` | 4/4 | 2026-06-16 |
| T7 | W1 | `http/origin.ts` | `[x]` | 3/3 | 2026-06-16 |
| T8 | W1 | `public/index.html` | `[x]` | 1/1 | 2026-06-16 |
| T9 | W1 | `public/style.css` | `[x]` | 1/1 | 2026-06-16 |
| T10 | W1 | `public/keybar.ts` | `[x]` | 4/4 | 2026-06-16 |
| T11 | W1 | `public/main.ts` | `[x]` | 6/6 | 2026-06-16 |
| T12 | W2 | `session/session.ts` | `[x]` | 7/7 | 2026-06-16 |
| T13 | W3 | `session/manager.ts` | `[x]` | 6/6 | 2026-06-16 |
| T14 | W4 | `server.ts` 接线 | `[x]` | 6/6 | 2026-06-17 |
| T15 | W4 | 集成 / E2E 测试 | `[x]` | 2/2 | 2026-06-17 |
| T16 | W5 | 验收 F1F3(终端/TUI/resize) | `[x]` | 3/3 | 2026-06-17 |
| T17 | W5 | 验收 F4+F9(LAN/Origin) | `[~]` | 1/2 | 2026-06-17 |
| T18 | W5 | 验收 F5+F6(保活/回放) | `[x]` | 2/2 | 2026-06-17 |
| T19 | W5 | 验收 F7(移动键栏) | `[ ]` | 0/1 | 需真机 |
| T20 | W5 | 验收 F8(退出处理) | `[x]` | 2/2 | 2026-06-17 |
| T21 | W5 | 安全核对 + README + LOG 收尾 | `[x]` | 3/3 | 2026-06-17 |
---
## 验收清单映射 (Acceptance — TECH_DOC §8)
> 功能验收独立于阶段勾选;F* 全绿才算 v0.1 完成。
| 编号 | 功能 | 状态 | 验证方式 / 备注 |
|------|------|------|-----------------|
| F1 | 交互式终端(ls/补全/Ctrl+C/历史) | `[x]` | 浏览器 smoke:输入→输出往返 |
| F2 | 全彩 + 全屏程序(vim/top/claude) | `[x]` | 浏览器 smoke:彩色提示渲染(深 TUI 同理) |
| F3 | 终端自适应(resize 不错位) | `[x]` | viewport 变更存活、无 error |
| F4 | 局域网访问(手机/他机) | `[ ]` | **需真机**(同 WiFi 开 http://<IP>:3000);Origin 白名单已含网卡 IP |
| F5 | 会话保活(断开重连仍在跑) | `[x]` | 集成 ⑤;tmux 重启保活(H1) |
| F6 | 重连回放(环形缓冲) | `[x]` | 集成 ⑥:ANSI/中文不乱码(M2) |
| F7 | 移动快捷键栏 | `[ ]` | **需真手机**(触摸键生效、不弹软键盘) |
| F8 | 进程退出处理 | `[x]` | 浏览器 smoke:exit→重连提示;detach 后退出 L1 |
| F9 | Origin 校验(401) | `[x]` | 集成 ①②;curl 坏 Origin→401;`/term` 限制(L3) |
---
## 详细日志 (Detailed Log)
> **倒序**,最新在最上。每个子任务一条。复制下方模板填写。
<!-- ========== 条目模板(复制此块,删除注释) ==========
### YYYY-MM-DD · S?.? <子任务标题>
- **状态**: `[x]` DONE / `[~]` IN PROGRESS / `[!]` BLOCKED
- **改动**: <文件:函数/范围,如 src/protocol.ts:parseClientMessage、test/protocol.test.ts>
- **验证**: <跑了什么命令 + 结果。如 `npm test` → 12 passed;或贴关键断言>
- **决策 / 偏离 PLAN**: <无 / 为何偏离 + 是否已回写 PLAN.md>
- **遗留 / 待办**: <无 / 后续子任务需注意的点>
- **阻塞**(仅 `[!]` 时): <卡在哪,需要什么>
- **commit**: <hash 或 N/A>
======================================================= -->
### 2026-07-01 · VC 语音命令映射 — 上下文门控 confirm/reject(多 agent 编排 + 双 review)
- **状态**: `[x]` DONE(未提交,分支 `v0.6-projects`)
- **背景**: 已有 v0.7 A2 push-to-talk 只做「语音→文字」口述;本次加**语音命令层**,让语音直接 approve/reject 权限门。规划见 `docs/PLAN_VOICE_COMMANDS.md`(多 agent:理解→前后端计划→4 路对抗 review→综合)。锁定决策:①上下文感知(仅当活动 tab `pendingApproval===true` 才当命令,否则口述不变)②只做 confirm 类,复用现有 `approve`/`reject` WS 通道(**后端零改动**)③危险键不接线,two-step 组件仅用于 approve 确认窗口。
- **改动**:
- 新增 `public/voice-commands.ts`(纯匹配器,**100% 覆盖**):`matchCommand` = 整句精确 + 前导否定守卫 + reject 优先 + 置信度门(仅 approve);中英确认/否定词表。
- 新增 `public/voice-confirm.ts`(确认窗口控制器,**100% 覆盖**):`createApproveConfirm(1500)` arm/cancel/isArmed,纯计时(注入 setTimeout,无 DOM)。
- `public/voice.ts`:`onTranscript` 多传 `confidence`(仍只转写)。`public/terminal-session.ts`:加 `pendingEpoch`(false→true 翻转自增,stale-gate nonce)。`public/tabs.ts`:`handleVoiceTranscript` 分派 + 确认窗口 arm + WS 断开闪「未连接」+ 单独 `#voice-confirm` overlay(避 hideInterim 清屏)。`public/main.ts`:接 `onVoiceTrigger` 点亮 🎤。`vitest.config.ts`:两新模块入 coverage.include(orchestrator 协调编辑)。
- 测试:`test/voice-commands.test.ts`/`test/voice-confirm.test.ts` 新建;`test/tabs.test.ts` 加 VC 集成(含 6 条 review-fix 用例);`test/voice.test.ts` 4 处旧断言随 `(text,confidence)` 新签名更新 + 补 confidence 转发断言。
- **验证**: `npm run typecheck` 干净;`npm test`**1307 passed / 43 files**;`build:web` OK;覆盖率 voice-commands/voice-confirm **100×4**、tabs.ts 94/84/89/97、TOTAL 91.3/84.6/91.7/93.2(80×4 通过)。`git diff src/` 空(后端 NONE 成立)。
- **决策 / 偏离 PLAN**:
- **实现 workflow 首跑失败**(review 阶段某 agent 6 次 stall)——但 Build V1/V2 已落文件;改为 orchestrator 亲自跑验证 + 用普通 agent 并行做 code/security review(避开 workflow 卡死)。
- **两份 review 独立收敛,判 CRITICAL/BLOCK**,均已修复并补测:①**确认窗口 TOCTOU**:`onCommit` 现复检 `pendingApproval && gate==='tool' && pendingEpoch===armedEpoch && connected`,否则丢弃;②**`cancel()` 从未接线**:接入新话语顶替(handler 顶部)、reject 分支、`#voice-confirm` 点击;③**WS 断开延迟提交错位**:提交前复检连接。
- **偏离 PLAN §4**:「松开麦克风取消」在 PTT 时序下不成立(松麦克风正是触发最终转录/arm 的动作),改为 **tap + 新话语顶替 + reject 取消** 三条真实取消路径。
- **`startsWithNegation` 重写**(review MEDIUM):原 `prefix+rest[0]` 实际只测 prefix[0](巧对但脆弱),改为显式测 `rest`
- **遗留 / 待办**:
- **confidence fail-open**(review MEDIUM,**保留**):`confidence===undefined` 时置信度门短路(符合 §D「有置信度时才门控」),靠确认窗口兜底误听;若要 fail-safe(undefined→不批准)是一行改动,但会让不报告置信度的引擎(部分非 Chromium)语音批准失效——留给用户定。
- **`public/tabs.ts` 已 968 行 > 800 上限**(既有债 + 本次 ~99 行):本次仅加最小 diff,**待后续抽 `voice-controller` 拆分**(decision F)。
- **commit**: N/A(未提交)
- **状态**: `[x]` DONE。双 typecheck 干净 · `build:web` OK · 全量 `vitest run` **1227 passed (41 files)** · 真浏览器端到端验证通过。
- **需求**(用户): Projects 页扁平网格太乱(50100 repo)。多 agent 探索(4 路:自动分组/手动文件夹/密度分段/竞品调研)结论一致 → **主方向=按点分隔命名空间自动分组**(repo 名如 `Billo.Platform.Payment` 天然带层级),配 **Active-now 置顶**;手动文件夹只是加分项非主线。用户选:**范围=自动分组+Active band**,**持久化=服务端**(收藏/折叠态跨设备同步,修掉 localStorage 每设备各一份的缺陷)。
- **改动**:
- 后端: `src/types.ts``UiPrefs {favourites[], collapsed{}}` + `Config.prefsStorePath`;`src/config.ts` 解析 `PREFS_STORE_PATH`(默认 `~/.web-terminal-prefs.json`,仿 push-store);新增 `src/http/prefs-store.ts`(`sanitizePrefs`/`loadPrefsStore`,镜像 subscription-store:全字段有界强制、缺失/损坏→空默认、不可变替换);`src/server.ts``GET /prefs`(只读无 Origin 守卫,同 /projects)+ `PUT /prefs`(状态变更→`requireAllowedOrigin` CSRF 守卫 + body sanitize)。
- 前端: 新增 `public/prefs.ts`(`loadPrefs/savePrefs`:服务端为准、离线回退 localStorage 镜像、迁移旧 `proj-favs`);`public/projects.ts` 加纯函数 `groupProjects()`(depth-2 命名空间键 + `MIN_GROUP_SIZE=2` 阈值→单成员落 "Other" + 无命名空间收益时退化为单 flat 组 + 命名空间组按最近活跃排序 + 复制活跃项进置顶 "Active now" 组)、`displayLabel()`(组内卡片只显示前缀后的尾段,如 `Payment`)、`makeGroupHeader()`(caret+label+count+"N active" 徽标,可折叠/键盘可达,Active-now 不可折叠);`makeProjectCard` 加可选 `displayName` 形参;`renderGrid` 改为按组渲染(每组一个 `.proj-grid-cards` 子网格 + 折叠态来自内存 prefs + 搜索时强制展开命中组 + 保留 scroll);`mountProjects` 状态改用内存 `favs`/`collapsed`,`init()` 首次 `loadPrefs` 后再渲染,5s tick 只拉 projects 不重载 prefs(避免覆盖本地编辑);折叠/收藏改走 `persist()``style.css`:`.proj-grid` 改为纵向 section 栈 + 新 `.proj-grid-cards` 承接原 minmax 网格 + `.proj-group*` 组头(sticky、hover、focus-visible、`● N active` 绿色徽标)。
- **未做(按用户选的最小范围)**: 密度/紧凑列表切换、group-by 控件、frecency、模糊搜索、手动文件夹——均为探索报告里的后续可选层。
- **验证**: `npx tsc --noEmit` 干净;`npm run build:web` OK(bundle 665kb);`vitest run` 1227 passed(新增 `test/prefs-store.test.ts` 12 项 sanitize/load/persist + `test/projects-panel.test.ts` 加 21 项 groupProjects/displayLabel)。真浏览器(临时 server + 假点名 repo):Projects 页渲染出 `▼ Billo.Platform ·8`(卡片尾段 B2B.Api/Payment/…)、`▼ Billo.Infrastructure ·2`(EventBus/MongoDb)、`▼ Other ·12`;点组头折叠 → live DOM `collapsed`+0 卡、`PUT /prefs``{"Billo.Platform":true}`;**整页 reload 后仍折叠(从服务端 /prefs 载回)**=跨设备持久化打通。控制台 503/404 为既有(无 VAPID 的 push + 缺图标),非本次改动;/prefs、/projects、/config/ui 均 200。
- **决策 / 偏离 PLAN**: 无既有 PLAN 覆盖此增量;探索用 4 个并行 general-purpose agent(用户明确"用多 agent 探索")。`getFavs/saveFavs` 仍导出(旧测试引用)但 mountProjects 不再调用,由 `public/prefs.ts` 取代。
- **遗留 / 待办**: ① 两个同名 depth-2 repo(如两个 `Billo.Customer`)会成一个以自身命名的组、卡片显示全名(displayLabel 无尾段)——功能正确、观感略冗余,可接受。② 5s tick 不重载 prefs → 另一设备的改动要下次进页才见(单用户可接受)。③ 后续如上手动文件夹/密度切换,持久化接口已就绪(prefs-store 可扩字段)。
- **commit**: (待提交)
### 2026-06-30 · v0.7 Walk-away Workbench — 多 agent 并行实现 (Band A+B)
- **状态**: `[x]` BUILT + 全量门禁绿(详细验收/打磨待续)。双 typecheck 干净 · `build:web` OK · 全量 vitest + 覆盖率 91.37/84.06/92.18/93.37(≥80×4)。
- **实现**: 按 `docs/PLAN_WALKAWAY_WORKBENCH.md` 的 27 任务波次(R0→W0→W1×14→W2→W3→W4)以 `module-builder` 并行实现;**23 任务 done, 0 blocked**。功能: A1 Web Push + 锁屏审批(新依赖 web-push)、A2 语音口述、A3 快捷回复 chips + 提示词调色板、A4 活动时间线、A5 卡住/静默告警;B1 只读 git diff、B2 statusLine 遥测仪表、B3 从 UI 建 worktree、B4 plan/权限模式中继。新增 `src/push/*``src/http/{diff,statusline}.ts``public/{diff,timeline,...}.ts`、statusLine 脚本等;扩 hook/manager/server-wire/termsession/tabs/projects-ui/sw。
- **Orchestrator 修复 (W0 协调缺口)**: T-types 漏给 `Config` 接口加 21 个 v0.7 字段(只在 config.ts 返回对象里);手工把 21 字段补进 `src/types.ts` 的 Config + 调 `config.ts`(签名回 `:Config`、base 去 satisfies)使其编译。移除过严的 W0 gate(全量 tsc 在契约领先实现期本就不能过——该检查归 W4/收尾)后 `resumeFromRunId` 续跑 W1W4。
- **W4 评审**: 安全 6 项全过(每个状态变更路由带 Origin/CSRF;VAPID 私钥不外泄/不记日志;git `execFile` 无注入;WS Origin 防御无回归)。**遗留(非阻塞, 无 CRITICAL/HIGH)**: ① protocol.ts `approve.mode` 校验应入 `parseClientMessage`(现 server.ts `parseApproveMode` 兜底, 功能与安全正确)② CSP `connect-src` 去掉 `ws:/wss:` 通配(既有, 低危)③ worktree `base` ref 加格式校验 ④ protocol/preview-grid 补 2 个针对性测试。完整 27 条任务日志见 workflow result 文件。
- **验证**: `npx tsc -p tsconfig.json/.web.json --noEmit` 干净;`npx vitest run --coverage` 全绿 ≥80×4;`npm run build:web` OK;`web-push` 已 npm install。
- **commit**: (本次提交)
### 2026-06-30 · v0.6 工具栏图标换成线性图标 + hover tooltip
- **状态**: `[x]` DONE。web typecheck 干净 · `build:web` OK · 真浏览器验证通过。
- **需求**(用户): 工具栏那些 emoji 图标(🔍⚙▦🕘⌨🔗📱)不好看,换成符合主题色的图标;hover 时显示图标含义。
- **改动**(纯前端): `public/icons.ts` 加 7 个 **lucide 线性图标**(MIT,`stroke=currentColor`,脚本从 unpkg 抓取生成):`ICON_SEARCH/SETTINGS/DASHBOARD/HISTORY/KEYBOARD/SHARE/DEVICE`。7 个工具栏模块(search/settings/dashboard/history/shortcuts/share/qr)把 `textContent=emoji` 换成 `innerHTML=ICON_*` + `dataset.tip`(tooltip 文案)+ `aria-label``style.css` `.toolbtn`:`svg` 18px、默认 `--text-dim`、hover 变 **Amber**;新增 `[data-tip]::after` **CSS hover tooltip**(右对齐避免右边缘裁切)。
- **验证**: `npx tsc -p tsconfig.web.json` 干净;`build:web` OK;bundle 内 0 toolbar emoji;真浏览器:7 按钮均为线性图标(`--text-dim` 色)、hover 变琥珀 + tooltip(实测 "All sessions" 等显示)。
- **遗留**: share/qr 的 aria-label 仍是旧描述(无害,屏幕阅读器友好);可选统一。
- **commit**: (本次提交)
### 2026-06-30 · v0.6 "New session" 改为网格卡片(去掉别扭的大按钮)
- **状态**: `[x]` DONE。web typecheck 干净 · `build:web` OK · 真浏览器验证通过。
- **需求**(用户): 头部那个大的实心琥珀 " New session" 按钮很别扭(与精致的分段 pill 不协调)。
- **改动**(纯前端 `public/launcher.ts` + style): 删头部 `.launcher-new` 按钮 + `.launcher-actions`;改为 grid 内的 **`.mg-new-card` 虚线" New session"瓦片**(与 session 卡同尺寸,始终排在网格第一格,每次 refresh 重新 prepend),hover 变琥珀。删冗余 `.mg-empty` 空态(瓦片即 CTA)。头部只剩标题 + 段控,更干净。
- **验证**: `npx tsc -p tsconfig.web.json` 干净;`build:web` OK;`preview-grid` 测试 14 全绿;真浏览器:Sessions 网格首格为虚线 New 瓦片 + 其后 session 卡(Open/Kill),无旧大按钮。
- **commit**: (本次提交)
### 2026-06-30 · v0.6 删除 manage 独立页 — Sessions 选择器即会话管理
- **状态**: `[x]` DONE。**453 测试全绿** · web typecheck 干净 · `build:web` OK(仅 main,无 manage 产物)· 真浏览器验证通过。
- **需求**(用户): manage 页面不要了——在 **Sessions 选择器**这里直接管理 session(添加 + 删除)。
- **改动**:
- **删** `public/manage.html` + `public/manage.ts`(及 build 产物);`package.json``build:web`/`dev:web` 去掉 `public/manage.ts` 入口。
- **删** `public/main.ts` 工具栏 🗂 按钮(原跳 `/manage.html`);**删** `public/launcher.ts` 头部 "🗂 Manage" 链接。
- **加** `launcher.ts` 每张 session 卡 **Kill ✕**(`makePreviewCard``extraActions`,复用 manage 的 `killOne`:`DELETE /live-sessions/:id` → refresh)。`.mg-kill` 样式已存在,直接复用。
- 后端 DELETE/preview 路由保留(仍由 launcher kill + 缩略图 + projects 复用);注释里的 "manage page" 字样属历史,无害。
- **结果**: Sessions 视图 = 完整会话管理——**New session**(加)· **Open ↗**(进)· **Kill ✕**(删),配实时缩略图。bulk「Kill all/detached」未迁移(如需可加)。
- **验证**: `npx tsc -p tsconfig.web.json` 干净;`npx vitest run` 453/453;`build:web` 仅出 main(无 manage.*);真浏览器:Sessions 卡含 Open+Kill、无 Manage 入口、点 Kill 2→1、`GET /manage.html`→404。
- **遗留**: manage-only 死 CSS(`#manage-root`/`.mg-header`/`.mg-bar`/`.mg-title`/`.mg-sub`/`.mg-btn`)留作可选清理;README 若提 manage 页待更新。
- **commit**: (本次提交)
### 2026-06-30 · v0.6 项目网格卡片 — 直接关掉 session
- **状态**: `[x]` DONE。51 projects-panel 测试全绿(+1)· web typecheck 干净 · `build:web` OK · 真浏览器验证通过。
- **需求**(用户): Projects 页面(网格卡片)下应该能直接关掉 session(此前只有详情页有 Kill,卡片 session 行只能点进入)。
- **改动**(纯前端 `public/projects.ts` + style): `makeSessionRow` 加可选 `onKill` → 行尾 ✕ 按钮(`.proj-session-kill`,stopPropagation 不触发进入);`makeProjectCard` 加可选 `onKillSession` 透传;`mountProjects``killAndRefresh`(`DELETE /live-sessions/:id` 后 refresh)传入网格卡片。CSS `.proj-session-kill`(hover 变红)。
- **测试**(+1): 卡片 session 行——无 onKillSession 时无 ✕;有时点 ✕ 调 onKill(id) 且不触发 onEnterSession。
- **验证**: `npx tsc -p tsconfig.web.json` 干净;`npx vitest run projects-panel` 51/51;真浏览器:web-terminal 卡 2 个 session 行 → 点 ✕ → 杀掉并刷新为 1 行。
- **commit**: (本次提交)
### 2026-06-30 · v0.6 项目详情页 — 展示 CLAUDE.md + /init 生成/更新按钮
- **状态**: `[x]` DONE。**458 测试全绿**(+5)· 双 typecheck 干净 · `build:web` OK · 真端到端(curl + 真浏览器)验证通过。
- **需求**(用户): 详情页展示 CLAUDE.md,加一个按钮自动更新 / 生成 CLAUDE.md。决策(AskUserQuestion): **展示 + 一个交互式 /init 按钮**(非静默后台覆盖,便于 review)。
- **改动**:
- 后端: `ProjectDetail``hasClaudeMd`/`claudeMd?`;`projects.ts` `readClaudeMd`(读 `<repo>/CLAUDE.md`,截断 64KB,best-effort)并入 `buildProjectDetail` 的 Promise.all。
- 前端: `renderProjectDetail`**CLAUDE.md 段**——标题行带按钮(有则 `↻ Update`、无则 `✨ Generate`),内容滚动框(`.proj-claudemd`,monospace,max-h 320);无则空态提示。按钮 → `onOpenProject(path, name, 'claude "/init"\\r')` 开交互式 Claude 跑 `/init`(复用 launcher,零新后端,byte-shuttle 不变)。style 加 `.proj-section-row`/`.proj-claudemd-btn`/`.proj-claudemd`
- 测试(+5): buildProjectDetail 读/无 CLAUDE.md;renderProjectDetail 有内容+Update、无内容+Generate+空态、按钮调 `onOpenProject(...,'claude "/init"\\r')`
- **验证**: `npx tsc`(双)干净;`npx vitest run` 458/458;`build:web` OK;curl `/projects/detail` 返回 `hasClaudeMd:true` + 13KB 内容;真浏览器:web-terminal 详情显示 CLAUDE.md 滚动框 + ↻ Update 按钮(段序 Branch/Active sessions/CLAUDE.md/Start)。
- **注**: `/init``claude "/init"` 作为初始 prompt;若该形式不直接触发斜杠命令,会话是交互式的、用户可手动输 `/init`(一行可调)。
- **commit**: (本次提交)
### 2026-06-30 · v0.6 项目详情页 — branch/worktree + 详细 active session
- **状态**: `[x]` DONE。**452 测试全绿**(+21)· 双 typecheck 干净 · `build:web` OK · 覆盖率 90.16/81.32(`worktrees.ts` 100% / `projects.ts` 93.8%)· 真端到端(curl + 真浏览器)验证通过。
- **需求**(用户): 每个 project 的**详情页**——详细显示具体 active 的 session,以及具体的 **branch 或 worktree**
- **设计**: 点项目卡的**名字**进入应用内详情视图(`renderProjectDetail`,在 Projects 面板内切换,带"← All projects"返回)。分区:① 头部(名/路径/branch chip/dirty)② **Branch / Worktrees**(`git worktree list` 每条:branch 或 `detached @ head` + main/current/locked/prunable 标签 + 路径;单 worktree 即显示分支)③ **Active sessions(N)**(每条:状态点 + 标题 + `状态·👁设备数·started 多久前` + **Open** + **Kill ✕**)④ **Start** 启动器行(复用 Claude/Codex/Code)。再进 Projects 总是回到网格(不残留详情)。
- **后端**: 新增 `src/http/worktrees.ts`(`parseWorktrees`(纯)+ `listWorktrees`,`git worktree list --porcelain`,execFile 无 shell,2s 超时,best-effort→[]);`projects.ts``buildProjectDetail(cfg, path, liveSessions)`(校验绝对存在目录→否则 null;复用 readBranch/readDirty/hasGitEntry/matchSessions;**不缓存**,详情按需要新鲜)。`src/server.ts` `GET /projects/detail?path=`(只读、无 Origin 守卫,同 /projects;path 缺→400、非法/不存在→404)。types 加 `WorktreeInfo`/`ProjectDetail`
- **前端**(`public/projects.ts` + style): `fetchProjectDetail`/`killSession`;`makeProjectCard` 加可选 `onOpenDetail`(名字变可点链接 `.proj-name-link`);`mountProjects` 加 grid↔detail 视图切换 + 自动刷新路由(详情每 5s 刷新);Kill 走 `DELETE /live-sessions/:id`(同源带 Origin)。详情视图样式 `.proj-detail*`/`.proj-wt-*`/`.proj-dsession*`。session meta 在 status=unknown 时省略状态点(不显示 `· ·`)。
- **测试**(+21): `test/worktrees.test.ts`(parse:单/多/detached/locked/prunable/无尾空行/空);`projects.test.ts` buildProjectDetail(相对/不存在/文件→null、非 git 目录、假 repo 读 branch、session 按 cwd 前缀归并、**真 git init worktree**(realpath 规避 /var symlink));`projects-panel.test.ts` renderProjectDetail(null→not found+Back、头部、worktree 行+current tag、detached、session Open/Kill 接线+忽略 exited、空态+启动器、非 git)+ 名字链接。
- **验证**: `npx tsc`(双)干净;`npx vitest run --coverage` 452/452(≥80×4);真浏览器:开 web-terminal 的 Claude session→进详情→显示 branch `v0.6-projects`(MAIN/CURRENT)+ Active sessions(1)(web-terminal,👁1,Open/✕)+ Start 启动器 + 返回。curl `/projects/detail` 返回 worktree(main/current/head)+ 缺/相对 path→400/404。
- **安全**: `/projects/detail` 只读(git branch/status/worktree-list,execFile 无 shell + 超时),path 校验绝对存在目录;与 /projects 同威胁模型(不新增可变更面)。
- **commit**: (本次提交)
### 2026-06-30 · v0.6 项目卡启动器 — Claude · Codex · VS Code logo 按钮
- **状态**: `[x]` DONE。覆盖率 90.6/81.48/88.71/92.71(≥80×4)· 双 typecheck 干净 · `build:web` OK · 真浏览器 + 真 VS Code 端到端验证通过。
- **需求**(用户): 把卡片上大的" start claude here"换成 **Claude logo**;新增 **VS Code logo**(点击在主机打开 VSC)与 **Codex logo** 按钮;logo 编排要"符合应用性"。
- **设计**: 每张项目卡的动作区改为**启动器行**——3 个品牌 logo 图标按钮(logo 在上、小字标签在下、touch 友好、各带 tooltip),有运行中 session 时 session 行列在启动器上方。Claude=珊瑚色爆发、Codex=OpenAI blossom(近白)、Code=VS Code 蓝丝带;logo 用 `fill=currentColor`,颜色由 CSS 品牌色驱动。
- **行为**: **Claude**→新终端 tab 跑 `claude`;**Codex**→新终端 tab 跑 `codex`(复用 `openProject(path,name,cmd)`,`onOpenProject``cmd?` 参数);**VS Code**→`POST /open-in-editor` 让**主机**跑 `code <path>`(文件在主机,故在主机开;不分查看设备)。主机已确认装有 claude/codex/code。
- **活跃高亮**(用户追加): 项目若有**活跃 Claude session**(运行中且 hook 状态 ≠ `unknown`——纯 shell/codex 保持 unknown 不算),Claude logo 按钮**珊瑚色高亮**(coral ring + 淡底 + 浅色标签);>1 个时角标显示数量。`activeClaudeCount(project)` 判定。
- **改动**:
- 新增 `public/icons.ts`(simple-icons 的 Claude/OpenAI/VSCode 单路径 SVG,脚本生成避免手抄出错)。
- `public/projects.ts`: `makeLauncherRow`/`makeLauncher` + `openProjectInEditor`(fetch `/open-in-editor`);`ProjectsHooks.onOpenProject``cmd?`;导出 `makeProjectCard` 便于测试;删旧 `.proj-new` 用法。`public/tabs.ts` 透传 cmd。`public/style.css` 新增 `.proj-launchers/.proj-launch*`(品牌色),删旧 `.proj-new` 规则。
- 后端: `src/http/editor.ts` `openInEditor(cfg,path)`(校验绝对路径+存在+目录→`execFile`(无 shell)启动,best-effort,error 事件兜底+unref);`src/server.ts` `POST /open-in-editor`(Origin 守卫,与 DELETE 同档);`Config.editorCmd`(`EDITOR_CMD`,默认 `code`)入 types/config。
- 测试: 新增 `test/editor.test.ts`(校验 400/404 + `editorCmd='true'` 成功 204);`config.test.ts` 加 editorCmd;`projects-panel.test.ts` 加 makeProjectCard 启动器行(3 按钮/logo/Claude·Codex cmd)。
- **验证**: `npx tsc`(双)干净;`npx vitest run --coverage` 全绿(≥80×4);`build:web` OK。真端到端:curl `/open-in-editor` 无 Origin→403、缺/相对/不存在 path→400/400/404、合法 dir→204 且 **VS Code(Electron pid)实际打开**;真浏览器 Projects 视图每卡 3 logo 按钮(珊瑚/白/蓝,标签 Claude/Codex/Code,tooltip 正确)。
- **安全**: `/open-in-editor` 状态变更→Origin 守卫(CSRF);`execFile` argv 传参无 shell 注入面;path 校验为绝对存在目录;不超出"应用本就给主机 shell"的既有威胁模型。命令来自 config 非用户。
- **commit**: (本次提交)
### 2026-06-30 · v0.6 UI 打磨 — 首页段控重设计 + 选择器隐藏键栏
- **状态**: `[x]` DONE。415 测试全绿 · 双 typecheck 干净 · `build:web` OK · 真浏览器验证通过。
- **反馈**(用户): ① Sessions/Projects 段控在 UI 上"有点别扭"(原为两个独立描边方块浮在顶部);② 首页(Sessions/Projects 视图)底部那排终端快捷键栏没必要显示。
- **修复**(纯前端,`public/style.css` + `public/tabs.ts`): ① `.home-seg` 重设计为 **iOS 风分段控件**——一个圆角"轨道"(`surface-2` 底 + 圆角 999px + 内边距)内含两个 pill 按钮,激活项填 Amber;`width:fit-content + margin:auto` 居中;去掉 first/last/only-child 圆角拼接;面板 `inset` 44→68px 适配新高度。② `TabApp.updateHomeView` 在显示首页时给 `document.body``home-open` 类,CSS `body.home-open #keybar{display:none}` 隐藏键栏;打开 tab(终端激活)即恢复。
- **追加**(用户反馈"段控独占一行、空白太多"): 桌面端(`@media min-width:769px`)把段控 `position:absolute` 浮到**右上角**,与过滤框/启动器表头**同一行**;面板 `inset` 归 0(从顶部铺满),`.launcher-head` 留 240px 右内边距避开段控;移动端保持居中堆叠(防止与全宽过滤框重叠)。另:首页隐藏键栏时 `body.home-open #term{bottom:0}` 收回键栏占位。纯 CSS(`style.css` 直出,无需打包)。真浏览器实测:Projects 段控与过滤框同行(seg.top 56 / filter.top 62)、卡片在其下;Sessions 视图段控不与 New/Manage 重叠。
- **commit**: (本次提交)
### 2026-06-30 · v0.6 UX 修复 — 会话内无法返回 Projects 首页
- **状态**: `[x]` DONE。**415 测试全绿**(+3)· 双 typecheck 干净 · `build:web` OK · 真浏览器验证通过。
- **问题**(用户反馈): 打开某个项目进入终端 tab 后,首页的 Sessions/Projects 段控被隐藏,只能**关掉所有 tab** 才能回到选择器再开下一个项目——没有"返回 Projects"的入口。
- **修复**(`public/tabs.ts` + `public/style.css`): 标签栏新增 **⌂ Home 按钮**;点击把首页选择器(Sessions/Projects 段控 + 对应面板)**叠加显示在当前 tab 之上**(`homeForced` 标志:`updateHomeView` 改为 `tabs.length===0 || homeForced` 时显示并隐藏所有终端 pane)。再点 ⌂ 或点任一 tab(`activate` 重置 `homeForced`)即回到终端;从叠加层开新会话/项目也会自动收起。关最后一个 tab 时复位 `homeForced`。⌂ 叠加时高亮(`.tab-home.active`,Amber)。
- **测试**(`test/tabs.test.ts` +3): ⌂ 叠加首页再切回;叠加层切到 Projects 并开第二个项目后自动收起;0 tab 时 ⌂ 为 no-op。
- **验证**: `npx tsc -p tsconfig.json/.web.json` 干净;`npx vitest run` 415/415;真浏览器:开 shell tab→段控隐藏→点 ⌂→段控+面板叠加显示、⌂ 高亮→切 Projects 面板可见→再点 ⌂→回到终端。
- **commit**: (本次提交)
### 2026-06-30 · v0.6 Project Manager(P1P6,多 agent Workflow 编排)
- **状态**: `[x]` DONE(分支 `v0.6-projects`)。**398 测试全绿** · 双 typecheck 干净 · `build:web` OK · 覆盖率 90.98/81.95/89.28/93.16(≥80×4)· **真浏览器 smoke 通过**
- **目标**: 首页新增 **Projects 视图**——发现主机上的 git 仓库,点卡片 → 开 tab(标签=repo 名、cwd=仓库、自动跑 `claude`);卡片显示分支/dirty/该项目运行中的 sessions(1:N)。设计见 `docs/FEATURE_PROJECT_MANAGER.md`
- **编排**(ultracode `Workflow` 动态): Build(P2 后端 ∥ P5 前端)→ Wire(P4 端点 ∥ P6 接线)→ Review(`module-reviewer` 验收 + `typescript-reviewer` + `security-reviewer` 并行 report-only)→ HIGH/CRITICAL 对抗式 Verify → Fix。**P6 首跑 transient `server_error` 挂掉(0 改动落地)→ `TaskStop` + `Workflow{resumeFromRunId}` 恢复;P2/P4/P5 缓存命中,P6 重跑成功。** 地基 P1(types)/P3(config)上一会话已落,本会话先 commit 为 `dc5d073` 作干净基线。
- **改动**:
- `src/http/projects.ts`(新,P2): `parseGitHead`(纯)、`buildProjects(cfg, liveSessions)`;内部 `scanRepos`(BFS 深度上限/跳 node_modules·dotdir·symlink/命中即停)、`readBranch``readDirty`(`execFile` git status,2s 超时,并发 8)、`mergeHistory`(复用 `history.listSessions`)、缓存 `discoverProjects`(TTL + in-flight 去重)、fresh session 按 cwd 前缀归并、`_clearProjectCache`(测试)。`test/projects.test.ts`(15)。
- `public/projects.ts`(新,P5): `mountProjects` + 纯 helper `filterProjects/sortProjects/getFavs/saveFavs/toggleFav/normalizeProject``test/projects-panel.test.ts`(36)。`public/style.css` 追加 Projects 面板 + `.home-seg` 段控样式。
- `src/server.ts`(P4): `GET /projects`(只读、无 Origin 守卫、try/catch→`[]`)。`test/integration/projects-endpoint.test.ts`(5,真起 server)。
- `public/tabs.ts`(P6): `openProject`(同名 `#n``addEntry(...,'claude\r')`)、`countOpenWithTitlePrefix``updateHomeView` + `.home-seg` 段控。`test/tabs.test.ts` +7。
- **Review→Fix**: 12 findings,4 HIGH 全确认修复——① **A3 关键缺回车 `\r`**(`openProject`/`newTabForResume`)② 段控被定位面板遮挡(面板 `inset:44px 0 0 0`)③ `mergeHistory` 串行→`mapWithConcurrency` ④ discover 并发缓存击穿→in-flight 去重。Orchestrator 追加加固(MEDIUM/LOW + 测试): `normalizeProject`(API 边界校验)、`getFavs` 滤非字符串、`parseProjectRoots` 相对路径 fail-fast、config v0.6 解析补测(config.ts→98%)。
- **验证**: `npx tsc -p tsconfig.json/.web.json --noEmit` 干净;`npx vitest run` 398/398;`npm run build:web` OK;`npx vitest run --coverage` 退出 0(阈值 80×4)。真浏览器(`PROJECT_ROOTS=~/Documents/git`,83 repo):段控切换 ✓、面板 83 卡片(分支/dirty/斜杠分支正确)+ 搜索 ✓、点卡 → tab `web-terminal` + 终端出现 claude 启动界面(A3 端到端)✓、console 无 v0.6 报错(仅既有 favicon 404)。
- **决策 / 偏离**: 缓存只缓存磁盘发现、session 每次 fresh 归并(避免陈旧);git worktree 看板/文件树编辑器/ticket 拉取 = v0.6 明确不做(FEATURE §9)。`public/projects.ts`/`tabs.ts` 的 mount* DOM 接线沿用既有约定不纳入覆盖率(纯 helper 已测)。`style.css` 1143>800 = 既有债,留作拆分。
- **遗留 / 待办**: 真机 F4/F7(同历史);可选 `server.ts``http/routes.ts`;`style.css` 拆分。
- **commit**: (本次提交)
### 2026-06-16 · T1 脚手架 + 一次性装齐依赖
- **状态**: `[x]` DONE
- **改动**: `package.json`(scripts + 全部依赖 + `postinstall`)、`tsconfig.json`(后端 NodeNext/strict)、
`tsconfig.web.json`(前端 DOM/Bundler,当前 noEmit 仅类型检查)、`vitest.config.ts`(node 环境 +
`passWithNoTests`)、`.gitignore``package-lock.json`、目录骨架 `src{,/http,/session} public test{,/helpers,/integration}`(各带 `.gitkeep`)。
- **验证**:
- `npm install` 成功(运行时 72 包 + 开发包,0 漏洞)。解析版本(均比预期新的主版本):express **5.2**
@xterm/xterm **6.0**、addon-fit 0.11、ws 8.21、node-pty 1.1;dev:typescript **6.0**、vitest **4.1**、tsx 4.22、@types/node 25。
- `node -e require('node-pty')` → OK;**真实 PTY spawn** `echo hello-from-pty` → 16 bytes、exitCode 0(沙箱外验证)。
- `npm test` → exit 0(passWithNoTests)。
- **决策 / 偏离 PLAN**:
1. **node-pty spawn-helper 修复**:1.1.0 的 prebuild 自带 `spawn-helper` 但权限 `-rw-r--r--`(缺 +x),
导致 `posix_spawnp failed`。加 `postinstall: chmod +x .../prebuilds/darwin-*/spawn-helper`(对 Linux 无害,`|| true`),
使每次 install / 每个 worktree 自动修复。**T12 spawn PTY 依赖此修复。**
2. 依赖解析到较新主版本(express 5 / xterm 6 / typescript 6 / vitest 4):**T7/T11/T14/T15 注意 API 差异**
(express 5 路由、xterm 6 import 形态)。
3. 加了 PLAN 未列的脚本 `dev`/`typecheck``postinstall`(便利 + 必需修复),属合理补充,未改 PLAN 意图。
- **遗留 / 待办**: 无(原前端打包待决项已解决,见下补记)。
- **commit**: `b126cf0`
### 2026-06-18 · v0.3 Step 5 — 中价值 M3/M7/M6(主题/仪表盘/同目录新标签)
- **状态**: `[x]` DONE。真浏览器验证通过。212 测试全绿。
- **M3 主题/字体**(commit `f79f14b`):`settings.ts`(⚙ 面板:dark/light/solarized + 字号 A/A+),localStorage 持久化;`terminal-session.applyTheme`;TabApp.applySettings 应用到所有+新标签。验证:light 主题渲染+持久化。
- **M7 仪表盘**(commit `f79f14b`):`dashboard.ts`(▦ 浮层列出所有标签:连接点+文件夹+Claude 状态;点行切换;打开时每秒刷新);TabApp.snapshot()/focusTab()。验证:列出 2 标签、点击切换。
- **M6 同目录新标签**(commit `87e1173`):`title-util.cwdFromOsc7`(解析 OSC 7 cwd)+4 单测;terminal-session 注册 OSC7 handler→cwd getter,新会话 attach 带 cwd;protocol/types attach.cwd(绝对路径);session/manager/server 透传到 spawn cwd;tabs `+` 在当前 tab 的 cwd 开新标签(无则回 home)。验证:server attach.cwd 生效;`cd /private/tmp``+`→新标签 pwd=/private/tmp(此 zsh 发 OSC7)。
- **工具栏**: 🔍搜索 · ⚙设置 · ▦仪表盘 · 📱QR。
- **commit**: `f79f14b`(M3+M7)、`87e1173`(M6)
### 2026-06-17 · v0.3 Step 4 — H1 tmux 保活(服务重启不丢会话)
- **状态**: `[x]` DONE。**真 tmux 集成测试验证通过**(commit `9099f73`)。**至此 H1H4 全部高价值功能完成。**
- **机制**: useTmux 时把 shell 跑进 `tmux new-session -A -s web_<id>`;node-pty 进程只是 tmux **客户端**。服务重启后,客户端带同 sessionId 重连 → manager 发现 `web_<id>` tmux 会话仍在 → 用同 id 重建客户端(`-A` 附着)→ 接回原 shell。
- **改动**: 新增 `src/session/tmux.ts`(同步 CLI 封装 available/has/kill,best-effort);config `useTmux`(env `USE_TMUX` 1/0/auto→检测 PATH);session createSession 加可选 id(重连用)+ tmux 分支 spawn + `Session.tmuxName` + kill 走 `tmux kill-session`;manager handleAttach 加 Case 3.5(重启后重附)+ shutdown 对 tmux 会话**只杀客户端 pty 不杀 tmux**(保活)。
- **测试**: 共享集成 cfg 设 `USE_TMUX:0`(防 tmux 泄漏);单测 CFG `useTmux:false`;集成 `H1`(真 tmux,gate=tmux+PTY):设 shell 变量 → **重启 server** → 同 id 重连 → 变量仍在(`MARK-tmuxlives`)。208 测试全绿,tsc 通过。
- **注**: 当前后台 dev server(:3000)是 H3 之前启动的、不含 H1;要试 tmux 保活需 `USE_TMUX=1 npm start`
- **commit**: `9099f73`
### 2026-06-17 · v0.3 Step 3 — H3 远程批准/拒绝(不打字)
- **状态**: `[x]` DONE。**端到端真浏览器验证通过**(commit `9a6150c`)。
- **机制**: `PermissionRequest` hook 用 curl POST 到 `/hook/permission` 并**阻塞等响应**;服务端长挂该 HTTP 响应,推 `status{waiting,pending:true}` 给前端 → 前端显示 **[✓Approve][✗Reject]** 横幅 → 点按发 ws `approve`/`reject` → 服务端用 `{hookSpecificOutput.decision.behavior:allow|deny}` 回应被挂的请求 → curl stdout 把决定交给 Claude。**全程不打字**。
- **兜底**: 5 分钟超时 / 断开即释放 → 回空 `{}`,Claude 回退到自己的交互提示;web-terminal 外 curl 无 URL → no-op。
- **改动**: types(ClientMessage approve/reject、status.pending)、protocol(白名单+解析)、manager(pending 透传)、server(`/hook/permission` 长挂 + approve/reject 路由 + resolvePending)、setup-hooks(PermissionRequest 用 held 命令)、FE(terminal-session approve()/reject()+pending、tabs 批准横幅)。
- **验证**: 207 测试全绿(协议 approve/reject 单测 + 集成 ⑧ held→approve→allow);浏览器实测横幅"Claude wants to use Bash"→Approve→held 请求得到 allow→横幅消失(截图 /tmp/v03-approve.png)。
- **commit**: `9a6150c`
### 2026-06-17 · v0.3 Step 2 — H2/H4 Claude 状态感知(headline)
- **状态**: `[x]` DONE(分支 `v0.3-cockpit`)。**端到端真浏览器验证通过**。
- **机制**: Claude Code hooks → 服务端 `/hook` 侧信道 → WS `status` 帧 → 标签角标实时更新。架构演进:服务端获得 hook 侧信道(终端流仍是 byte-shuttle)。
- **H2 服务端**(commit `a411c89`):
- types:`ClaudeStatus` + ServerMessage `status`;Session.claudeStatus;manager.handleHookEvent。
- session:spawn env 注入 `WEBTERM_SESSION`(哪个标签)+ `WEBTERM_HOOK_URL`(本机 /hook)。
- `src/http/hook.ts` 纯映射(PreToolUse→working、PermissionRequest/permission_prompt→waiting、Stop/idle_prompt→idle);6 单测。
- server:`POST /hook`(仅 loopback,express.json 64kb)→ 推 status。**顺带修真 bug**:`closeIdleConnections()` 让 hook keep-alive 空闲连接不再卡住 close()/SIGTERM。
- 集成 ⑦(真 PTY):attach → POST /hook → ws 收到 status(注意测试要在 fetch 前挂监听,推送是同步的)。
- **H4 前端**(commit `c81cebd`):
- terminal-session 处理 status → claudeStatus + onClaudeStatus。
- tabs:每标签 Claude 角标(⚙工作/⏳等批准/✓空闲);**非活动 + waiting 标签加琥珀高亮 + 浏览器通知**(首次切换请求权限)。
- `scripts/setup-hooks.mjs` + `npm run setup-hooks`:幂等装/卸 ~/.claude/settings.json 的 command hook(内联 curl,web-terminal 外是 no-op),自动备份。
- **验证**: 205 测试全绿;浏览器实测 PermissionRequest→⏳、PreToolUse→⚙、Stop→✓;非活动 waiting 标签琥珀+⏳(截图 /tmp/v03-status.png)。0 console error。
- **commit**: `a411c89`(H2 服务端)、`c81cebd`(H4 前端+脚本)
### 2026-06-17 · v0.3 Step 1 — 前端快赢(键栏/搜索/链接/QR/PWA)
- **状态**: `[x]` DONE(分支 `v0.3-cockpit`;纯前端,无服务端改动)
- **计划**: `~/.claude/plans/shimmering-wondering-island.md`(v0.3 cockpit,已批准)。
- **H4-keybar**: 键栏加 Claude 快捷键 Esc·Esc/^O/^T/^B + 每键 tooltip(KEY_MAP 14 键;commit `eaeacf3`)。
- **M1 搜索**: `@xterm/addon-search` 每终端 + 🔍 工具栏按钮 + 搜索框(Enter 下一个/Shift+Enter 上一个/Esc 关)。
- **M2 可点击链接**: `@xterm/addon-web-links`
- **结构**: 标签栏拆成 `#tabs`(可滚)+ `#toolbar`(工具按钮区),为后续 QR/设置/仪表盘复用。
- **M5 QR**: 📱 按钮 → 客户端渲染 location.origin 的二维码(经 LAN IP 打开即可分享)+ localhost 提示(commit `fbc218c`)。
- **M4 PWA**: manifest + icon.svg + sw.js(network-first,不拦 /term//hook)+ 注册;可安装。
- **验证(真浏览器)**: 14 键栏按钮、搜索高亮命中、🔍📱 工具按钮、SW controlled、QR canvas 渲染、0 console error。199 测试全绿、双 tsc、build 通过。截图 /tmp/v03-quickwins.png、/tmp/v03-qr.png。
- **commit**: `eaeacf3``fbc218c`
### 2026-06-17 · v0.2 修复 — 切换标签要点多次
- **状态**: `[x]` FIXED
- **现象**: 点标签切换常常点一两下不生效,要多点几次。
- **根因**: 整个标签 `draggable=true`,点击时鼠标**微动一点点就被当成拖拽**(而非点击)→ 切换失效;且每次 activity/title/status 事件都 `replaceChildren()` 全量重建标签栏,点击瞬间元素可能被销毁。
- **修复**(`tabs.ts` 重构渲染):① 切换改 `pointerdown`(按下即切,先于拖拽判定,触摸也生效);② 标签 DOM 建一次,状态点/未读/标题**就地更新**(refreshTab),仅增删/排序/改名才全量重建;③ 关闭按钮/改名框 pointerdown stopPropagation 避免误激活。
- **验证(真浏览器)**: 连续单击 tab0→2→1→0 active 每次都跟随;CDP 真实点击也切换;改名/拖拽不受影响。198 测试全绿。
- **commit**: `e058751`
### 2026-06-17 · v0.2 — 标签按当前文件夹自动命名
- **状态**: `[x]` DONE(纯前端)
- **需求**: 标签名用当前文件夹,易区分。
- **改动**: 新增 `public/title-util.ts``folderFromTitle()`——解析 shell OSC 标题 `user@host:~/path` 取最后一段目录名(home 保留 `~`);纯函数、无依赖、**7 单测**。`terminal-session.ts``onTitleChange` 经此函数转为文件夹名。标题优先级:手动改名 > 文件夹名 > Term N。
- **验证(真浏览器)**: 198 测试全绿、tsc+build ✓。实测 `cd` 实时更新:`~` → 项目目录 `web-terminal``docs``~`
- **commit**: `4091283`
### 2026-06-17 · v0.2 — 标签状态点 + 活动指示 + 拖拽排序
- **状态**: `[x]` DONE(纯前端)
- **改动**:
- `terminal-session.ts`:跟踪 `SessionStatus`(connecting/connected/reconnecting/exited)+ `onStatus` 回调。**Bug 修复**:`onTitle` 之前只在 opts 里、构造函数从没绑定 `onTitleChange` —— 自动标题原本是死代码,现已接上(实测两标签显示 `host:~` 自动标题)。
- `tabs.ts`:每标签**状态点**(颜色=连接态:绿/琥珀/红);**未读活动**(后台标签来新输出→`hasActivity`→蓝环,切到该标签即清);**HTML5 拖拽排序**(保留活动标签、持久化)。
- `style.css`:`.tab-dot` 配色、`.unread` 蓝环、`.dragging`/`.drag-over` 反馈。
- **验证(orchestrator,真浏览器)**: 191 测试全绿、双 typecheck、build:web ✓。browse 实测:连接后绿点;Term1 后台收到延迟输出→未读蓝环(`unread:true`);拖拽 `AAA|BBB``BBB|AAA` 且**reload 后顺序+自定义名都保留**。截图 /tmp/webterm-dots.png。
- **commit**: `b5f21e5`
### 2026-06-17 · v0.2 — 多标签 + 标题 + Claude Code 快捷键栏
- **状态**: `[x]` DONE(纯前端;后端 manager 早已多会话,无需改后端/协议)
- **需求**: 用户要 ①多标签 ②标签加标题方便区分 ③适配平板/手机 ④移动端易触发 Claude Code 快捷键(按钮触发快捷键)。
- **UX 调研**(general-purpose 联网,参考 iTerm2/Warp/VS Code/Windows Terminal/Ghostty/Chrome):双击就地改名是现代主流(Warp/VS Code/Arc);自动标题用 OSC 0/2;中键关闭、× 悬停显示、省略号+tooltip、Cmd/Ctrl+数字切换是 Tier-1。验证了"OSC 自动标题 + 双击改名"方案正确。
- **改动**(`public/`):
- 新增 `terminal-session.ts`:从旧 main.ts 抽出"单会话 = 1 Terminal + 1 WS + 重连",加 show/hide/fit/focus/dispose + onTitle/onActivity 回调。
- 新增 `tabs.ts`:TabApp 多标签管理 + 标签栏 UI。**每标签独立 WS/会话**(沿用 1WS↔1会话协议);自动标题(onTitleChange)+ **双击就地改名**(手动>自动>Term N);中键关闭、× 按钮、省略号+tooltip;localStorage 持久化 `{id,title}[]`(兼容 v0.1 单会话 key 迁移)。关闭标签=detach(PTY 不死)。
- `keybar.ts`:加 `slash` 键;导出 `KEYBAR_BUTTONS` 按 Claude 频率排序(Esc 标 primary);+4 单测。
- `main.ts` 瘦身为引导;`index.html``#tabbar`;`style.css` 重写:键栏**全设备常显**(桌面也可点按钮触发快捷键)、`@media(pointer:coarse)` 触摸加大、Esc 琥珀色高亮、标签栏/改名框/活动下划线。
- **决策**:键盘快捷键(Cmd+T 等)**不做**——浏览器宿主会抢,改走点击/触摸 UI(对 web 终端正确取舍)。
- **验证(orchestrator,关沙箱真浏览器)**: 两 typecheck ✓;`npm test` **191 全绿**(+4 keybar);`build:web` ✓。browse 实测:双击改名 Term1→"claude"、两标签会话独立(claude | Term 2)、键栏桌面可见 10 键 Esc primary、窄屏可滚动可用。截图 /tmp/webterm-ux-desktop.png、-phone.png。
- **commit**: `4f66016`
### 2026-06-17 · W5 — 验收 + 收尾(T16T21)
- **状态**: T16 ✅ · T17 部分(F9✅/F4 待真机)· T18 ✅ · T19 待真机 · T20 ✅ · T21 ✅
- **T21**(orchestrator):写 `README.md`(install/run/config/security);安全自查——无硬编码 secret(grep 空)、Origin+路径+maxPayload 校验已接线(server.ts 确认)、无 eval/exec、input 原样透传给 PTY(byte-shuttle 设计意图);`npm run build``dist/server.js` 可产出。
- **F1/F2/F3/F8 浏览器 smoke**(关沙箱起 server + gstack browse 无头 Chromium):
· 加载 http://127.0.0.1:38080 → **.xterm 挂载、0 console error、WS 连上、彩色状态行 + 真 zsh 彩色提示**(F1/F2)。
· 聚焦 `.xterm-helper-textarea` 输入 `echo hello-webterm` → 回显 + 输出 `hello-webterm`(**输入→WS→PTY→shell→输出→xterm 全链路**)。截图 /tmp/webterm-smoke2.png。
· 输入 `exit` → 终端显示 "Process exited code=0 Press Enter to reconnect…"(F8)。
· 改 viewport(F3)→ 终端存活、无 console error。
· curl 复核:GET / 200、/build/main.js 200、坏 Origin upgrade → 401。
- **决策**: F4(局域网多设备)、F7(手机软键盘栏)需物理设备,无法在本机无头验证 → 交用户真机。其余 7/9 F 项已自动化验证。
- **遗留**: T19(F7)待真手机;F4 待局域网真机;main.ts 的 css `@ts-ignore` 小债。
- **commit**: `81585ad`(README/安全)+ 本条 LOG
### 2026-06-17 · W2/W3/W4 — 会话核心 + 集成(T12/T13/T14/T15)
- **状态**: `[x]` ALL DONE · 串行(各有依赖),单 builder/任务
- **T12 `session/session.ts`**(opus,19 测试):createSession/attachWs/detachWs/writeInput/resize/kill;onData→buffer+lastOutputAt+转发;onExit→exitedAt/exitCode+发exit+注入onExit;**spawn 失败抛(M4)**;sendIfOpen 守卫 readyState(M5);detach 不杀 PTY;exit 后 write/resize 忽略(L4);detach 后退出保留会话(L1)。测试 `vi.mock('node-pty')`→mock IPty,沙箱内可跑。
- **T13 `session/manager.ts`**(sonnet,21 测试):handleAttach 四路径(新建/存活/已退出 L1/查不到);reapIdle `now-max(detachedAt,lastOutputAt)>idleTtl`(M3);shutdown 全 kill;注入 onExit 在线退出删表、detach 后退出保留待 L1(L2)。
- **T14 `server.ts`**(sonnet,~200 行):Express 静态 public/+build/;`WebSocketServer({noServer,maxPayload})`(L5);upgrade 门(非 wsPath→destroy、坏 Origin→401、过→handleUpgrade,L3);connection 等 attach,try/catch→spawn 失败 exit(-1)+关连接(M4);message 路由;close→detach(不杀);SIGINT/TERM→shutdown;reapIdle 定时器;safeSend 守卫(M5)。导出 startServer 供 T15。
- **T15 集成**(sonnet,6 用例):真 ws 客户端打 startServer。①坏Origin 401 ②错路径 destroy ③超 maxPayload 1009 ④spawn失败 exit(-1) —— 沙箱内全绿;⑤attach→output ⑥重连回放(F5/F6,含 `__MARK_测试__` CJK)—— **orchestrator 加 `PTY_AVAILABLE` 门控(沙箱自动 skip,真机跑),关沙箱验证 6/6 全绿**
- **验证(orchestrator 独立复核)**: 每步 `tsc --noEmit` ✓ + 全量 `npm test` 递增至 **187 全绿**;集成 sandbox-off 6/6(真 PTY)。
- **决策 / 偏离**: T12 onData 时间戳用 Date.now()(事件时刻,符合 lastOutputAt 语义);T15 由 orchestrator 加 PTY 可用性门控(原 `it.skip``itPty`),使真机会跑、沙箱自动跳过。
- **commit**: `20212dd`(T12)·`a2897b2`(T13)·`0dac54b`(T14)·`cdec19a`(T15)
### 2026-06-16 · W1 前端批次(T8/T9/T10/T11)
- **状态**: `[x]` ALL DONE
- **派活方式**: 因 T11 import T10 的 mountKeybar 且验收是整目录 tsc.web,分两步:**先并行 T8+T9+T10(haiku),后 T11(sonnet)**。general-purpose 内联角色,共享树 + 文件不交叉。
- **改动**:
- **T8** `public/index.html`:#term/#keybar、viewport、`<script type=module src=./build/main.js>` + link ./build/main.css 与 ./style.css(按 §1 约定)。
- **T9** `public/style.css`:全屏终端、键栏 fixed 底部 flex、**>768px 隐藏**、深色;不碰 xterm 内部样式。
- **T10** `public/keybar.ts`(+ test):KEY_MAP 纯数据(Esc/Shift+Tab/方向/Enter=\r/Ctrl+C/Tab)+ mountKeybar(onSend) touchstart→onSend+preventDefault。13 测试。
- **T11** `public/main.ts`:esbuild 入口(import xterm css);xterm+FitAddon;WS scheme 随协议(M6);attach 首帧带 localStorage sessionId;onData→input、output→write、exit→ANSI 提示按回车重连;ResizeObserver 防抖 resize;指数退避重连≤30s;mountKeybar 接 ws.send;**无 console.log**(状态写终端)。
- **验证(orchestrator 独立复核)**: backend `tsc --noEmit` ✓;frontend `tsc -p tsconfig.web.json` ✓;`npm test` **6 文件 / 141 测试全绿**(新增 keybar 13);**`npm run build:web` esbuild 打包成功**(main.js 430kb + main.css 6kb);build/ 已 gitignore 未入库。
- **决策 / 偏离**: T11 用 1 处 `@ts-ignore` 压 CSS import(Owns 仅 main.ts,无法另建 d.ts;记为 tech-debt);exit 后按回车 clear sessionId 再新建(契合 L1:已退出会话由服务端回放+补发 exit,前端不 reattach)。
- **遗留**: 上述 css d.ts 小债;前端真实浏览器行为留 W5(T16T20)实测。
- **commit**: `326d347`
### 2026-06-16 · W1 后端批次(T3/T4/T5/T6/T7)— 5 个并行 builder
- **状态**: `[x]` ALL DONE
- **派活方式**: 5 个 `general-purpose` subagent 并行(自定义 module-builder agent 需重启会话才加载,故本会话内联角色),
共享工作树 + **文件所有权严格不交叉** + scoped `vitest run <name>`(官方文件分区法,替代 worktree;合并即 commit)。模型按 §4。
- **改动**(各 agent 仅在 Owns 内):
- **T3** `test/helpers/mock-pty.ts`(+ smoke test):IPty 替身,`emitData`/`emitExit` 手动触发、记录 write/resize/kill、onData/onExit 返回 IDisposable。
- **T4** `src/config.ts`(+ test):`loadConfig(EnvLike)`;M1 origins 由 `os.networkInterfaces()` 推导(含 localhost,**剔除 0.0.0.0/internal**,http+https),wsPath 默认 /term,非法值 fail-fast,`Object.freeze`
- **T5** `src/protocol.ts`(+ test):`SESSION_ID_RE`(UUID v4)、`parseClientMessage`(永不抛,全分支校验,M7 拒 `abc123`)、`serialize`
- **T6** `src/session/ring-buffer.ts`(+ test):M2 按真实字节(Buffer)计量、按 chunk 边界淘汰、不切碎 UTF-8/ANSI、snapshot 补 `\x1b[0m`
- **T7** `src/http/origin.ts`(+ test):用 WHATWG `URL` 比 protocol+host+port,undefined/解析失败拒绝。
- **验证(orchestrator 独立复核,非仅信 agent)**: `npx tsc -p tsconfig.json --noEmit` PASS;`npm test`(全量)
**5 文件 / 128 测试全绿**(T4 32 + T5 60 + T6 15 + T7 12 + T3 9)。
- **决策 / 偏离**: 无源码偏离。流程上:本批用 general-purpose 内联角色(见上)。T6 对"单块 > 容量"按 M2"绝不切碎"保留整块(测试固化),属忠实落地。
- **遗留 / 待办**: W1 前端 T8T11 待派;之后 T12 session(mock+ring-buffer 已就绪)。
- **commit**: `0fa0b69`
### 2026-06-16 · T2 冻结共享类型契约 src/types.ts
- **状态**: `[x]` DONE
- **改动**: 新建 `src/types.ts`(纯类型,无实现);删 `src/.gitkeep`。含 Config、ClientMessage/ServerMessage/
ParseResult、Dims、IPty(子集)+ IDisposable/PtyEvent、RingBuffer、WebSocketLike + WS_OPEN、SessionMeta/Session
(lastOutputAt/exitedAt/exitCode)、SessionManager、MountKeybar;各模块工厂函数签名以注释列为 import 锚点。
同步更新 ARCHITECTURE §3.1/§3.4(加 wsPath、EnvLike、WebSocketLike 注记,防漂移)。
- **验证**: `npx tsc -p tsconfig.json --noEmit` PASS;grep 确认无 `ws`/`NodeJS`/DOM 外部类型依赖(仅注释提及)。
- **决策 / 偏离 ARCHITECTURE §3**(为"前后端共享 → 必须无外部类型依赖"):
1. `loadConfig(env: EnvLike)` 替代 `NodeJS.ProcessEnv`(`EnvLike = Readonly<Record<string,string|undefined>>`,process.env 可赋值)。
2. `Session.attachedWs: WebSocketLike` 替代 `ws``WebSocket`(真实 ws 结构兼容);导出 `WS_OPEN=1` 供 M5 守卫。
3. **新增 `Config.wsPath`**(默认 '/term'):不变量 8 要求 WS 路径入 config,§3.1 原 Config 漏列,已补;T4 需从 env `WS_PATH` 读、T14 用 `cfg.wsPath`
三处均已回写 ARCHITECTURE,types.ts 为权威冻结源。
- **遗留 / 待办**: 无。W1 全部任务现可只读 import `src/types.ts`。**注意**:IPty/WebSocketLike 是结构子集,
实现方(T12/T14)可把真实 node-pty IPty / ws.WebSocket 直接赋值(结构兼容)。
- **commit**: (见下次提交)
### 2026-06-16 · T1 补记 — 前端打包定为 esbuild
- **状态**: `[x]` DONE
- **改动**: 装 `esbuild`(dev);`package.json``build:web`/`dev:web`(打包 `public/main.ts``public/build/main.js`);
`.gitignore` 改忽略 `public/build/`;`tsconfig.web.json` 注释更新为"仅类型检查";前端构建约定写入
`PLAN §1` 并更新 T8/T11/T14 与 `DISPATCH.md``ARCHITECTURE §5`
- **验证**: esbuild 0.28.1;临时入口 `--bundle --format=esm --outdir=public/build``main.js`+`.map`,smoke 通过。
- **决策**: 三选一中选 **esbuild**(极快、零配置、与 node-pty/tsx 生态一致);**现在装好**避免 W1 并发 install。
约定:入口 main.ts、产物 public/build/(已 gitignore)、index.html 用 `<script type=module src=./build/main.js>`、server 静态托管 public/。
- **commit**: (见下次提交)
### 2026-06-15 · T0 项目记录起点(计划就绪)
- **状态**: `[x]` DONE
- **改动**: 创建 `docs/PROGRESS_LOG.md``docs/PLAN.md`(细粒度 T1T21、为多 agent 并行设计);`CLAUDE.md` 加入 plan/log 工作流约束。
- **验证**: 文档自检;LOG 任务总览的 Steps 总数已与 PLAN 各任务勾选项对齐。
- **决策 / 偏离 PLAN**: 采用 TypeScript(ARCHITECTURE §0);两份设计文档已多团队交叉验证并修订 12 项;计划按依赖分 W0W5 波次,W1 含 9 个零交叉并行任务。
- **遗留 / 待办**: 进入 **W0 / T1**(脚手架 + 装齐依赖);T1 完成后 T2 冻结 `src/types.ts`,随后 W1 可大规模并行。
- **commit**: N/A