lark-remote 0.1.12 → 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 (160) hide show
  1. package/CHANGELOG.md +37 -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/inbound-media.d.ts +0 -3
  7. package/dist/bridge/inbound-media.js +0 -1
  8. package/dist/bridge/index.d.ts +7 -7
  9. package/dist/bridge/index.js +100 -133
  10. package/dist/bridge/queue-manager.js +4 -3
  11. package/dist/card/approval-render.js +39 -94
  12. package/dist/card/bash-renderer.js +12 -21
  13. package/dist/card/card-shared.d.ts +14 -5
  14. package/dist/card/card-shared.js +23 -34
  15. package/dist/card/run-renderer.d.ts +0 -22
  16. package/dist/card/run-renderer.js +206 -153
  17. package/dist/card/run-state.d.ts +29 -1
  18. package/dist/card/run-state.js +172 -84
  19. package/dist/card/text-truncate.d.ts +0 -9
  20. package/dist/card/text-truncate.js +0 -9
  21. package/dist/card/tool-render.d.ts +4 -1
  22. package/dist/card/tool-render.js +76 -32
  23. package/dist/cli.js +2 -2
  24. package/dist/common/fs.d.ts +14 -0
  25. package/dist/common/fs.js +57 -3
  26. package/dist/common/truncate.d.ts +0 -6
  27. package/dist/common/truncate.js +1 -1
  28. package/dist/common/ttl-cache.d.ts +24 -0
  29. package/dist/common/ttl-cache.js +53 -0
  30. package/dist/config/codex-config.js +14 -11
  31. package/dist/config/index.d.ts +0 -8
  32. package/dist/config/index.js +0 -16
  33. package/dist/config/kimi-config.d.ts +1 -1
  34. package/dist/config/kimi-config.js +2 -1
  35. package/dist/config/opencode-config.d.ts +1 -1
  36. package/dist/config/opencode-config.js +16 -10
  37. package/dist/config/qr.d.ts +36 -0
  38. package/dist/config/qr.js +124 -0
  39. package/dist/config/wizard.js +69 -27
  40. package/dist/connector/index.d.ts +3 -2
  41. package/dist/connector/index.js +5 -3
  42. package/dist/index.js +14 -41
  43. package/dist/instance-lock.js +7 -0
  44. package/dist/persistence/atomic-write.d.ts +2 -0
  45. package/dist/persistence/atomic-write.js +20 -5
  46. package/dist/persistence/load-json-file.d.ts +1 -2
  47. package/dist/persistence/load-json-file.js +2 -3
  48. package/dist/platform/agent-stopper.d.ts +22 -0
  49. package/dist/platform/agent-stopper.js +19 -0
  50. package/dist/platform/command.d.ts +11 -0
  51. package/dist/platform/command.js +107 -0
  52. package/dist/platform/identity.d.ts +33 -0
  53. package/dist/platform/identity.js +187 -0
  54. package/dist/platform/path.d.ts +24 -0
  55. package/dist/platform/path.js +110 -0
  56. package/dist/platform/select.d.ts +9 -0
  57. package/dist/platform/select.js +12 -0
  58. package/dist/platform/shell.d.ts +51 -0
  59. package/dist/platform/shell.js +75 -0
  60. package/dist/platform/spawn.d.ts +41 -0
  61. package/dist/platform/spawn.js +77 -0
  62. package/dist/platform/terminator-posix.d.ts +38 -0
  63. package/dist/platform/terminator-posix.js +103 -0
  64. package/dist/platform/terminator-win32.d.ts +21 -0
  65. package/dist/platform/terminator-win32.js +110 -0
  66. package/dist/platform/terminator.d.ts +44 -0
  67. package/dist/platform/terminator.js +24 -0
  68. package/dist/platform/types.d.ts +23 -0
  69. package/dist/platform/types.js +5 -0
  70. package/dist/restart.js +4 -2
  71. package/dist/router/card-helpers.d.ts +33 -6
  72. package/dist/router/card-helpers.js +67 -11
  73. package/dist/router/config/common/render.d.ts +1 -1
  74. package/dist/router/config/common/render.js +1 -1
  75. package/dist/router/config-diff.d.ts +4 -0
  76. package/dist/router/config-diff.js +70 -0
  77. package/dist/router/index.d.ts +73 -6
  78. package/dist/router/index.js +663 -590
  79. package/dist/router/utils.d.ts +6 -0
  80. package/dist/router/utils.js +12 -0
  81. package/dist/runner/bash/runner.d.ts +9 -2
  82. package/dist/runner/bash/runner.js +45 -20
  83. package/dist/runner/claude/session.d.ts +4 -4
  84. package/dist/runner/claude/session.js +18 -9
  85. package/dist/runner/codex/app-server/protocol-types.d.ts +35 -1
  86. package/dist/runner/codex/app-server/runner.d.ts +1 -16
  87. package/dist/runner/codex/app-server/runner.js +2 -2
  88. package/dist/runner/codex/app-server/translator.d.ts +18 -0
  89. package/dist/runner/codex/app-server/translator.js +146 -0
  90. package/dist/runner/common/acp/base-acp-runner.d.ts +116 -0
  91. package/dist/runner/common/acp/base-acp-runner.js +248 -0
  92. package/dist/runner/common/acp/base-translator.d.ts +141 -0
  93. package/dist/runner/common/acp/base-translator.js +271 -0
  94. package/dist/runner/common/acp/protocol-helpers.d.ts +70 -0
  95. package/dist/runner/common/acp/protocol-helpers.js +82 -0
  96. package/dist/runner/common/connection-based-runner.d.ts +0 -2
  97. package/dist/runner/common/connection-based-runner.js +1 -1
  98. package/dist/runner/common/jsonrpc/transport.d.ts +2 -0
  99. package/dist/runner/common/jsonrpc/transport.js +25 -5
  100. package/dist/runner/common/process-stopper.d.ts +4 -26
  101. package/dist/runner/common/process-stopper.js +4 -76
  102. package/dist/runner/common/runner-utils.d.ts +0 -5
  103. package/dist/runner/common/runner-utils.js +0 -7
  104. package/dist/runner/common/spawning-runner.d.ts +42 -132
  105. package/dist/runner/common/spawning-runner.js +52 -271
  106. package/dist/runner/common/usage.d.ts +2 -0
  107. package/dist/runner/common/usage.js +2 -0
  108. package/dist/runner/dsh/index.d.ts +0 -2
  109. package/dist/runner/dsh/index.js +0 -2
  110. package/dist/runner/dsh/runner.d.ts +0 -2
  111. package/dist/runner/dsh/runner.js +1 -1
  112. package/dist/runner/index.d.ts +1 -1
  113. package/dist/runner/kimi/acp/runner.d.ts +63 -53
  114. package/dist/runner/kimi/acp/runner.js +189 -334
  115. package/dist/runner/kimi/acp/translator.d.ts +18 -101
  116. package/dist/runner/kimi/acp/translator.js +61 -255
  117. package/dist/runner/opencode/acp/runner.d.ts +22 -46
  118. package/dist/runner/opencode/acp/runner.js +74 -297
  119. package/dist/runner/opencode/acp/translator.d.ts +28 -104
  120. package/dist/runner/opencode/acp/translator.js +54 -249
  121. package/dist/runner/pi/rpc/index.d.ts +0 -2
  122. package/dist/runner/pi/rpc/index.js +0 -2
  123. package/dist/runner/pi/rpc/translator.d.ts +3 -0
  124. package/dist/runner/pi/rpc/translator.js +47 -9
  125. package/dist/runner/probe.d.ts +8 -11
  126. package/dist/runner/probe.js +17 -21
  127. package/dist/runner/registry.d.ts +4 -4
  128. package/dist/runner/registry.js +4 -4
  129. package/dist/runner/resolve-agent-choices.js +6 -2
  130. package/dist/runner/sync-agent-choices.js +1 -1
  131. package/dist/runner/types.d.ts +38 -1
  132. package/dist/session/claude/session-index.js +3 -1
  133. package/dist/session/claude/sessions.js +37 -64
  134. package/dist/session/codex/rollout-reader.d.ts +10 -14
  135. package/dist/session/codex/rollout-reader.js +126 -48
  136. package/dist/session/common/constants.d.ts +7 -0
  137. package/dist/session/common/constants.js +9 -0
  138. package/dist/session/common/pagination.d.ts +0 -2
  139. package/dist/session/common/pagination.js +1 -1
  140. package/dist/session/common/recency.d.ts +7 -0
  141. package/dist/session/common/recency.js +10 -0
  142. package/dist/session/common/two-pass.d.ts +22 -0
  143. package/dist/session/common/two-pass.js +70 -0
  144. package/dist/session/common/usage-accumulator.d.ts +14 -0
  145. package/dist/session/common/usage-accumulator.js +15 -0
  146. package/dist/session/dsh/sessions.js +15 -11
  147. package/dist/session/kimi/sessions.d.ts +24 -4
  148. package/dist/session/kimi/sessions.js +99 -103
  149. package/dist/session/opencode/sessions.d.ts +2 -14
  150. package/dist/session/opencode/sessions.js +35 -15
  151. package/dist/session/pi/sessions.js +64 -95
  152. package/dist/update/index.d.ts +2 -3
  153. package/dist/update/index.js +1 -2
  154. package/dist/update/install.d.ts +2 -1
  155. package/dist/update/install.js +58 -9
  156. package/dist/update/version-check.d.ts +5 -0
  157. package/dist/update/version-check.js +9 -0
  158. package/package.json +9 -7
  159. package/dist/update/startup-hint.d.ts +0 -5
  160. package/dist/update/startup-hint.js +0 -11
package/CHANGELOG.md CHANGED
@@ -5,6 +5,43 @@ 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
+
33
+ ## [0.1.13] - 2026-09-04
34
+
35
+ ### 修复
36
+
37
+ - kimi ACP Compact 等待机制重构:等待协议终态(complete / cancel / unknown)再收尾,移除「30s 超时即视为成功」的假完成路径——Compact 实际失败或被取消时如实反馈
38
+
39
+ ### 变更
40
+
41
+ - 代码与测试清理:死导出/死字段清理、bridge usage 统计收敛、router 样板收敛、session 公共件下沉(`ttl-cache` / `recency`)、ACP 共享层下沉
42
+ - 测试治理:超大测试文件按主题拆分、agent choices 测试表驱动化、弱断言收敛、移除无效测试
43
+ - 移除未使用的 `@types/form-data` 依赖
44
+
8
45
  ## [0.1.12] - 2026-08-29
9
46
 
10
47
  ### 修复
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
@@ -23,9 +23,6 @@ export declare function sanitizeFileName(name: string): string;
23
23
  * 两者都无法识别时返回 undefined(调用方省略扩展名,避免错误标注格式)。
24
24
  */
25
25
  export declare function imageExtension(mimeType: string | undefined, head: Buffer): string | undefined;
26
- /** 按 UTF-8 字节数截断字符串(避免截在多字节字符中间)。 */
27
- import { truncateUtf8 } from '../common/truncate.js';
28
- export { truncateUtf8 };
29
26
  /**
30
27
  * 文件名长度限制(字节级,保留扩展名),避免 ENAMETOOLONG 整文件失败。
31
28
  * 极罕见情况(扩展名本身超限)直接截断全名。
@@ -99,7 +99,6 @@ export function imageExtension(mimeType, head) {
99
99
  }
100
100
  /** 按 UTF-8 字节数截断字符串(避免截在多字节字符中间)。 */
101
101
  import { truncateUtf8 } from '../common/truncate.js';
102
- export { truncateUtf8 };
103
102
  /**
104
103
  * 文件名长度限制(字节级,保留扩展名),避免 ENAMETOOLONG 整文件失败。
105
104
  * 极罕见情况(扩展名本身超限)直接截断全名。
@@ -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.
@@ -463,6 +456,13 @@ export declare class Bridge {
463
456
  handleCodexCompact(value: {
464
457
  runId?: string;
465
458
  }, ctx: BridgeContext): Promise<void>;
459
+ /**
460
+ * §5.3: kimi 压缩在途检测——reader 鸭子实现 `readCompactionState` 时,若
461
+ * wire 状态机判定当前有未终结的压缩(最后一次 begin 无 terminal 记录),
462
+ * 直接回文本「已有一次 Compact 正在进行」并返回 true(调用方不开卡、不发
463
+ * session/prompt)。codex/claude 等 reader 无此方法 → 返回 false,行为不变。
464
+ */
465
+ private rejectIfCompactionInFlight;
466
466
  /**
467
467
  * Stream a compaction to a card: start a RunCardSession with
468
468
  * operationKind='compaction' (no recursive Compact button), consume
@@ -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,29 @@ 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;
844
+ // 统一的 usage meta 构造(finalize 各分支共享,本方法内原先 5 处手写 11-15
845
+ // 字段对象;第 6 处 streamCodexCompact 形状不同未收敛)。
846
+ // catch 路径需覆盖 flow 字段时在展开后覆写即可。catch error 路径亦复用此
847
+ // 闭包,相比旧手写对象补齐了累计 cache 字段(cumulativeCacheReadTokens/
848
+ // cumulativeCacheCreationTokens),属有意的口径对齐(向 done 路径看齐)。
849
+ const usageMeta = () => ({
850
+ contextLength: finalContextLength,
851
+ contextLimit: finalContextLimit,
852
+ compactCount: finalCompactCount,
853
+ cacheReadTokens: finalCacheReadTokens,
854
+ cacheCreationTokens: finalCacheCreationTokens,
855
+ totalTokens: finalTotalTokens,
856
+ inputTokens: finalInputTokens,
857
+ outputTokens: finalOutputTokens,
858
+ cumulativeTotalTokens: finalCumulativeTotalTokens,
859
+ cumulativeInputTokens: finalCumulativeInputTokens,
860
+ cumulativeOutputTokens: finalCumulativeOutputTokens,
861
+ cumulativeCacheReadTokens: finalCumulativeCacheReadTokens,
862
+ cumulativeCacheCreationTokens: finalCumulativeCacheCreationTokens,
863
+ reasoningTokens: finalReasoningTokens,
864
+ });
837
865
  try {
838
866
  try {
839
867
  getLogger().debug(`[bridge] cardSession.start() begin runId=${runId}`);
@@ -1027,6 +1055,9 @@ export class Bridge {
1027
1055
  if (u.totalTokens !== undefined) {
1028
1056
  finalTotalTokens = u.totalTokens;
1029
1057
  }
1058
+ if (u.reasoningTokens !== undefined) {
1059
+ finalReasoningTokens = u.reasoningTokens;
1060
+ }
1030
1061
  }
1031
1062
  }
1032
1063
  // Approval events (Codex app-server mode)
@@ -1105,7 +1136,7 @@ export class Bridge {
1105
1136
  const finalSessionId = this.sessionStore.getSessionId(ctx.userId, agentKind);
1106
1137
  const finalUsage = resultNotSuccess
1107
1138
  ? undefined
1108
- : this.resolveFinalUsage(finalSessionId, cwd, agentKind);
1139
+ : resolveFinalUsage(this.sessionReaderRegistry, this.config.defaultAgent, finalSessionId, cwd, agentKind);
1109
1140
  finalContextLength = finalUsage?.contextLength ?? contextLength;
1110
1141
  finalContextLimit = finalUsage?.contextLimit ?? liveContextLimit;
1111
1142
  finalCompactCount = finalUsage?.compactCount;
@@ -1156,57 +1187,19 @@ export class Bridge {
1156
1187
  await cardSession.finish(terminal, {
1157
1188
  resultSubtype: subtype,
1158
1189
  errorMsg: finalState.errorMsg,
1159
- contextLength: finalContextLength,
1160
- contextLimit: finalContextLimit,
1161
- compactCount: finalCompactCount,
1162
- cacheReadTokens: finalCacheReadTokens,
1163
- cacheCreationTokens: finalCacheCreationTokens,
1164
- totalTokens: finalTotalTokens,
1165
- inputTokens: finalInputTokens,
1166
- outputTokens: finalOutputTokens,
1167
- cumulativeTotalTokens: finalCumulativeTotalTokens,
1168
- cumulativeInputTokens: finalCumulativeInputTokens,
1169
- cumulativeOutputTokens: finalCumulativeOutputTokens,
1170
- cumulativeCacheReadTokens: finalCumulativeCacheReadTokens,
1171
- cumulativeCacheCreationTokens: finalCumulativeCacheCreationTokens,
1190
+ ...usageMeta(),
1172
1191
  });
1173
1192
  }
1174
1193
  else if (finalState.terminal === 'running') {
1175
1194
  await cardSession.finish('error', {
1176
- contextLength: finalContextLength,
1177
- contextLimit: finalContextLimit,
1178
- compactCount: finalCompactCount,
1179
- cacheReadTokens: finalCacheReadTokens,
1180
- cacheCreationTokens: finalCacheCreationTokens,
1181
- totalTokens: finalTotalTokens,
1182
- inputTokens: finalInputTokens,
1183
- outputTokens: finalOutputTokens,
1184
- cumulativeTotalTokens: finalCumulativeTotalTokens,
1185
- cumulativeInputTokens: finalCumulativeInputTokens,
1186
- cumulativeOutputTokens: finalCumulativeOutputTokens,
1187
- cumulativeCacheReadTokens: finalCumulativeCacheReadTokens,
1188
- cumulativeCacheCreationTokens: finalCumulativeCacheCreationTokens,
1195
+ ...usageMeta(),
1189
1196
  errorMsg: agentDisplayName(agentKind) + ' 输出流已结束,但未收到 result 事件',
1190
1197
  });
1191
1198
  }
1192
1199
  else if (sawResult) {
1193
1200
  // 已终态(interrupted/idle_timeout)+ result 已收到:首终态优先,
1194
1201
  // 仅补充 usage meta(finalizing 期间被 /stop 或 idle 超时也展示 token 统计)
1195
- await cardSession.finish(finalState.terminal, {
1196
- contextLength: finalContextLength,
1197
- contextLimit: finalContextLimit,
1198
- compactCount: finalCompactCount,
1199
- cacheReadTokens: finalCacheReadTokens,
1200
- cacheCreationTokens: finalCacheCreationTokens,
1201
- totalTokens: finalTotalTokens,
1202
- inputTokens: finalInputTokens,
1203
- outputTokens: finalOutputTokens,
1204
- cumulativeTotalTokens: finalCumulativeTotalTokens,
1205
- cumulativeInputTokens: finalCumulativeInputTokens,
1206
- cumulativeOutputTokens: finalCumulativeOutputTokens,
1207
- cumulativeCacheReadTokens: finalCumulativeCacheReadTokens,
1208
- cumulativeCacheCreationTokens: finalCumulativeCacheCreationTokens,
1209
- });
1202
+ await cardSession.finish(finalState.terminal, usageMeta());
1210
1203
  }
1211
1204
  // else: 已终态且未收到 result(如 spawn 失败后 interrupted)-> 无 usage 可补,跳过
1212
1205
  }
@@ -1225,38 +1218,16 @@ export class Bridge {
1225
1218
  const catchTerminal = cardSession.currentState.terminal;
1226
1219
  if (catchTerminal === 'running' || catchTerminal === 'finalizing') {
1227
1220
  await cardSession.finish('error', {
1228
- contextLength: finalContextLength,
1229
- contextLimit: finalContextLimit,
1230
- compactCount: finalCompactCount,
1231
- cacheReadTokens: finalCacheReadTokens,
1232
- cacheCreationTokens: finalCacheCreationTokens,
1233
- totalTokens: finalTotalTokens,
1221
+ ...usageMeta(),
1234
1222
  inputTokens: liveInputTokens,
1235
1223
  outputTokens: liveOutputTokens,
1236
- cumulativeTotalTokens: finalCumulativeTotalTokens,
1237
- cumulativeInputTokens: finalCumulativeInputTokens,
1238
- cumulativeOutputTokens: finalCumulativeOutputTokens,
1239
1224
  errorMsg: errorMessage(err),
1240
1225
  });
1241
1226
  }
1242
1227
  else {
1243
1228
  getLogger().info(`[bridge] skip error finish: state already terminal (${catchTerminal}) runId=${runId}`);
1244
1229
  if (sawResult) {
1245
- await cardSession.finish(catchTerminal, {
1246
- contextLength: finalContextLength,
1247
- contextLimit: finalContextLimit,
1248
- compactCount: finalCompactCount,
1249
- cacheReadTokens: finalCacheReadTokens,
1250
- cacheCreationTokens: finalCacheCreationTokens,
1251
- totalTokens: finalTotalTokens,
1252
- inputTokens: finalInputTokens,
1253
- outputTokens: finalOutputTokens,
1254
- cumulativeTotalTokens: finalCumulativeTotalTokens,
1255
- cumulativeInputTokens: finalCumulativeInputTokens,
1256
- cumulativeOutputTokens: finalCumulativeOutputTokens,
1257
- cumulativeCacheReadTokens: finalCumulativeCacheReadTokens,
1258
- cumulativeCacheCreationTokens: finalCumulativeCacheCreationTokens,
1259
- });
1230
+ await cardSession.finish(catchTerminal, usageMeta());
1260
1231
  }
1261
1232
  }
1262
1233
  }
@@ -1283,9 +1254,6 @@ export class Bridge {
1283
1254
  if (settleResult === 'unsent') {
1284
1255
  const sent = await this.sendResult({
1285
1256
  card: renderRunCard(cardSession.currentState, {
1286
- showThinking: this.config.output.showThinking,
1287
- showToolUse: this.config.output.showToolUse,
1288
- showToolResult: this.config.output.showToolResult,
1289
1257
  agentKind: activeRun.agentKind,
1290
1258
  // 与 createRunSession 的 renderOptions 一致:fallback 卡也要按
1291
1259
  // runner 能力门控 Compact 按钮(缺省 undefined = 显示按钮,
@@ -1335,8 +1303,7 @@ export class Bridge {
1335
1303
  // §6.2-3:写入条件从「codex 终态」放宽为「runner 有 runCompact 的终态」
1336
1304
  // (codex app-server + kimi acp 均满足)。
1337
1305
  const runTerminal = cardSession.currentState.terminal;
1338
- const runnerHasCompact = 'runCompact' in activeRun.runner &&
1339
- typeof activeRun.runner.runCompact === 'function';
1306
+ const runnerHasCompact = runnerHasRunCompact(activeRun.runner);
1340
1307
  if (runnerHasCompact &&
1341
1308
  activeRun.runner.lifetime === 'workspace' &&
1342
1309
  (runTerminal === 'done' ||
@@ -1357,47 +1324,6 @@ export class Bridge {
1357
1324
  }
1358
1325
  }
1359
1326
  }
1360
- /**
1361
- * Read final usage (contextLength + compactCount + cache tokens) from the session jsonl.
1362
- * Called after a run finishes: live stream-json does not emit compact_boundary,
1363
- * so the live contextLength (fallback to result.usage input+output) is
1364
- * unreliable. The jsonl is authoritative for postTokens + compact event count.
1365
- */
1366
- resolveFinalUsage(sessionId, cwd, agentKind = this.config.defaultAgent) {
1367
- if (!sessionId)
1368
- return undefined;
1369
- try {
1370
- const content = this.sessionReaderRegistry.get(agentKind).readSessionContent(sessionId, cwd);
1371
- if (!content.usage) {
1372
- // EnterWorktree relocate (2026-08-04): jsonl read silently returned no
1373
- // usage (e.g. transcript moved mid-session). Surface it so token-stat
1374
- // fallback to per-run live usage is visible in logs, not silent.
1375
- getLogger().warn(`[bridge] resolveFinalUsage: no usage from jsonl sessionId=${sessionId} cwd=${cwd} agent=${agentKind}, card falls back to per-run live usage`);
1376
- }
1377
- return content.usage
1378
- ? {
1379
- contextLength: content.usage.contextLength,
1380
- contextLimit: content.usage.contextLimit,
1381
- compactCount: content.usage.compactCount,
1382
- compactPreContextLength: content.usage.compactPreContextLength,
1383
- cacheReadTokens: content.usage.cacheReadTokens,
1384
- cacheCreationTokens: content.usage.cacheCreationTokens,
1385
- totalTokens: content.usage.totalTokens,
1386
- inputTokens: content.usage.inputTokens,
1387
- outputTokens: content.usage.outputTokens,
1388
- cumulativeTotalTokens: content.usage.cumulativeTotalTokens,
1389
- cumulativeInputTokens: content.usage.cumulativeInputTokens,
1390
- cumulativeOutputTokens: content.usage.cumulativeOutputTokens,
1391
- cumulativeCacheReadTokens: content.usage.cumulativeCacheReadTokens,
1392
- cumulativeCacheCreationTokens: content.usage.cumulativeCacheCreationTokens,
1393
- }
1394
- : undefined;
1395
- }
1396
- catch (err) {
1397
- getLogger().warn(`[bridge] resolveFinalUsage failed sessionId=${sessionId}: ${errorMessage(err)}`);
1398
- return undefined;
1399
- }
1400
- }
1401
1327
  /**
1402
1328
  * Send a notification card when a run completes.
1403
1329
  * This card contains the session summary and a button to resume the session.
@@ -1562,10 +1488,13 @@ export class Bridge {
1562
1488
  await this.sendResult({ text: '⚠️ 未找到可压缩的会话' }, ctx);
1563
1489
  return;
1564
1490
  }
1491
+ // §5.3: kimi 压缩在途检测(wire 状态机)。第二次点击在本地毫秒级如实回答,
1492
+ // 不开卡、不发 session/prompt——否则 kimi 服务端拒绝并产生「秒回」假完成。
1493
+ if (await this.rejectIfCompactionInFlight(last.sessionId, cwd, last.agentKind, ctx))
1494
+ return;
1565
1495
  // Check that the runner has runCompact method
1566
1496
  const runner = this.getRunner(cwd, last.agentKind);
1567
- if (!('runCompact' in runner) ||
1568
- typeof runner.runCompact !== 'function') {
1497
+ if (!runnerHasRunCompact(runner)) {
1569
1498
  await this.sendResult({ text: '⚠️ 当前运行模式不支持 Compact' }, ctx);
1570
1499
  return;
1571
1500
  }
@@ -1578,6 +1507,36 @@ export class Bridge {
1578
1507
  ctx,
1579
1508
  });
1580
1509
  }
1510
+ /**
1511
+ * §5.3: kimi 压缩在途检测——reader 鸭子实现 `readCompactionState` 时,若
1512
+ * wire 状态机判定当前有未终结的压缩(最后一次 begin 无 terminal 记录),
1513
+ * 直接回文本「已有一次 Compact 正在进行」并返回 true(调用方不开卡、不发
1514
+ * session/prompt)。codex/claude 等 reader 无此方法 → 返回 false,行为不变。
1515
+ */
1516
+ async rejectIfCompactionInFlight(sessionId, cwd, agentKind, ctx) {
1517
+ const reader = this.sessionReaderRegistry.get(agentKind);
1518
+ if (!reader || !('readCompactionState' in reader))
1519
+ return false;
1520
+ try {
1521
+ const state = reader.readCompactionState(sessionId, cwd);
1522
+ if (state?.inFlight) {
1523
+ const since = state.inFlightSince;
1524
+ let timeLabel = '未知时刻';
1525
+ if (since !== undefined) {
1526
+ const d = new Date(since);
1527
+ const hh = String(d.getHours()).padStart(2, '0');
1528
+ const mm = String(d.getMinutes()).padStart(2, '0');
1529
+ timeLabel = `${hh}:${mm}`;
1530
+ }
1531
+ await this.sendResult({ text: `⚠️ 已有一次 Compact 正在进行(开始于 ${timeLabel}),请等待完成` }, ctx);
1532
+ return true;
1533
+ }
1534
+ }
1535
+ catch (err) {
1536
+ getLogger().warn(`[bridge] readCompactionState failed sessionId=${sessionId}: ${errorMessage(err)}`);
1537
+ }
1538
+ return false;
1539
+ }
1581
1540
  /**
1582
1541
  * Stream a compaction to a card: start a RunCardSession with
1583
1542
  * operationKind='compaction' (no recursive Compact button), consume
@@ -1607,9 +1566,6 @@ export class Bridge {
1607
1566
  replyTo: ctx.messageId,
1608
1567
  runId: compactRunId,
1609
1568
  renderOptions: {
1610
- showThinking: this.config.output.showThinking,
1611
- showToolUse: this.config.output.showToolUse,
1612
- showToolResult: this.config.output.showToolResult,
1613
1569
  agentKind,
1614
1570
  },
1615
1571
  });
@@ -1623,22 +1579,34 @@ export class Bridge {
1623
1579
  operationKind: 'compaction',
1624
1580
  });
1625
1581
  let sawResult = false;
1582
+ let resultSubtype;
1583
+ let resultError;
1626
1584
  for await (const event of runner.runCompact('', { cwd, sessionId })) {
1627
- if (event.type === 'result')
1585
+ if (event.type === 'result') {
1628
1586
  sawResult = true;
1587
+ resultSubtype = event.subtype;
1588
+ resultError = event.errorMessage;
1589
+ }
1629
1590
  await cardSession.push(event);
1630
1591
  }
1592
+ // §5.3: 卡片终态必须等于引擎真实终态。旧实现只看 sawResult 布尔——error
1593
+ // subtype 的 result 也被 finish 'done'(放大器 bug,2026-08-31 事故)。
1594
+ const terminal = resultSubtype === 'success'
1595
+ ? 'done'
1596
+ : resultSubtype === 'interrupted'
1597
+ ? 'interrupted'
1598
+ : 'error';
1631
1599
  // 压缩结束后从会话 jsonl 读权威统计(压缩后上下文、压缩次数、本次压缩
1632
1600
  // token 消耗与会话累计)。thread/compacted 通知与 jsonl 落盘几乎同时
1633
1601
  // (实测差 ~9ms),compactCount 未读到则短重试,仍失败优雅降级(无统计)。
1634
- let finalUsage = this.resolveFinalUsage(sessionId, cwd, agentKind);
1602
+ let finalUsage = resolveFinalUsage(this.sessionReaderRegistry, this.config.defaultAgent, sessionId, cwd, agentKind);
1635
1603
  for (let attempt = 0; !finalUsage?.compactCount && attempt < 2; attempt++) {
1636
1604
  await sleep(150);
1637
- finalUsage = this.resolveFinalUsage(sessionId, cwd, agentKind);
1605
+ finalUsage = resolveFinalUsage(this.sessionReaderRegistry, this.config.defaultAgent, sessionId, cwd, agentKind);
1638
1606
  }
1639
- await cardSession.finish(sawResult ? 'done' : 'error', {
1640
- resultSubtype: sawResult ? 'success' : 'error',
1641
- errorMsg: sawResult ? undefined : '未收到压缩结果',
1607
+ await cardSession.finish(terminal, {
1608
+ resultSubtype: resultSubtype ?? 'error',
1609
+ errorMsg: terminal === 'error' ? (resultError ?? '未收到压缩结果') : undefined,
1642
1610
  contextLength: finalUsage?.contextLength,
1643
1611
  contextLimit: finalUsage?.contextLimit,
1644
1612
  compactPreContextLength: finalUsage?.compactPreContextLength,
@@ -1654,7 +1622,7 @@ export class Bridge {
1654
1622
  cumulativeCacheReadTokens: finalUsage?.cumulativeCacheReadTokens,
1655
1623
  cumulativeCacheCreationTokens: finalUsage?.cumulativeCacheCreationTokens,
1656
1624
  });
1657
- log.info(`[bridge] streamCodexCompact finished sessionId=${sessionId} sawResult=${sawResult}`);
1625
+ log.info(`[bridge] streamCodexCompact finished sessionId=${sessionId} sawResult=${sawResult} subtype=${resultSubtype ?? 'none'}`);
1658
1626
  }
1659
1627
  catch (err) {
1660
1628
  log.error(`[bridge] streamCodexCompact failed: ${errorMessage(err)}`);
@@ -1709,10 +1677,12 @@ export class Bridge {
1709
1677
  await this.sendResult({ text: `⚠️ 未找到 session ${sessionId}(当前目录: ${cwd})` }, ctx);
1710
1678
  return;
1711
1679
  }
1680
+ // §5.3: kimi 压缩在途检测(与 handleCodexCompact 同一入口)。
1681
+ if (await this.rejectIfCompactionInFlight(sessionId, cwd, agentKind, ctx))
1682
+ return;
1712
1683
  // Check that the runner has runCompact(codex/kimi/opencode/pi/claude 鸭子探测)。
1713
1684
  const runner = this.getRunner(cwd, agentKind);
1714
- if (!('runCompact' in runner) ||
1715
- typeof runner.runCompact !== 'function') {
1685
+ if (!runnerHasRunCompact(runner)) {
1716
1686
  await this.sendResult({ text: '⚠️ 当前运行模式不支持 Compact' }, ctx);
1717
1687
  return;
1718
1688
  }
@@ -1852,7 +1822,4 @@ export class Bridge {
1852
1822
  }
1853
1823
  }
1854
1824
  }
1855
- function errorMessage(error) {
1856
- return error instanceof Error ? error.message : String(error);
1857
- }
1858
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 },