# 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、B1–B5、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 收尾 `[x]` DONE**: ① 功能阻塞已闭环 — `browser-server.ts` 现从 `term.dpop.` 子协议读 DPoP proof 喂 `UpgradeRequest.dpop`(头优先、否则回退子协议),真实浏览器可通过 DPoP 门。② F1 `/auth/mint` 加 Redis 按 IP 限流(HMAC 盐哈希 key,burst5/refill0.2,早于口令比对,耗尽 429);F2 `activeSessionCount` 接真实同租户在线 WS 计数;F5 错误日志只记 `e.message(+.code)` 防 DSN 泄漏。relay-run `npx vitest run` → **10 files / 92 passed**、tsc 干净。**F3/F4(撤销后 agent 空转重连、slowloris)为 LOW → Phase 2 backlog。** - **⚠️ 端到端仅可在 VPS 验证**:浏览器登录→token→DPoP→upgrade→agent 拼接,本地只到类型/单元层。E2–E5 远端执行见 `deploy/RUNBOOK.md`(需 VPS 访问权)。 - **远端(需 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` 会重建)。**阻塞**: 无。 ### ✅ NATIVE-TUNNEL — 原生 mTLS 反向隧道:四轨代码全部落地(2026-07-07,分支 `feat/relay-phase1`,commits 5337281/e38e6d1/bb09495/d0c249c) - **背景**: 原生 iOS/Android/桌面客户端讲 base-app 协议(非 E2E relay),需经用户 VPS 反向隧道(frp)回到本机 `127.0.0.1:3000`。**base app 无鉴权 → mTLS 客户端证书是唯一闸门**。总计划见 [PLAN_NATIVE_TUNNEL.md](./PLAN_NATIVE_TUNNEL.md)(Model A 单主人机队;LE 泛证书;frps 控制通道 mTLS+token;CRL 第一天上;`BIND_HOST=127.0.0.1` 强制)。 - **执行**: 两个后台 Workflow —— (1) build→review 四轨并行(8 agents,0 error,860k tok);(2) 修复两个 HIGH(2 agents,0 error,308k tok)。四轨文件互不相交(`ios/`·`desktop/`·`agent/`·`deploy/`),**零 `src/` 改动**(计划承诺兑现)。 - **各轨(全部已验证)**: - **C-iOS `[x]`**: 新 `ios/Packages/ClientTLS`(SecIdentity 包装、PKCS12 导入含类型化错误、keychain 存储 `AfterFirstUnlockThisDeviceOnly`、纯 `MutualTLSChallengeResponder` 真值表、跨平台 X.509 DER 摘要)。两个 transport + SessionThumbnail 改**惰性 `@Sendable ()->ClientIdentity?` provider**(WS 每连接、HTTP 每证书挑战解析 → 装证书后免重启生效)。`ClientCertScreen` 经 `hostMenu` 的「设备证书」入口可达;PairingViewModel 对 `*.terminal.yaojia.wang` 无证书时挡探测、mTLS 拒绝重映射为 clientCertRejected。**验证**: ClientTLS 14/14、SessionCore 93/93、xcodegen+xcodebuild BUILD SUCCEEDED。 - **C-Desktop `[x]`**(D-1/D-2): `remote-hosts.ts` + DesktopPrefs(不可变、向后兼容)+ 托盘选择器;`will-navigate` 动态锁定选中远端 origin、null 失败关闭(无开放重定向);`select-client-certificate` handler `event.preventDefault()` 优先、按 issuer CN 选证、缺证弹框不静默。**验证**: `npm run typecheck` 干净。 - **S2 host-packaging `[x]`**: launchd 写 ``、systemd 写 `EnvironmentFile`、originConfig zone 参数化(默认 `term` 保留);`InstallOptions` 从 CLI install → createCliDeps → installService seam → writers 全程贯通,生成单元携带 `BIND_HOST`(隧道机默认 `127.0.0.1`)/`ALLOWED_ORIGINS`/`PORT`/`SHELL_PATH`/`IDLE_TTL`/`USE_TMUX`;systemd 拒绝控制字符。**验证**: agent typecheck+build 干净、vitest **166/166**(154+12 新)。 - **V2/V7 VPS 脚本(可提交件)`[x]`**: `gen-device-ca.sh`(EC P-256,`--kind frp-client`)、`issue-device-cert.sh`(P-256 leaf,EKU=clientAuth only,.p12 `-legacy`+.pem,防 CSR 注入 serverAuth/CA:TRUE)、`revoke-device.sh`(CRL 强制)、`frp/{frps,frpc}` 模板(`disableCustomTLSFirstByte=true`)、`nginx/frp-mtls.conf`(:8470 TLS 终止 + `ssl_verify_client on` + `ssl_crl`,WS 升级代理 →:7080)、`nginx/stream-sni-additions.md`(两条加性 SNI map,合并式共存)。**验证**: tmp 目录跑通 gen→issue→`openssl verify`→revoke→CRL(OpenSSL 3.0.18);无密钥/证书误入库。 - **对抗式评审结论**: Desktop/VPS 直接 PASS;iOS/S2 各命中 1 个 HIGH(ClientCertScreen 无入口 / 环境注入未接到 install CLI)—— 均属跨 lane 收尾接线,已由第二个 Workflow 补齐并复验。 - **实机 VPS 部署 V0→V6 完成,M1 达成 `[x]`(2026-07-07,8.138.1.192)**: frps 0.61.1(systemd `frps.service`,控制 :7000 + vhost :7080,控制通道 mTLS+token);P-256 device CA + frp-client CA(openssl ca db + CRL);nginx `:8470`(`ssl_verify_client on` + device-CA + `ssl_crl` → WS 代理 :7080);stream map **加性合并** `frp.terminal→7000`/`*.terminal→8470`(保留 `*.term→8443`/`writer→8444`/`default→10443`,`.bak` 已存)。**M1 全绿**:无证书→400 拒;设备证书→200 `/live-sessions` JSON(全链路 :443→SNI→:8470 mTLS→frps→frpc→base app);frpc 无 frp-client 证书→控制层拒(`error: EOF`);吊销设备证书→400、另一证书仍 200(CRL,`nginx -s reload` 后即时);4-zone SNI oracle 共存(xray/writer 未动)。base app **零改动**。详见 memory `native-tunnel-m1-live`。 - **唯一剩余阻塞 → 真机客户端(M2)**: 服务器证书目前是**临时自签 `*.terminal`(90d)**,iOS ATS/Android/Chromium 会拒。须换 **Let's Encrypt 泛证书** `acme.sh --dns dns_ali`——需用户 `Ali_Key`/`Ali_Secret`(acme.sh 已装但无凭证)。换证=重签→覆盖 `/etc/relay/frp-tls/{fullchain,privkey}.pem`→`nginx -s reload`,零返工。 - **旁注(非本次改动)**: E2E 浏览器 relay `:8443` 在部署前已 DOWN(只有 :8444 writer 在),故 `terminal.term` 无证书;`*.term→8443` 路由从未被动过。 ### ✅ 修复:底部快捷键栏遮挡终端内容(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/lg16、Motion gated by reduceMotion、Haptics)、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 决策点(regular→split/compact→stack,仿 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 防 42→42.0)、公开四层 HostNetworkTier(W3 dedup 欠账,PairingVM 后续 pass 可切换)。76/76 绿,覆盖 95.26%。 - 遗留(LOW/cosmetic): Swift doc comment 里 server 行号引用漂移;PairingVM 分类去重待后续 pass。 - **[x] P1-B UI 九连链(22→24→25→27→26→23→29→28→21 全 DONE,10 agents;verify 判 BLOCKED→orchestrator 修复后放行)**: - **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 50→reverse);`[]`=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 是 §3.3 HostRegistry 类型,而 APIClient 依赖边仅 WireProtocol(§1 零耦合),签名无法编译(两轮 review 均漏)。**裁定(orchestrator,三选一取 c)**: 探针职责=验证 endpoint → 返回 `Result`,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:/')` 复用**前端零改动**(前端严格同源,`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=` 加入指定会话。 - ✅ **镜像尺寸 = 最近活跃端优先(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://: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) > **倒序**,最新在最上。每个子任务一条。复制下方模板填写。 ### 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`(读 `/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 `(文件在主机,故在主机开;不分查看设备)。主机已确认装有 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_`;node-pty 进程只是 tmux **客户端**。服务重启后,客户端带同 sessionId 重连 → manager 发现 `web_` 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、`