lark-remote 0.1.13 → 0.2.0

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 +25 -0
  2. package/README.en.md +2 -6
  3. package/README.md +3 -7
  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 +21 -63
  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 -3
  34. package/dist/index.js +14 -41
  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 +450 -315
  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 +34 -57
  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 +6 -5
  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,31 @@ 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.0] - 2026-09-11
9
+
10
+ ### 新增
11
+
12
+ - **Windows 平台支持(预发布)**:新增跨平台平台层(`src/platform/`)——路径、可执行文件发现、进程终止、spawn、shell 的抽象与 posix / win32 双实现;命令拉起统一走 `cross-spawn`,posix 行为零变化。Windows 尚未正式发布,README 前置条件已相应标注
13
+ - **`/download <path>`(别名 `/d`)**:把本地文件直接发到会话,不经 `/ls` 卡片。路径规则同 `/ls`(`~` 展开、相对 cwd、30MB 上限),目录 / 不存在 / 超限给出明确错误
14
+ - **`/ls <file>` 单文件卡**:传入文件路径渲染单文件卡(完整路径、大小、修改时间、「📎 下载」+「上级」按钮),不再报 `ls: xxx: Not a directory`
15
+ - **分页栏页码跳转**:所有分页卡片(`/ls`、`/ws`、`/resume`、`/active`、`/order`)支持在分页栏输入页码直达;越界钳到末页,非数字输入报错且不刷新卡片
16
+ - **扫码向导跨平台**:二维码渲染改用 `qrcode-generator`,Windows 全块渲染 + 静区 + 图片兜底
17
+
18
+ ### 修复
19
+
20
+ - `/ls` 的「返回」改为回到浏览起点(`/ls <dir>` 指定的目录),不再跳回 workspace cwd;浏览起点随卡片 payload 传递,翻页 / 刷新 / 上级 / 目录按钮不会丢失
21
+ - codex 会话兼容 rollout ≥0.153.4 的 `item_completed` 词表,修复 `/resume` 标题丢失
22
+ - compact 卡片空内容渲染「Compact 完成」,不再显示通用空占位
23
+ - 信息保真:notices 改为时间线消息渲染、省略提示固定置顶、工具卡片消除孤立代理项
24
+ - Windows:隐藏 cmd.exe 控制台窗口(`windowsHide`)
25
+
26
+ ### 变更
27
+
28
+ - **移除 `output.showThinking` / `showToolUse` / `showToolResult` 配置项与 `/config` 的「📤 输出」页签**:thinking、工具调用与工具结果恒常展示;旧配置文件的 `output` 段在加载时自动忽略,并在下次保存时从磁盘清理
29
+ - 代码精简:ACP / translator / session 共享层下沉(`BaseAcpTranslator` / `BaseAcpRunner` / two-pass 扫描),死代码清理与平行重复收敛
30
+ - 依赖:`qrcode-terminal` 换为 `qrcode-generator`;新增 `cross-spawn`(Windows 命令拉起)
31
+ - 测试基建:新增 scoped typecheck(`tsconfig.test-infra.json`)覆盖 `tests/lib/**` 共享工厂;跨平台测试稳定化(win32 路径归一化、并行 vitest 内存上限)
32
+
8
33
  ## [0.1.13] - 2026-09-04
9
34
 
10
35
  ### 修复
package/README.en.md CHANGED
@@ -73,11 +73,6 @@ claude:
73
73
  # permissionMode is hardcoded to bypassPermissions (inside the runner)
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
@@ -29,7 +29,7 @@ Codex 审批模式下,执行命令前会收到审批卡片,可直接在飞
29
29
 
30
30
  ## 前置条件
31
31
 
32
- - 操作系统:macOS / Linux。**暂不支持 Windows**(依赖 Unix 信号、bash、文件锁等 POSIX 行为)
32
+ - 操作系统:macOS / Linux。**暂不支持 Windows**(原生 Windows 支持在建设中:平台 seam 已落地,待真机验证后发布)
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 本地子进程。
@@ -73,11 +73,6 @@ claude:
73
73
  # permissionMode 硬编码为 bypassPermissions(runner 内部)
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
@@ -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 },
@@ -6,6 +6,7 @@
6
6
  */
7
7
  import { collapsibleMarkdownPanel } from './collapsible.js';
8
8
  import { truncateUtf8, truncateMarkdownTables } from './text-truncate.js';
9
+ import { cardButton } from './card-shared.js';
9
10
  /** Unique nonce for card button callbacks (SDK dedup + router validation). */
10
11
  function mkNonce() {
11
12
  return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
@@ -100,23 +101,13 @@ function renderDecisionButtons(approval, expired, runId) {
100
101
  // 决策按钮逐个作为 body 直接元素返回(CardKit body 纵向堆叠 = 上下排列)。
101
102
  // 不能用 column_set width:auto 横排:手机窄屏下多个按钮挤在一行,按钮文字
102
103
  // 被截断成省略号,用户无法辨认审批动作。
103
- return offered.map((d) => ({
104
- tag: 'button',
105
- text: { tag: 'plain_text', content: BUTTONS[d].label },
106
- type: BUTTONS[d].type,
107
- behaviors: [
108
- {
109
- type: 'callback',
110
- value: {
111
- cmd: 'approval.respond',
112
- decision: d,
113
- requestId: approval.requestId,
114
- runId,
115
- nonce: mkNonce(),
116
- },
117
- },
118
- ],
119
- }));
104
+ return offered.map((d) => cardButton(BUTTONS[d].label, {
105
+ cmd: 'approval.respond',
106
+ decision: d,
107
+ requestId: approval.requestId,
108
+ runId,
109
+ nonce: mkNonce(),
110
+ }, { type: BUTTONS[d].type }));
120
111
  }
121
112
  // =============================================================================
122
113
  // Tool Approval (non-command tools, e.g. ExitPlanMode)
@@ -350,28 +341,14 @@ function renderPermissionsApproval(approval, expired, runId) {
350
341
  tag: 'div',
351
342
  text: { tag: 'lark_md', content: `${icon} ${item.label}` },
352
343
  });
353
- elements.push({
354
- tag: 'button',
355
- text: {
356
- tag: 'plain_text',
357
- content: item.selected ? '取消授予' : '授予',
358
- },
359
- type: item.selected ? 'default' : 'primary',
360
- size: 'small',
361
- behaviors: [
362
- {
363
- type: 'callback',
364
- value: {
365
- cmd: 'approval.toggle',
366
- requestId: approval.requestId,
367
- permId: item.id,
368
- selected: !item.selected,
369
- runId,
370
- nonce: mkNonce(),
371
- },
372
- },
373
- ],
374
- });
344
+ elements.push(cardButton(item.selected ? '取消授予' : '授予', {
345
+ cmd: 'approval.toggle',
346
+ requestId: approval.requestId,
347
+ permId: item.id,
348
+ selected: !item.selected,
349
+ runId,
350
+ nonce: mkNonce(),
351
+ }, { type: item.selected ? 'default' : 'primary', size: 'small' }));
375
352
  }
376
353
  }
377
354
  // Action buttons
@@ -473,46 +450,26 @@ function renderQuestionApproval(approval, expired, runId) {
473
450
  }
474
451
  // 多选问题:有勾选才给提交按钮(单选在协调器内即时提交)。
475
452
  if (q.multiSelect && selected.length > 0) {
476
- elements.push({
477
- tag: 'button',
478
- text: { tag: 'plain_text', content: `✅ 提交答案(已选 ${selected.length} 项)` },
479
- type: 'primary',
480
- behaviors: [
481
- {
482
- type: 'callback',
483
- value: {
484
- cmd: 'approval.answerSubmit',
485
- requestId: approval.requestId,
486
- questionIndex: i,
487
- runId,
488
- nonce: mkNonce(),
489
- },
490
- },
491
- ],
492
- });
453
+ elements.push(cardButton(`✅ 提交答案(已选 ${selected.length} 项)`, {
454
+ cmd: 'approval.answerSubmit',
455
+ requestId: approval.requestId,
456
+ questionIndex: i,
457
+ runId,
458
+ nonce: mkNonce(),
459
+ }, { type: 'primary' }));
493
460
  }
494
461
  }
495
462
  // 提问卡底部统一「跳过回答」:走现有 approval.respond + decision=decline
496
463
  // (coordinator 对 question 允许 decline 作为安全兜底),runner 按协议映射
497
464
  // 跳过语义(Claude deny / Codex 空 answers / Kimi action decline / Pi cancelled)。
498
465
  if (!expired) {
499
- elements.push({
500
- tag: 'button',
501
- text: { tag: 'plain_text', content: '⏭️ 跳过回答' },
502
- type: 'default',
503
- behaviors: [
504
- {
505
- type: 'callback',
506
- value: {
507
- cmd: 'approval.respond',
508
- decision: 'decline',
509
- requestId: approval.requestId,
510
- runId,
511
- nonce: mkNonce(),
512
- },
513
- },
514
- ],
515
- });
466
+ elements.push(cardButton('⏭️ 跳过回答', {
467
+ cmd: 'approval.respond',
468
+ decision: 'decline',
469
+ requestId: approval.requestId,
470
+ runId,
471
+ nonce: mkNonce(),
472
+ }, { type: 'default' }));
516
473
  }
517
474
  return elements;
518
475
  }
@@ -530,27 +487,15 @@ function renderOptionRow(requestId, questionIndex, option, isSelected, icon, run
530
487
  width: 'auto',
531
488
  vertical_align: 'center',
532
489
  elements: [
533
- {
534
- tag: 'button',
535
- text: { tag: 'plain_text', content: isSelected ? '取消选择' : '选择' },
536
- type: isSelected ? 'default' : 'primary',
537
- size: 'small',
538
- // 固定宽度让所有选项行的按钮左对齐(描述长短不影响按钮列)。
539
- width: '100px',
540
- behaviors: [
541
- {
542
- type: 'callback',
543
- value: {
544
- cmd: 'approval.answer',
545
- requestId,
546
- questionIndex,
547
- option: option.label,
548
- runId,
549
- nonce: mkNonce(),
550
- },
551
- },
552
- ],
553
- },
490
+ // 固定宽度让所有选项行的按钮左对齐(描述长短不影响按钮列)。
491
+ cardButton(isSelected ? '取消选择' : '选择', {
492
+ cmd: 'approval.answer',
493
+ requestId,
494
+ questionIndex,
495
+ option: option.label,
496
+ runId,
497
+ nonce: mkNonce(),
498
+ }, { type: isSelected ? 'default' : 'primary', size: 'small', width: '100px' }),
554
499
  ],
555
500
  },
556
501
  {
@@ -78,16 +78,17 @@ function assembleCard(state, elements) {
78
78
  };
79
79
  }
80
80
  /**
81
- * Build elements for the normal path: full output/stderr (each ≤ OUTPUT_MAX_BYTES).
81
+ * Build output/stderr elements shared by normal & degraded paths (W2.3 单源):
82
+ * 除截断预算(OUTPUT_MAX_BYTES / DEGRADED_OUTPUT_BYTES)外逐行同构。
82
83
  */
83
- function buildNormalElements(state) {
84
+ function buildOutputElements(state, maxBytes) {
84
85
  const { terminal, runId, output, stderr } = state;
85
86
  const elements = [buildStatusRow(state)];
86
87
  const truncatedOutput = output
87
- ? truncateUtf8(output, OUTPUT_MAX_BYTES, true, '…(输出已截断,共 ' + output.length + ' 字符)')
88
+ ? truncateUtf8(output, maxBytes, true, '…(输出已截断,共 ' + output.length + ' 字符)')
88
89
  : '';
89
90
  const truncatedStderr = stderr
90
- ? truncateUtf8(stderr, OUTPUT_MAX_BYTES, true, '…(诊断输出已截断,共 ' + stderr.length + ' 字符)')
91
+ ? truncateUtf8(stderr, maxBytes, true, '…(诊断输出已截断,共 ' + stderr.length + ' 字符)')
91
92
  : '';
92
93
  if (truncatedOutput)
93
94
  elements.push(markdownDiv(`\`\`\`\n${truncatedOutput}\n\`\`\``));
@@ -99,28 +100,18 @@ function buildNormalElements(state) {
99
100
  elements.push(...buildExitFooter(state));
100
101
  return elements;
101
102
  }
103
+ /**
104
+ * Build elements for the normal path: full output/stderr (each ≤ OUTPUT_MAX_BYTES).
105
+ */
106
+ function buildNormalElements(state) {
107
+ return buildOutputElements(state, OUTPUT_MAX_BYTES);
108
+ }
102
109
  /**
103
110
  * Build elements for the degraded path: output/stderr truncated to a smaller
104
111
  * budget. Preserves command (status row) and exitCode (footer).
105
112
  */
106
113
  function buildDegradedElements(state) {
107
- const { terminal, runId, output, stderr } = state;
108
- const elements = [buildStatusRow(state)];
109
- const truncatedOutput = output
110
- ? truncateUtf8(output, DEGRADED_OUTPUT_BYTES, true, '…(输出已截断,共 ' + output.length + ' 字符)')
111
- : '';
112
- const truncatedStderr = stderr
113
- ? truncateUtf8(stderr, DEGRADED_OUTPUT_BYTES, true, '…(诊断输出已截断,共 ' + stderr.length + ' 字符)')
114
- : '';
115
- if (truncatedOutput)
116
- elements.push(markdownDiv(`\`\`\`\n${truncatedOutput}\n\`\`\``));
117
- if (truncatedStderr)
118
- elements.push(markdownDiv(`📋 诊断输出:\n\`\`\`\n${truncatedStderr}\n\`\`\``));
119
- if (terminal === 'running') {
120
- elements.push({ tag: 'div', text: { content: '‎', tag: 'lark_md' } }, stopButton(runId));
121
- }
122
- elements.push(...buildExitFooter(state));
123
- return elements;
114
+ return buildOutputElements(state, DEGRADED_OUTPUT_BYTES);
124
115
  }
125
116
  /**
126
117
  * Build elements for the extreme fallback: no output/stderr body, only