T-iOS-18 (F-iOS-1..13 walkthrough) + T-iOS-19 (security audit vs built artifacts): both PASS_WITH_FINDINGS, 0 CRITICAL/HIGH; real-device items DEFERRED with manual checklists. Fixes (1 MED + 3 LOW, all closed): - WebTermUITests: the single scripted happy path (manual-entry pair → list → attach → KeyBar ^L → injected held gate via POST /hook/permission → tap Approve → server-side behavior==allow assertion). Green twice locally (fresh + already-paired branches). - ios.yml: ui-test leg (server boot + TEST_RUNNER_ env as PROCESS env) + iOS17-floor leg (loud-skip if runtime absent, hard-fail if present and red); dropped CODE_SIGNING_ALLOWED=NO from app-tests leg (unsigned hosts get -34018 from the real keychain — measured) - URLSessionHTTPTransport defaults to .ephemeral (no disk cache of preview terminal bytes) - TARGETED_DEVICE_FAMILY moved to target level (built plist now UIDeviceFamily [1] only) Final: 306 automated checks green (214 pkg + 76 app + 10 integration + XCUITest); server src/ untouched; P0 complete (19/19 tasks)
584 lines
93 KiB
Markdown
584 lines
93 KiB
Markdown
# 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)
|
||
|
||
> 新会话读到的第一块。保持准确,只描述"此刻"。
|
||
|
||
### ✅ 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-11→12→13→14] 串行链 ∥ 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 任务 W0–W5 ≈13 人日、P1 W6–W8 ≈17.5、P2 W9–W10 ≈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.95–0.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 D5–D7)**: `.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 任务完成并验证**(P1–P6,多 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 全部完成**(H1–H4 + 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**: **W0–W5 基本完成**。v0.1 功能完整、测试与真机浏览器验证通过。仅剩 2 项需用户真设备。
|
||
- **进度**: **20/21 任务**(T19 待真手机)· **187 自动化测试全绿** · 真 PTY 端到端验证 · **真浏览器 smoke 通过**。
|
||
- **验收 F1–F9**: ✅ 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 | 验收 F1–F3(终端/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 页扁平网格太乱(50–100 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` 续跑 W1–W4。
|
||
- **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(P1–P6,多 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`)。**至此 H1–H4 全部高价值功能完成。**
|
||
- **机制**: 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 — 验收 + 收尾(T16–T21)
|
||
|
||
- **状态**: 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(T16–T20)实测。
|
||
- **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 前端 T8–T11 待派;之后 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`(细粒度 T1–T21、为多 agent 并行设计);`CLAUDE.md` 加入 plan/log 工作流约束。
|
||
- **验证**: 文档自检;LOG 任务总览的 Steps 总数已与 PLAN 各任务勾选项对齐。
|
||
- **决策 / 偏离 PLAN**: 采用 TypeScript(ARCHITECTURE §0);两份设计文档已多团队交叉验证并修订 12 项;计划按依赖分 W0–W5 波次,W1 含 9 个零交叉并行任务。
|
||
- **遗留 / 待办**: 进入 **W0 / T1**(脚手架 + 装齐依赖);T1 完成后 T2 冻结 `src/types.ts`,随后 W1 可大规模并行。
|
||
- **commit**: N/A
|