lark-remote 0.1.13 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (132) hide show
  1. package/CHANGELOG.md +36 -3
  2. package/README.en.md +7 -11
  3. package/README.md +7 -11
  4. package/dist/bridge/final-usage.d.ts +24 -0
  5. package/dist/bridge/final-usage.js +55 -0
  6. package/dist/bridge/index.d.ts +0 -7
  7. package/dist/bridge/index.js +22 -64
  8. package/dist/bridge/queue-manager.js +4 -3
  9. package/dist/card/approval-render.js +39 -94
  10. package/dist/card/bash-renderer.js +12 -21
  11. package/dist/card/card-shared.d.ts +13 -1
  12. package/dist/card/card-shared.js +21 -30
  13. package/dist/card/run-renderer.d.ts +0 -22
  14. package/dist/card/run-renderer.js +206 -153
  15. package/dist/card/run-state.d.ts +29 -1
  16. package/dist/card/run-state.js +172 -84
  17. package/dist/card/text-truncate.d.ts +0 -9
  18. package/dist/card/text-truncate.js +0 -9
  19. package/dist/card/tool-render.d.ts +4 -1
  20. package/dist/card/tool-render.js +76 -32
  21. package/dist/cli.js +2 -2
  22. package/dist/common/fs.d.ts +14 -0
  23. package/dist/common/fs.js +57 -3
  24. package/dist/config/codex-config.js +2 -0
  25. package/dist/config/index.d.ts +0 -5
  26. package/dist/config/index.js +0 -11
  27. package/dist/config/kimi-config.js +1 -0
  28. package/dist/config/opencode-config.js +13 -2
  29. package/dist/config/qr.d.ts +36 -0
  30. package/dist/config/qr.js +124 -0
  31. package/dist/config/wizard.js +69 -27
  32. package/dist/connector/index.d.ts +3 -2
  33. package/dist/connector/index.js +5 -4
  34. package/dist/index.js +15 -42
  35. package/dist/instance-lock.js +7 -0
  36. package/dist/persistence/atomic-write.d.ts +2 -0
  37. package/dist/persistence/atomic-write.js +20 -5
  38. package/dist/persistence/load-json-file.d.ts +1 -2
  39. package/dist/persistence/load-json-file.js +2 -3
  40. package/dist/platform/agent-stopper.d.ts +22 -0
  41. package/dist/platform/agent-stopper.js +19 -0
  42. package/dist/platform/command.d.ts +11 -0
  43. package/dist/platform/command.js +107 -0
  44. package/dist/platform/identity.d.ts +33 -0
  45. package/dist/platform/identity.js +187 -0
  46. package/dist/platform/path.d.ts +24 -0
  47. package/dist/platform/path.js +110 -0
  48. package/dist/platform/select.d.ts +9 -0
  49. package/dist/platform/select.js +12 -0
  50. package/dist/platform/shell.d.ts +51 -0
  51. package/dist/platform/shell.js +75 -0
  52. package/dist/platform/spawn.d.ts +41 -0
  53. package/dist/platform/spawn.js +77 -0
  54. package/dist/platform/terminator-posix.d.ts +38 -0
  55. package/dist/platform/terminator-posix.js +103 -0
  56. package/dist/platform/terminator-win32.d.ts +21 -0
  57. package/dist/platform/terminator-win32.js +110 -0
  58. package/dist/platform/terminator.d.ts +44 -0
  59. package/dist/platform/terminator.js +24 -0
  60. package/dist/platform/types.d.ts +23 -0
  61. package/dist/platform/types.js +5 -0
  62. package/dist/restart.js +4 -2
  63. package/dist/router/card-helpers.d.ts +33 -6
  64. package/dist/router/card-helpers.js +67 -11
  65. package/dist/router/config/common/render.d.ts +1 -1
  66. package/dist/router/config/common/render.js +1 -1
  67. package/dist/router/config-diff.d.ts +4 -0
  68. package/dist/router/config-diff.js +70 -0
  69. package/dist/router/index.d.ts +66 -6
  70. package/dist/router/index.js +451 -316
  71. package/dist/router/utils.d.ts +6 -0
  72. package/dist/router/utils.js +12 -0
  73. package/dist/runner/bash/runner.d.ts +9 -2
  74. package/dist/runner/bash/runner.js +45 -20
  75. package/dist/runner/claude/session.d.ts +4 -1
  76. package/dist/runner/claude/session.js +18 -4
  77. package/dist/runner/codex/app-server/protocol-types.d.ts +35 -1
  78. package/dist/runner/codex/app-server/translator.d.ts +18 -0
  79. package/dist/runner/codex/app-server/translator.js +146 -0
  80. package/dist/runner/common/acp/base-acp-runner.d.ts +116 -0
  81. package/dist/runner/common/acp/base-acp-runner.js +248 -0
  82. package/dist/runner/common/acp/base-translator.d.ts +141 -0
  83. package/dist/runner/common/acp/base-translator.js +271 -0
  84. package/dist/runner/common/acp/protocol-helpers.d.ts +39 -0
  85. package/dist/runner/common/acp/protocol-helpers.js +34 -0
  86. package/dist/runner/common/jsonrpc/transport.d.ts +2 -0
  87. package/dist/runner/common/jsonrpc/transport.js +25 -5
  88. package/dist/runner/common/process-stopper.d.ts +4 -26
  89. package/dist/runner/common/process-stopper.js +4 -76
  90. package/dist/runner/common/runner-utils.d.ts +0 -5
  91. package/dist/runner/common/runner-utils.js +0 -7
  92. package/dist/runner/common/spawning-runner.d.ts +42 -132
  93. package/dist/runner/common/spawning-runner.js +52 -271
  94. package/dist/runner/common/usage.d.ts +2 -0
  95. package/dist/runner/common/usage.js +2 -0
  96. package/dist/runner/index.d.ts +1 -1
  97. package/dist/runner/kimi/acp/runner.d.ts +45 -42
  98. package/dist/runner/kimi/acp/runner.js +140 -293
  99. package/dist/runner/kimi/acp/translator.d.ts +18 -100
  100. package/dist/runner/kimi/acp/translator.js +56 -242
  101. package/dist/runner/opencode/acp/runner.d.ts +22 -46
  102. package/dist/runner/opencode/acp/runner.js +73 -256
  103. package/dist/runner/opencode/acp/translator.d.ts +28 -103
  104. package/dist/runner/opencode/acp/translator.js +51 -236
  105. package/dist/runner/pi/rpc/translator.d.ts +3 -0
  106. package/dist/runner/pi/rpc/translator.js +47 -9
  107. package/dist/runner/probe.d.ts +8 -11
  108. package/dist/runner/probe.js +17 -21
  109. package/dist/runner/registry.d.ts +4 -4
  110. package/dist/runner/registry.js +4 -4
  111. package/dist/runner/types.d.ts +38 -1
  112. package/dist/session/claude/sessions.js +35 -58
  113. package/dist/session/codex/rollout-reader.d.ts +10 -0
  114. package/dist/session/codex/rollout-reader.js +123 -41
  115. package/dist/session/common/constants.d.ts +7 -0
  116. package/dist/session/common/constants.js +9 -0
  117. package/dist/session/common/two-pass.d.ts +22 -0
  118. package/dist/session/common/two-pass.js +70 -0
  119. package/dist/session/dsh/sessions.js +7 -6
  120. package/dist/session/kimi/sessions.d.ts +6 -0
  121. package/dist/session/kimi/sessions.js +50 -73
  122. package/dist/session/opencode/sessions.js +26 -8
  123. package/dist/session/pi/sessions.js +54 -73
  124. package/dist/update/index.d.ts +1 -2
  125. package/dist/update/index.js +1 -2
  126. package/dist/update/install.d.ts +2 -1
  127. package/dist/update/install.js +58 -9
  128. package/dist/update/version-check.d.ts +5 -0
  129. package/dist/update/version-check.js +9 -0
  130. package/package.json +9 -6
  131. package/dist/update/startup-hint.d.ts +0 -5
  132. package/dist/update/startup-hint.js +0 -11
package/CHANGELOG.md CHANGED
@@ -5,6 +5,41 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.2.1] - 2026-09-11
9
+
10
+ ### 新增
11
+
12
+ - **Windows 正式支持**:原生 Windows 支持已在真机完成验证,README / getting-started 的平台前置条件从「macOS / Linux,暂不支持 Windows」更新为 macOS / Linux / Windows 10+
13
+
14
+ ### 变更
15
+
16
+ - **安全文档措辞更新**:README 安全警告与 `SECURITY.md` 反映 Claude 审批模式——`permissionMode` 可配置(默认 `bypassPermissions`,配置为其他值即启用飞书卡片交互式审批),不再表述为「固定 bypassPermissions」;审批卡片描述从 Codex 专属改为 Claude / Codex / Kimi 通用
17
+
18
+ ## [0.2.0] - 2026-09-11
19
+
20
+ ### 新增
21
+
22
+ - **Windows 平台支持(预发布)**:新增跨平台平台层(`src/platform/`)——路径、可执行文件发现、进程终止、spawn、shell 的抽象与 posix / win32 双实现;命令拉起统一走 `cross-spawn`,posix 行为零变化。Windows 尚未正式发布,README 前置条件已相应标注
23
+ - **`/download <path>`(别名 `/d`)**:把本地文件直接发到会话,不经 `/ls` 卡片。路径规则同 `/ls`(`~` 展开、相对 cwd、30MB 上限),目录 / 不存在 / 超限给出明确错误
24
+ - **`/ls <file>` 单文件卡**:传入文件路径渲染单文件卡(完整路径、大小、修改时间、「📎 下载」+「上级」按钮),不再报 `ls: xxx: Not a directory`
25
+ - **分页栏页码跳转**:所有分页卡片(`/ls`、`/ws`、`/resume`、`/active`、`/order`)支持在分页栏输入页码直达;越界钳到末页,非数字输入报错且不刷新卡片
26
+ - **扫码向导跨平台**:二维码渲染改用 `qrcode-generator`,Windows 全块渲染 + 静区 + 图片兜底
27
+
28
+ ### 修复
29
+
30
+ - `/ls` 的「返回」改为回到浏览起点(`/ls <dir>` 指定的目录),不再跳回 workspace cwd;浏览起点随卡片 payload 传递,翻页 / 刷新 / 上级 / 目录按钮不会丢失
31
+ - codex 会话兼容 rollout ≥0.153.4 的 `item_completed` 词表,修复 `/resume` 标题丢失
32
+ - compact 卡片空内容渲染「Compact 完成」,不再显示通用空占位
33
+ - 信息保真:notices 改为时间线消息渲染、省略提示固定置顶、工具卡片消除孤立代理项
34
+ - Windows:隐藏 cmd.exe 控制台窗口(`windowsHide`)
35
+
36
+ ### 变更
37
+
38
+ - **移除 `output.showThinking` / `showToolUse` / `showToolResult` 配置项与 `/config` 的「📤 输出」页签**:thinking、工具调用与工具结果恒常展示;旧配置文件的 `output` 段在加载时自动忽略,并在下次保存时从磁盘清理
39
+ - 代码精简:ACP / translator / session 共享层下沉(`BaseAcpTranslator` / `BaseAcpRunner` / two-pass 扫描),死代码清理与平行重复收敛
40
+ - 依赖:`qrcode-terminal` 换为 `qrcode-generator`;新增 `cross-spawn`(Windows 命令拉起)
41
+ - 测试基建:新增 scoped typecheck(`tsconfig.test-infra.json`)覆盖 `tests/lib/**` 共享工厂;跨平台测试稳定化(win32 路径归一化、并行 vitest 内存上限)
42
+
8
43
  ## [0.1.13] - 2026-09-04
9
44
 
10
45
  ### 修复
@@ -37,7 +72,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
37
72
  ### 变更
38
73
 
39
74
  - kimi ACP 能力声明:`fs` 保持关闭(走本地磁盘兜底,避免未实现的 `fs/*` RPC 报 `METHOD_NOT_FOUND`),`terminal` 打开
40
- - 文档清理:移除设计文档中对内部文档名(`lessons-redlines`)与本地仓库路径(`/Users/.../lark-remote`)的引用
41
75
 
42
76
  ## [0.1.10] - 2026-08-22
43
77
 
@@ -187,7 +221,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
187
221
  - `displayTitle`/`summary` 跳过 task-notification 注入
188
222
  - 修复 `scanClaudeSessions` 对迁移到其他目录的会话的遗漏(S2)
189
223
  - 修复 probe 生命周期、工厂回退、卡片回调等评审发现的问题(F1-F8)
190
- - 清理引用已删除文档(lessons-redlines、resume-card-budget-failure-analysis)导致的失效链接
191
224
 
192
225
  ### 变更
193
226
 
@@ -196,7 +229,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
196
229
  - 移除 `pnpm-lock.yaml`,统一使用 `bun.lock` 管理依赖
197
230
  - 新增 husky lint-staged pre-commit / pre-push 钩子(提交前 lint + typecheck)
198
231
  - 启用 npm provenance(构建产物可溯源)
199
- - 移除 watchdog 相关用户文档及 `open-source-playbook` 等临时文档
200
232
 
201
233
  ### 测试
202
234
 
@@ -275,3 +307,4 @@ Initial release.
275
307
  [0.1.6]: https://github.com/bungabungawoda/lark-remote/releases/tag/v0.1.6
276
308
  [0.1.7]: https://github.com/bungabungawoda/lark-remote/releases/tag/v0.1.7
277
309
  [0.1.8]: https://github.com/bungabungawoda/lark-remote/releases/tag/v0.1.8
310
+ [0.2.1]: https://github.com/bungabungawoda/lark-remote/releases/tag/v0.2.1
package/README.en.md CHANGED
@@ -16,20 +16,20 @@ Designed for a single-user, peer-to-peer private chat. The agent's system prompt
16
16
 
17
17
  ## ⚠️ Security Warning (read first)
18
18
 
19
- This tool turns Feishu messages directly into local agent executions. Claude runs with **full permissions** by default (fixed `bypassPermissions`); Codex uses the **app-server approval mode** (default `approvalPolicy=on-request` — command execution requires your confirmation on the Feishu card, sandbox defaults to `workspace-write`), which is relatively safe; other agents run with their CLI defaults. Agents can still read and write the local directories you specify. You MUST:
19
+ This tool turns Feishu messages directly into local agent executions. **Out of the box, agents run without approval prompts**: Claude defaults to `bypassPermissions` (no approval card), but that permission mode is **configurable** — set it to anything other than `bypassPermissions` (`default` / `acceptEdits` / `auto` / `dontAsk` / `plan`) to enable interactive approval, where high-risk actions wait for your confirmation on the Feishu card. Codex uses the **app-server approval mode** (default `approvalPolicy=on-request` — command execution requires your confirmation on the Feishu card, sandbox defaults to `workspace-write`); Kimi defaults to `manual` (per-action approval); other agents run with their CLI defaults. **In any mode**, agents can read and write the local directories you specify. You MUST:
20
20
 
21
21
  - **Use it only in your own private (p2p) chat.** Never add the bot to any group chat; never let anyone else talk to it.
22
22
  - Restrict the app's visibility to **yourself only** in the Feishu Open Platform.
23
23
  - Don't run it on a machine holding data you can't afford to lose; prefer a dedicated user / machine / container.
24
24
  - Keep credentials such as `appSecret` in your local `config.yaml` only — **never commit them anywhere**.
25
25
 
26
- In Codex approval mode, an approval card arrives before any command runs — approve or reject it right in Feishu:
26
+ In approval mode (Claude, Codex, Kimi), an approval card arrives before a command runs — approve or reject it right in Feishu:
27
27
 
28
- ![Codex command approval card example](docs/images/approval-card.png)
28
+ ![Command approval card example](docs/images/approval-card.png)
29
29
 
30
30
  ## Prerequisites
31
31
 
32
- - OS: macOS / Linux. **Windows is not supported yet** (relies on POSIX behaviors such as Unix signals, bash, and file locks)
32
+ - OS: macOS / Linux / Windows 10+ (native Windows support is verified on real hardware, with behavior matching macOS/Linux)
33
33
  - Node.js 20+ ([Bun](https://bun.sh/) for development)
34
34
  - A Feishu custom app (either way): scan-to-create (a QR code pops up in the terminal on first launch; scan it with the Feishu app to create the app and write credentials automatically); or create one manually in the Open Platform — enable bot capability, subscribe to `im.message.receive_v1` and `card.action.trigger` via **long connection** (WebSocket, no public address needed), with at least the `im:message` scope.
35
35
  - Claude Code CLI installed locally and logged in once in a terminal (`claude` → browser OAuth). Same idea for other agents (codex / opencode / pi / kimi): install the corresponding CLI first. DSH connects to a local DSH Web Host via HTTP+WebSocket (no local subprocess), default address `http://127.0.0.1:3080`.
@@ -70,14 +70,9 @@ defaultAgent: claude
70
70
  claude:
71
71
  model: claude-opus-4-8
72
72
  effort: medium # low | medium | high | xhigh | max
73
- # permissionMode is hardcoded to bypassPermissions (inside the runner)
73
+ permissionMode: bypassPermissions # Claude official --permission-mode: default | acceptEdits | auto | bypassPermissions | manual | dontAsk | plan (switchable via /config card; anything other than bypassPermissions enables approval)
74
74
  stopGraceMs: 5000
75
75
 
76
- output:
77
- showThinking: true
78
- showToolUse: true
79
- showToolResult: true
80
-
81
76
  logging:
82
77
  level: info # debug | info | warn | error
83
78
 
@@ -113,7 +108,8 @@ Your original message also gets an emoji reaction: `Typing` while the run is in
113
108
  |---------|-------|----------|
114
109
  | `/help` | `/h` | Command list |
115
110
  | `/cd <path>` | - | Switch agent working directory (`~`, absolute/relative; clears the session) |
116
- | `/ls [dir]` | - | Directory/file card; click a directory to switch, click a file ≤30MB to send it to Feishu; paginates beyond 30 entries |
111
+ | `/ls [dir\|file]` | - | Directory/file card; click a directory to browse, click a file ≤30MB to send it to Feishu; paginates beyond 30 entries with a page-number jump input. Passing a file path lists that file itself |
112
+ | `/download <path>` | `/d` | Send a local file straight to the chat (`~`, absolute/relative paths, 30MB cap) |
117
113
  | `/ws save\|use\|remove` | - | Named directory aliases (`/ws` lists by default) |
118
114
  | `/resume [agent] [N\|id]` | `/r` | List/switch agent sessions for the current directory (card) |
119
115
  | `/active` | - | List all running sessions |
package/README.md CHANGED
@@ -16,20 +16,20 @@
16
16
 
17
17
  ## ⚠️ 安全警告(必读)
18
18
 
19
- 本工具会把飞书消息直接转为本地 agent 执行。Claude 默认以**完全权限**运行(固定 `bypassPermissions`);Codex 采用 **app-server 审批模式**(默认 `approvalPolicy=on-request`,执行命令需在飞书卡片上确认,沙箱默认 `workspace-write`),相对安全;其他 agent 以各自 CLI 的默认权限运行。agent 仍可读写你指定的本地目录,请务必:
19
+ 本工具会把飞书消息直接转为本地 agent 执行。**开箱默认是不弹审批的**:Claude 默认以 `bypassPermissions` 运行(无审批卡),但该权限模式**可配置**——把它改成 `bypassPermissions` 以外的值(`default` / `acceptEdits` / `auto` / `dontAsk` / `plan`)即启用交互式审批,高风险操作会在飞书卡片上等你确认;Codex 采用 **app-server 审批模式**(默认 `approvalPolicy=on-request`,执行命令需在飞书卡片上确认,沙箱默认 `workspace-write`);Kimi 默认 `manual`(逐条审批);其他 agent 以各自 CLI 的默认权限运行。**无论哪种模式**,agent 都能读写你指定的本地目录,请务必:
20
20
 
21
21
  - **仅限你自己的私聊(p2p)使用**。不要把机器人拉进任何群聊,不要让任何其他人能与它对话。
22
22
  - 在飞书开放平台把应用可见范围限制到**只有你自己**。
23
23
  - 运行本工具的机器上不要存放与损失承受力不符的数据;建议在独立用户/机器/容器里运行。
24
24
  - `appSecret` 等凭据只在本地 `config.yaml`,**不要提交到任何仓库**。
25
25
 
26
- Codex 审批模式下,执行命令前会收到审批卡片,可直接在飞书里允许或拒绝:
26
+ 审批模式下(Claude、Codex、Kimi),执行命令前会收到审批卡片,可直接在飞书里允许或拒绝:
27
27
 
28
- ![Codex 命令审批卡片示例](docs/images/approval-card.png)
28
+ ![命令审批卡片示例](docs/images/approval-card.png)
29
29
 
30
30
  ## 前置条件
31
31
 
32
- - 操作系统:macOS / Linux。**暂不支持 Windows**(依赖 Unix 信号、bash、文件锁等 POSIX 行为)
32
+ - 操作系统:macOS / Linux / Windows 10+(原生 Windows 支持已完成真机验证,行为与 macOS/Linux 对齐)
33
33
  - Node.js 20+(开发时用 [Bun](https://bun.sh/))
34
34
  - 飞书自建应用(二选一):扫码创建(首次启动终端弹二维码,飞书 App 扫码即自动创建并写入凭据);或手动在开放平台建应用——开启机器人能力,订阅 `im.message.receive_v1` 和 `card.action.trigger`,订阅方式选「长连接」(WebSocket,无需公网地址),权限至少 `im:message`。
35
35
  - Claude Code CLI:本地安装并在终端完成一次登录(`claude` → 浏览器 OAuth)。使用其他 agent(codex / opencode / pi / kimi)同理,先装好对应 CLI;DSH 需本地 DSH Web Host 在跑(默认 `http://127.0.0.1:3080`),lark-remote 通过 HTTP+WebSocket 直连、不 spawn 本地子进程。
@@ -70,14 +70,9 @@ defaultAgent: claude
70
70
  claude:
71
71
  model: claude-opus-4-8
72
72
  effort: medium # low | medium | high | xhigh | max
73
- # permissionMode 硬编码为 bypassPermissions(runner 内部)
73
+ permissionMode: bypassPermissions # Claude 官方 --permission-mode:default | acceptEdits | auto | bypassPermissions | manual | dontAsk | plan(`/config` 卡片可切换;非 bypassPermissions 会启用审批)
74
74
  stopGraceMs: 5000
75
75
 
76
- output:
77
- showThinking: true
78
- showToolUse: true
79
- showToolResult: true
80
-
81
76
  logging:
82
77
  level: info # debug | info | warn | error
83
78
 
@@ -113,7 +108,8 @@ bridge 启动后不在终端输出,运行日志写入 `~/.lark-remote/logs/YYY
113
108
  |------|------|------|
114
109
  | `/help` | `/h` | 命令列表 |
115
110
  | `/cd <path>` | - | 切换 agent 工作目录(支持 `~`、绝对/相对路径,清空会话) |
116
- | `/ls [dir]` | - | 弹出目录/文件卡片;点击目录切换,点击 30MB 内文件发送到飞书;条目 >30 时支持翻页 |
111
+ | `/ls [dir\|file]` | - | 弹出目录/文件卡片;点击目录切换,点击 30MB 内文件发送到飞书;条目 >30 时支持翻页与直接输入页码跳转。传文件路径时直接列出该文件(可下载) |
112
+ | `/download <path>` | `/d` | 直接下载(发送)指定文件到飞书(支持 `~`、绝对/相对路径,上限 30MB) |
117
113
  | `/ws save\|use\|remove` | - | 命名目录别名管理(`/ws` 默认列出) |
118
114
  | `/resume [agent] [N\|id]` | `/r` | 列出/切换当前目录的 agent session(卡片) |
119
115
  | `/active` | - | 列出所有正在进行中的 session |
@@ -0,0 +1,24 @@
1
+ import type { SessionReaderRegistry } from '../session/index.js';
2
+ import type { AgentKind } from '../runner/index.js';
3
+ export declare function errorMessage(error: unknown): string;
4
+ export type FinalUsageSnapshot = {
5
+ contextLength?: number;
6
+ contextLimit?: number;
7
+ compactCount?: number;
8
+ compactPreContextLength?: number;
9
+ cacheReadTokens?: number;
10
+ cacheCreationTokens?: number;
11
+ totalTokens?: number;
12
+ inputTokens?: number;
13
+ outputTokens?: number;
14
+ cumulativeTotalTokens?: number;
15
+ cumulativeInputTokens?: number;
16
+ cumulativeOutputTokens?: number;
17
+ cumulativeCacheReadTokens?: number;
18
+ cumulativeCacheCreationTokens?: number;
19
+ };
20
+ /**
21
+ * Read final usage (contextLength + compactCount + cache tokens) from the
22
+ * session jsonl. Called after a run finishes.
23
+ */
24
+ export declare function resolveFinalUsage(registry: SessionReaderRegistry, defaultAgent: AgentKind, sessionId: string | undefined, cwd: string, agentKind?: AgentKind): FinalUsageSnapshot | undefined;
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Final usage resolution extracted from Bridge (2026-09 round-2
3
+ * simplification): after a run finishes, re-read the session transcript via
4
+ * the agent's session reader — live stream-json does not emit
5
+ * compact_boundary, so the live contextLength (fallback to result.usage
6
+ * input+output) is unreliable. The transcript jsonl is authoritative for
7
+ * postTokens + compact event count.
8
+ *
9
+ * Bridge state is passed in explicitly (registry + default agent); the
10
+ * function itself is stateless.
11
+ */
12
+ import { getLogger } from '../logger/index.js';
13
+ export function errorMessage(error) {
14
+ return error instanceof Error ? error.message : String(error);
15
+ }
16
+ /**
17
+ * Read final usage (contextLength + compactCount + cache tokens) from the
18
+ * session jsonl. Called after a run finishes.
19
+ */
20
+ export function resolveFinalUsage(registry, defaultAgent, sessionId, cwd, agentKind = defaultAgent) {
21
+ if (!sessionId)
22
+ return undefined;
23
+ try {
24
+ const content = registry.get(agentKind).readSessionContent(sessionId, cwd);
25
+ if (!content.usage) {
26
+ // EnterWorktree relocate (2026-08-04): jsonl read silently returned no
27
+ // usage (e.g. transcript moved mid-session). Surface it so token-stat
28
+ // fallback to per-run live usage is visible in logs, not silent.
29
+ getLogger().warn(`[bridge] resolveFinalUsage: no usage from jsonl sessionId=${sessionId} cwd=${cwd} agent=${agentKind}, card falls back to per-run live usage`);
30
+ }
31
+ return content.usage
32
+ ? {
33
+ contextLength: content.usage.contextLength,
34
+ contextLimit: content.usage.contextLimit,
35
+ compactCount: content.usage.compactCount,
36
+ compactPreContextLength: content.usage.compactPreContextLength,
37
+ cacheReadTokens: content.usage.cacheReadTokens,
38
+ cacheCreationTokens: content.usage.cacheCreationTokens,
39
+ totalTokens: content.usage.totalTokens,
40
+ inputTokens: content.usage.inputTokens,
41
+ outputTokens: content.usage.outputTokens,
42
+ cumulativeTotalTokens: content.usage.cumulativeTotalTokens,
43
+ cumulativeInputTokens: content.usage.cumulativeInputTokens,
44
+ cumulativeOutputTokens: content.usage.cumulativeOutputTokens,
45
+ cumulativeCacheReadTokens: content.usage.cumulativeCacheReadTokens,
46
+ cumulativeCacheCreationTokens: content.usage.cumulativeCacheCreationTokens,
47
+ }
48
+ : undefined;
49
+ }
50
+ catch (err) {
51
+ getLogger().warn(`[bridge] resolveFinalUsage failed sessionId=${sessionId}: ${errorMessage(err)}`);
52
+ return undefined;
53
+ }
54
+ }
55
+ //# sourceMappingURL=final-usage.js.map
@@ -361,13 +361,6 @@ export declare class Bridge {
361
361
  * Handles fallback card sending, completion notification, and cleanup.
362
362
  */
363
363
  private finalizeRun;
364
- /**
365
- * Read final usage (contextLength + compactCount + cache tokens) from the session jsonl.
366
- * Called after a run finishes: live stream-json does not emit compact_boundary,
367
- * so the live contextLength (fallback to result.usage input+output) is
368
- * unreliable. The jsonl is authoritative for postTokens + compact event count.
369
- */
370
- private resolveFinalUsage;
371
364
  /**
372
365
  * Send a notification card when a run completes.
373
366
  * This card contains the session summary and a button to resume the session.
@@ -1,5 +1,6 @@
1
1
  import { randomUUID } from 'node:crypto';
2
2
  import { sleep } from '../common/sleep.js';
3
+ import { resolveFinalUsage, errorMessage } from './final-usage.js';
3
4
  import { getLogger } from '../logger/index.js';
4
5
  import { RunCardSession } from '../card/run-card-session.js';
5
6
  import { renderRunCard } from '../card/run-renderer.js';
@@ -48,6 +49,13 @@ const TERMINAL_REACTION_EMOJI = {
48
49
  function terminalReactionEmoji(terminal) {
49
50
  return TERMINAL_REACTION_EMOJI[terminal] ?? 'Done';
50
51
  }
52
+ /**
53
+ * W2.4 单点化:`'runCompact' in runner` 鸭子探测(compact 按钮门控,
54
+ * design doc §6.2-2)。codex app-server + kimi/opencode ACP runner 满足。
55
+ */
56
+ function runnerHasRunCompact(runner) {
57
+ return ('runCompact' in runner && typeof runner.runCompact === 'function');
58
+ }
51
59
  /**
52
60
  * Bridge owns the per-workspace serial processing queue (§9.6) and the
53
61
  * single-card streaming forwarding loop (§9.12 watchdog + session sync).
@@ -245,7 +253,7 @@ export class Bridge {
245
253
  */
246
254
  hasRunCompact(workspace, kind = this.config.defaultAgent) {
247
255
  const runner = this.getRunner(workspace, kind);
248
- return ('runCompact' in runner && typeof runner.runCompact === 'function');
256
+ return runnerHasRunCompact(runner);
249
257
  }
250
258
  /** 入队时刻快照:当前 defaultAgent + 该 agent 的 sessionId(无 session 则 undefined)。
251
259
  * 唯一捕获点:排队消息在 T0 把 agent+session 钉进 AgentBinding,随任务闭包带到 T1
@@ -691,7 +699,7 @@ export class Bridge {
691
699
  const workspaces = this.workspaceStore.list();
692
700
  if (workspaces.length > 0) {
693
701
  // NOTE: fallback uses insertion order, not sort preference — by design
694
- // (workspace-sorting.md §9: cwd fallback stays insertion-order for now)
702
+ // (cwd fallback stays insertion-order for now)
695
703
  cwd = workspaces[0].path;
696
704
  getLogger().info(`[bridge] resolveCwd fallback cwd=${cwd} (first saved workspace)`);
697
705
  }
@@ -764,9 +772,6 @@ export class Bridge {
764
772
  replyTo: ctx.messageId,
765
773
  runId,
766
774
  renderOptions: {
767
- showThinking: this.config.output.showThinking,
768
- showToolUse: this.config.output.showToolUse,
769
- showToolResult: this.config.output.showToolResult,
770
775
  // Run card header shows the current agent name (e.g. "Claude · 思考中")
771
776
  agentKind,
772
777
  // Compact 按钮能力门控与 resume 卡 / auto-resume 卡一致(hasRunCompact
@@ -834,6 +839,8 @@ export class Bridge {
834
839
  let finalCumulativeOutputTokens;
835
840
  let finalCumulativeCacheReadTokens;
836
841
  let finalCumulativeCacheCreationTokens;
842
+ // Reasoning tokens (pi usage.reasoning) from the live result event.
843
+ let finalReasoningTokens;
837
844
  // 统一的 usage meta 构造(finalize 各分支共享,本方法内原先 5 处手写 11-15
838
845
  // 字段对象;第 6 处 streamCodexCompact 形状不同未收敛)。
839
846
  // catch 路径需覆盖 flow 字段时在展开后覆写即可。catch error 路径亦复用此
@@ -853,6 +860,7 @@ export class Bridge {
853
860
  cumulativeOutputTokens: finalCumulativeOutputTokens,
854
861
  cumulativeCacheReadTokens: finalCumulativeCacheReadTokens,
855
862
  cumulativeCacheCreationTokens: finalCumulativeCacheCreationTokens,
863
+ reasoningTokens: finalReasoningTokens,
856
864
  });
857
865
  try {
858
866
  try {
@@ -1047,6 +1055,9 @@ export class Bridge {
1047
1055
  if (u.totalTokens !== undefined) {
1048
1056
  finalTotalTokens = u.totalTokens;
1049
1057
  }
1058
+ if (u.reasoningTokens !== undefined) {
1059
+ finalReasoningTokens = u.reasoningTokens;
1060
+ }
1050
1061
  }
1051
1062
  }
1052
1063
  // Approval events (Codex app-server mode)
@@ -1125,7 +1136,7 @@ export class Bridge {
1125
1136
  const finalSessionId = this.sessionStore.getSessionId(ctx.userId, agentKind);
1126
1137
  const finalUsage = resultNotSuccess
1127
1138
  ? undefined
1128
- : this.resolveFinalUsage(finalSessionId, cwd, agentKind);
1139
+ : resolveFinalUsage(this.sessionReaderRegistry, this.config.defaultAgent, finalSessionId, cwd, agentKind);
1129
1140
  finalContextLength = finalUsage?.contextLength ?? contextLength;
1130
1141
  finalContextLimit = finalUsage?.contextLimit ?? liveContextLimit;
1131
1142
  finalCompactCount = finalUsage?.compactCount;
@@ -1243,9 +1254,6 @@ export class Bridge {
1243
1254
  if (settleResult === 'unsent') {
1244
1255
  const sent = await this.sendResult({
1245
1256
  card: renderRunCard(cardSession.currentState, {
1246
- showThinking: this.config.output.showThinking,
1247
- showToolUse: this.config.output.showToolUse,
1248
- showToolResult: this.config.output.showToolResult,
1249
1257
  agentKind: activeRun.agentKind,
1250
1258
  // 与 createRunSession 的 renderOptions 一致:fallback 卡也要按
1251
1259
  // runner 能力门控 Compact 按钮(缺省 undefined = 显示按钮,
@@ -1295,8 +1303,7 @@ export class Bridge {
1295
1303
  // §6.2-3:写入条件从「codex 终态」放宽为「runner 有 runCompact 的终态」
1296
1304
  // (codex app-server + kimi acp 均满足)。
1297
1305
  const runTerminal = cardSession.currentState.terminal;
1298
- const runnerHasCompact = 'runCompact' in activeRun.runner &&
1299
- typeof activeRun.runner.runCompact === 'function';
1306
+ const runnerHasCompact = runnerHasRunCompact(activeRun.runner);
1300
1307
  if (runnerHasCompact &&
1301
1308
  activeRun.runner.lifetime === 'workspace' &&
1302
1309
  (runTerminal === 'done' ||
@@ -1317,47 +1324,6 @@ export class Bridge {
1317
1324
  }
1318
1325
  }
1319
1326
  }
1320
- /**
1321
- * Read final usage (contextLength + compactCount + cache tokens) from the session jsonl.
1322
- * Called after a run finishes: live stream-json does not emit compact_boundary,
1323
- * so the live contextLength (fallback to result.usage input+output) is
1324
- * unreliable. The jsonl is authoritative for postTokens + compact event count.
1325
- */
1326
- resolveFinalUsage(sessionId, cwd, agentKind = this.config.defaultAgent) {
1327
- if (!sessionId)
1328
- return undefined;
1329
- try {
1330
- const content = this.sessionReaderRegistry.get(agentKind).readSessionContent(sessionId, cwd);
1331
- if (!content.usage) {
1332
- // EnterWorktree relocate (2026-08-04): jsonl read silently returned no
1333
- // usage (e.g. transcript moved mid-session). Surface it so token-stat
1334
- // fallback to per-run live usage is visible in logs, not silent.
1335
- getLogger().warn(`[bridge] resolveFinalUsage: no usage from jsonl sessionId=${sessionId} cwd=${cwd} agent=${agentKind}, card falls back to per-run live usage`);
1336
- }
1337
- return content.usage
1338
- ? {
1339
- contextLength: content.usage.contextLength,
1340
- contextLimit: content.usage.contextLimit,
1341
- compactCount: content.usage.compactCount,
1342
- compactPreContextLength: content.usage.compactPreContextLength,
1343
- cacheReadTokens: content.usage.cacheReadTokens,
1344
- cacheCreationTokens: content.usage.cacheCreationTokens,
1345
- totalTokens: content.usage.totalTokens,
1346
- inputTokens: content.usage.inputTokens,
1347
- outputTokens: content.usage.outputTokens,
1348
- cumulativeTotalTokens: content.usage.cumulativeTotalTokens,
1349
- cumulativeInputTokens: content.usage.cumulativeInputTokens,
1350
- cumulativeOutputTokens: content.usage.cumulativeOutputTokens,
1351
- cumulativeCacheReadTokens: content.usage.cumulativeCacheReadTokens,
1352
- cumulativeCacheCreationTokens: content.usage.cumulativeCacheCreationTokens,
1353
- }
1354
- : undefined;
1355
- }
1356
- catch (err) {
1357
- getLogger().warn(`[bridge] resolveFinalUsage failed sessionId=${sessionId}: ${errorMessage(err)}`);
1358
- return undefined;
1359
- }
1360
- }
1361
1327
  /**
1362
1328
  * Send a notification card when a run completes.
1363
1329
  * This card contains the session summary and a button to resume the session.
@@ -1528,8 +1494,7 @@ export class Bridge {
1528
1494
  return;
1529
1495
  // Check that the runner has runCompact method
1530
1496
  const runner = this.getRunner(cwd, last.agentKind);
1531
- if (!('runCompact' in runner) ||
1532
- typeof runner.runCompact !== 'function') {
1497
+ if (!runnerHasRunCompact(runner)) {
1533
1498
  await this.sendResult({ text: '⚠️ 当前运行模式不支持 Compact' }, ctx);
1534
1499
  return;
1535
1500
  }
@@ -1601,9 +1566,6 @@ export class Bridge {
1601
1566
  replyTo: ctx.messageId,
1602
1567
  runId: compactRunId,
1603
1568
  renderOptions: {
1604
- showThinking: this.config.output.showThinking,
1605
- showToolUse: this.config.output.showToolUse,
1606
- showToolResult: this.config.output.showToolResult,
1607
1569
  agentKind,
1608
1570
  },
1609
1571
  });
@@ -1637,10 +1599,10 @@ export class Bridge {
1637
1599
  // 压缩结束后从会话 jsonl 读权威统计(压缩后上下文、压缩次数、本次压缩
1638
1600
  // token 消耗与会话累计)。thread/compacted 通知与 jsonl 落盘几乎同时
1639
1601
  // (实测差 ~9ms),compactCount 未读到则短重试,仍失败优雅降级(无统计)。
1640
- let finalUsage = this.resolveFinalUsage(sessionId, cwd, agentKind);
1602
+ let finalUsage = resolveFinalUsage(this.sessionReaderRegistry, this.config.defaultAgent, sessionId, cwd, agentKind);
1641
1603
  for (let attempt = 0; !finalUsage?.compactCount && attempt < 2; attempt++) {
1642
1604
  await sleep(150);
1643
- finalUsage = this.resolveFinalUsage(sessionId, cwd, agentKind);
1605
+ finalUsage = resolveFinalUsage(this.sessionReaderRegistry, this.config.defaultAgent, sessionId, cwd, agentKind);
1644
1606
  }
1645
1607
  await cardSession.finish(terminal, {
1646
1608
  resultSubtype: resultSubtype ?? 'error',
@@ -1720,8 +1682,7 @@ export class Bridge {
1720
1682
  return;
1721
1683
  // Check that the runner has runCompact(codex/kimi/opencode/pi/claude 鸭子探测)。
1722
1684
  const runner = this.getRunner(cwd, agentKind);
1723
- if (!('runCompact' in runner) ||
1724
- typeof runner.runCompact !== 'function') {
1685
+ if (!runnerHasRunCompact(runner)) {
1725
1686
  await this.sendResult({ text: '⚠️ 当前运行模式不支持 Compact' }, ctx);
1726
1687
  return;
1727
1688
  }
@@ -1861,7 +1822,4 @@ export class Bridge {
1861
1822
  }
1862
1823
  }
1863
1824
  }
1864
- function errorMessage(error) {
1865
- return error instanceof Error ? error.message : String(error);
1866
- }
1867
1825
  //# sourceMappingURL=index.js.map
@@ -1,4 +1,5 @@
1
1
  import { getLogger } from '../logger/index.js';
2
+ import { displayName } from '../platform/path.js';
2
3
  /**
3
4
  * Per-workspace cap on the number of tasks WAITING to execute (review P2-5).
4
5
  * Without a bound, a message flood grows `queuedTasks` / the promise chain /
@@ -377,7 +378,7 @@ export class QueueManager {
377
378
  }
378
379
  /** Build queue status card elements. */
379
380
  buildQueueStatusCardElements(workspace, actualPosition, tasksAhead, messageId, messagePreview) {
380
- const workspaceName = workspace.split('/').pop() ?? workspace;
381
+ const workspaceName = displayName(workspace);
381
382
  const isRunning = this.isWorkspaceRunning(workspace);
382
383
  const elements = [
383
384
  {
@@ -471,7 +472,7 @@ export class QueueManager {
471
472
  getLogger().debug(`[queue-manager] no queue card to update, messageId=${messageId}`);
472
473
  return;
473
474
  }
474
- const workspaceName = workspace.split('/').pop() ?? workspace;
475
+ const workspaceName = displayName(workspace);
475
476
  try {
476
477
  // Prefer the live preview: an edited task's messagePreview is updated in
477
478
  // place, and the passed-in snapshot may be stale if the card send was in
@@ -547,7 +548,7 @@ export class QueueManager {
547
548
  getLogger().debug(`[queue-manager] no queue card to update (cancelled), messageId=${messageId}`);
548
549
  return;
549
550
  }
550
- const workspaceName = workspace.split('/').pop() ?? workspace;
551
+ const workspaceName = displayName(workspace);
551
552
  const card = {
552
553
  schema: '2.0',
553
554
  config: { wide_screen_mode: true },