lark-remote 0.5.19 → 0.5.21

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 (136) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/dist/bridge/approval-actions.d.ts +1 -1
  3. package/dist/bridge/approval-actions.js +1 -1
  4. package/dist/bridge/bash-run.d.ts +71 -0
  5. package/dist/bridge/bash-run.js +217 -0
  6. package/dist/bridge/compact.d.ts +155 -0
  7. package/dist/bridge/compact.js +450 -0
  8. package/dist/bridge/final-usage.js +4 -4
  9. package/dist/bridge/inbound-media.js +72 -46
  10. package/dist/bridge/index.d.ts +106 -153
  11. package/dist/bridge/index.js +400 -1000
  12. package/dist/bridge/job-watch.d.ts +68 -0
  13. package/dist/bridge/job-watch.js +146 -0
  14. package/dist/bridge/queue-manager.d.ts +3 -3
  15. package/dist/bridge/queue-manager.js +10 -29
  16. package/dist/card/approval-render.d.ts +2 -2
  17. package/dist/card/approval-render.js +141 -130
  18. package/dist/card/card-budget.js +123 -98
  19. package/dist/card/card-helpers.d.ts +3 -3
  20. package/dist/card/card-helpers.js +6 -6
  21. package/dist/card/final-answer-file.js +1 -1
  22. package/dist/card/run-renderer.d.ts +3 -3
  23. package/dist/card/run-renderer.js +114 -159
  24. package/dist/card/run-state.d.ts +1 -15
  25. package/dist/card/run-state.js +358 -292
  26. package/dist/card/tool-render.d.ts +1 -1
  27. package/dist/card/tool-render.js +21 -17
  28. package/dist/card/usage-format.d.ts +22 -7
  29. package/dist/card/usage-format.js +119 -100
  30. package/dist/cli.js +15 -3
  31. package/dist/clone.js +2 -2
  32. package/dist/common/text-width.js +36 -24
  33. package/dist/common/tool-summary.js +132 -90
  34. package/dist/config/codex-config.js +130 -106
  35. package/dist/config/index.d.ts +16 -2
  36. package/dist/config/index.js +23 -8
  37. package/dist/connector/index.js +6 -6
  38. package/dist/error-classification.js +3 -2
  39. package/dist/inbound/placeholder.d.ts +1 -1
  40. package/dist/inbound/placeholder.js +1 -1
  41. package/dist/index.js +387 -281
  42. package/dist/logger/index.js +6 -6
  43. package/dist/message-handlers.d.ts +0 -5
  44. package/dist/message-handlers.js +254 -237
  45. package/dist/model-catalog.d.ts +2 -2
  46. package/dist/model-catalog.js +3 -8
  47. package/dist/platform/identity.js +1 -1
  48. package/dist/platform/path.d.ts +1 -1
  49. package/dist/platform/path.js +1 -1
  50. package/dist/platform/sleep-blocker.js +1 -1
  51. package/dist/platform/terminator-win32.js +2 -1
  52. package/dist/router/commands/ls.d.ts +3 -0
  53. package/dist/router/commands/ls.js +256 -175
  54. package/dist/router/commands/order.js +184 -159
  55. package/dist/router/commands/resume.js +190 -152
  56. package/dist/router/commands/ws.js +219 -174
  57. package/dist/router/config/dsh.d.ts +4 -8
  58. package/dist/router/config/dsh.js +3 -10
  59. package/dist/router/config/kimi.js +1 -1
  60. package/dist/router/config/types.d.ts +1 -1
  61. package/dist/router/config-diff.js +1 -1
  62. package/dist/router/index.d.ts +56 -11
  63. package/dist/router/index.js +401 -391
  64. package/dist/runner/bash/runner.js +2 -4
  65. package/dist/runner/claude/session.d.ts +1 -1
  66. package/dist/runner/claude/session.js +6 -4
  67. package/dist/runner/codex/app-server/runner.d.ts +9 -2
  68. package/dist/runner/codex/app-server/runner.js +30 -25
  69. package/dist/runner/codex/app-server/translator.d.ts +2 -5
  70. package/dist/runner/codex/app-server/translator.js +291 -238
  71. package/dist/runner/common/acp/acp-approval.d.ts +1 -1
  72. package/dist/runner/common/acp/acp-approval.js +1 -1
  73. package/dist/runner/common/acp/base-acp-runner.d.ts +5 -2
  74. package/dist/runner/common/acp/base-acp-runner.js +26 -65
  75. package/dist/runner/common/acp/base-translator.d.ts +12 -2
  76. package/dist/runner/common/acp/base-translator.js +50 -26
  77. package/dist/runner/common/acp/protocol-types.d.ts +1 -1
  78. package/dist/runner/common/acp/protocol-types.js +2 -2
  79. package/dist/runner/common/connection-based-runner.d.ts +32 -1
  80. package/dist/runner/common/connection-based-runner.js +32 -2
  81. package/dist/runner/common/jsonl-stream.js +154 -187
  82. package/dist/runner/common/jsonrpc/client.d.ts +29 -0
  83. package/dist/runner/common/jsonrpc/client.js +43 -12
  84. package/dist/runner/common/jsonrpc/connection-manager.d.ts +2 -2
  85. package/dist/runner/common/jsonrpc/connection-manager.js +4 -4
  86. package/dist/runner/common/spawning-runner.d.ts +6 -1
  87. package/dist/runner/common/spawning-runner.js +9 -4
  88. package/dist/runner/common/turn-started.d.ts +8 -0
  89. package/dist/runner/common/turn-started.js +11 -0
  90. package/dist/runner/dsh/approval.js +84 -51
  91. package/dist/runner/dsh/catalog-probe.d.ts +17 -0
  92. package/dist/runner/dsh/catalog-probe.js +10 -0
  93. package/dist/runner/dsh/client.d.ts +7 -11
  94. package/dist/runner/dsh/client.js +356 -289
  95. package/dist/runner/dsh/exit-cancel.d.ts +2 -2
  96. package/dist/runner/dsh/exit-cancel.js +2 -2
  97. package/dist/runner/dsh/inflight.d.ts +1 -1
  98. package/dist/runner/dsh/inflight.js +1 -1
  99. package/dist/runner/dsh/mux-stream.js +28 -8
  100. package/dist/runner/dsh/runner.d.ts +44 -4
  101. package/dist/runner/dsh/runner.js +296 -199
  102. package/dist/runner/dsh/translator.d.ts +1 -31
  103. package/dist/runner/dsh/translator.js +180 -178
  104. package/dist/runner/index.d.ts +1 -1
  105. package/dist/runner/kimi/acp/runner.d.ts +1 -2
  106. package/dist/runner/kimi/acp/runner.js +82 -46
  107. package/dist/runner/kimi/acp/translator.d.ts +1 -1
  108. package/dist/runner/kimi/acp/translator.js +3 -4
  109. package/dist/runner/opencode/acp/error-monitor.js +5 -5
  110. package/dist/runner/opencode/acp/runner.d.ts +2 -2
  111. package/dist/runner/opencode/acp/runner.js +4 -4
  112. package/dist/runner/pi/rpc/client.js +4 -13
  113. package/dist/runner/pi/rpc/protocol-types.d.ts +1 -1
  114. package/dist/runner/pi/rpc/protocol-types.js +1 -1
  115. package/dist/runner/pi/rpc/runner.d.ts +3 -0
  116. package/dist/runner/pi/rpc/runner.js +27 -42
  117. package/dist/runner/pi/rpc/translator.js +61 -60
  118. package/dist/runner/types.d.ts +2 -2
  119. package/dist/runner/types.js +1 -1
  120. package/dist/session/claude/sessions.d.ts +45 -0
  121. package/dist/session/claude/sessions.js +160 -122
  122. package/dist/session/codex/rollout-reader.js +181 -182
  123. package/dist/session/common/throw-session-list-error.d.ts +10 -0
  124. package/dist/session/common/throw-session-list-error.js +16 -0
  125. package/dist/session/dsh/sessions.js +291 -235
  126. package/dist/session/kimi/sessions.d.ts +40 -8
  127. package/dist/session/kimi/sessions.js +373 -301
  128. package/dist/session/opencode/sessions.d.ts +142 -1
  129. package/dist/session/opencode/sessions.js +378 -321
  130. package/dist/session/pi/sessions.js +4 -5
  131. package/dist/session/session-store.d.ts +9 -1
  132. package/dist/session/session-store.js +99 -99
  133. package/dist/update/version-check.js +2 -1
  134. package/dist/workspace/index.d.ts +1 -1
  135. package/dist/workspace/index.js +1 -1
  136. package/package.json +1 -1
@@ -36,7 +36,7 @@ const SESSION_PANEL_TITLE_MAX = 42;
36
36
  export function sessionEventPanel(ev, index, totalEvents, tailExpandedCount = 2, agentKind = 'claude') {
37
37
  // reader 可以给事件一个"标题即摘要"的名字(工具调用:`🔧 bash — npm test`);
38
38
  // 缺省用类型标签。标题连同时间戳一起收进一行显示宽度——手机上折叠面板只有
39
- // 一行可用(2026-10-19 工具标题被长 URL 撑成两行那次)。
39
+ // 一行可用,长 URL 把标题撑成两行会断在 URL 中间。
40
40
  const label = ev.title ?? eventLabel(ev, agentKind);
41
41
  const ts = formatMonthDayClock(ev.timestamp);
42
42
  const suffix = ts ? ` (${ts})` : '';
@@ -103,7 +103,7 @@ export function parseJumpOffset(inputValue, pageSize) {
103
103
  * Build the CardKit 2.0 pagination block shared by /ls, /ws, /resume, /active,
104
104
  * /order.
105
105
  *
106
- * 第一性原理(2026-09-10 窄屏重设计):分页栏要同时承载「页码信息」和「翻页
106
+ * 第一性原理(窄屏):分页栏要同时承载「页码信息」和「翻页
107
107
  * 控件」两类需求。旧布局把两者塞进同一行 `column_set`(文案 weighted 4 : 输入
108
108
  * weighted 2),手机窄屏下 auto 按钮先占走固有宽度,剩余宽度按 4:2 切分后文案
109
109
  * 列只有百来像素、输入框只有几十像素——文案折行、输入框窄到没法点。
@@ -253,8 +253,8 @@ export function searchBar(opts) {
253
253
  *
254
254
  * 旧口径是 `events.slice(-max)`:按**原始事件条数**截尾。工具往返(bash 调用 →
255
255
  * job 登记 → 结果)一段就能把窗口占满,于是重返会话的用户真正需要的两条信息
256
- * ——最后一句我说了什么、最后一句它答了什么——被挤出窗口。2026-10-19 实测:
257
- * 自动恢复卡 5 个面板全是 bash 调用,一条用户消息、一条 assistant 正文都没有。
256
+ * ——最后一句我说了什么、最后一句它答了什么——被挤出窗口:自动恢复卡 5 个
257
+ * 面板全是 bash 调用,一条用户消息、一条 assistant 正文都没有。
258
258
  *
259
259
  * 契约:先取最新 `max` 条,再把最后一条 `user` 与最后一条 `assistant`(正文)
260
260
  * 不在窗口内时补进来——补位时淘汰最早的**非锚点**事件。输出保持时间顺序,
@@ -313,7 +313,7 @@ export function buildSessionHistoryCard(state, opts) {
313
313
  // (DSH 的 projections.title / claude 的 ai-title),后者是最近一条用户输入。
314
314
  // claude 的 displayTitle 本身就取 aiTitle,两者相等 → 保持旧的「AI 标题」
315
315
  // 单段文案;DSH 两者都有(标题 + 末轮 prompt)→ 各占一段,别把用户那句话
316
- // 挂在「AI 标题」下面(2026-10-21)。
316
+ // 挂在「AI 标题」下面。
317
317
  if (displayTitle) {
318
318
  if (aiTitle && aiTitle !== displayTitle) {
319
319
  sections.push(`🏷️ **标题**\n${preview(aiTitle)}`);
@@ -357,7 +357,7 @@ export function buildSessionHistoryCard(state, opts) {
357
357
  elements.push(markdownDiv(usageStr));
358
358
  }
359
359
  // Remove trailing hr
360
- if (elements.length > 0 && elements[elements.length - 1].tag === 'hr') {
360
+ if (elements[elements.length - 1].tag === 'hr') {
361
361
  elements.pop();
362
362
  }
363
363
  if (opts.actions && opts.actions.length > 0) {
@@ -2,7 +2,7 @@
2
2
  * 完整回答超出一张卡片时,落成 Markdown 文件随卡片发送。
3
3
  *
4
4
  * 为什么不再拆纯文本消息:拆出来的「完整回答(1/N)」与卡片正文重复
5
- * (2026-11-02 实机反馈:同一篇回答用户看到两遍),而且纯文本会丢表格、
5
+ * (同一篇回答用户看到两遍),而且纯文本会丢表格、
6
6
  * 代码块等 Markdown 结构。文件通道保留原文逐字不变,且只多一条文件消息。
7
7
  *
8
8
  * 调用方负责用后删除:`bestEffortUnlink()` 见 `common/fs.ts`。
@@ -55,7 +55,7 @@ type GroupContentPrepared = Map<BlockGroup, string>;
55
55
  /**
56
56
  * 状态行内容:**一行、一个 div、窄屏不换行**。
57
57
  *
58
- * 设计约束(2026-10-19 手机端实测):旧版把「状态标签」和「耗时/指标」放进
58
+ * 设计约束(手机端):旧版把「状态标签」和「耗时/指标」放进
59
59
  * `column_set` 的 auto + grow 两列,窄屏上 auto 列被挤到只剩两个字宽,
60
60
  * "运行中" 被拆成 "运行 / 中"。列式布局的可用宽度取决于对端渲染,服务端无法
61
61
  * 保证;所以这里改成单 div,段间用 " · " 分隔——换行只会发生在分隔符处,
@@ -64,8 +64,8 @@ type GroupContentPrepared = Map<BlockGroup, string>;
64
64
  * 状态标签(运行中/已完成…)不再重复:header 已经写了(`🔧 dsh · 调用工具`
65
65
  * / `✅ dsh · 已完成`),终态 summary 还有一次。
66
66
  *
67
- * 终态也保留状态行:耗时定格,DSH 的步数/上下文投影展示最后一次快照
68
- * (2026-10-21 用户要求)。summary 的最终 usage 与状态行并存。
67
+ * 终态也保留状态行:耗时定格,DSH 的步数/上下文投影展示最后一次快照。
68
+ * summary 的最终 usage 与状态行并存。
69
69
  *
70
70
  * 导出供窄屏守卫测试断言分段宽度(与 `estimateCardBytes` 同一惯例)。
71
71
  */
@@ -8,7 +8,7 @@ import { countCardElements, MAX_CARD_ELEMENTS } from './card-budget.js';
8
8
  import { displayWidth, truncateToDisplayWidth } from '../common/text-width.js';
9
9
  import { isContentFilterFailure } from '../common/turn-failure.js';
10
10
  import { formatClock, formatTimestamp } from './time.js';
11
- import { formatUsageStats } from './usage-format.js';
11
+ import { formatTokenK, formatUsageStats } from './usage-format.js';
12
12
  import { renderApprovalArea, renderApprovalAreaCompact } from './approval-render.js';
13
13
  const REASONING_BYTES = 4_500;
14
14
  const TEXT_BYTES = 10_000;
@@ -38,7 +38,7 @@ const EMPTY_MD_DIV_BYTES = measureJson(markdownDiv('', 'notation'));
38
38
  * 能力门控(compactSupported !== false)与执行侧 `'runCompact' in runner`
39
39
  * 鸭子探测对齐:claude 走 stream-json,不发射 turn_started,operationKind
40
40
  * 恒 undefined——若以 operationKind==='turn' 作门控,claude 终态卡永远拿
41
- * 不到按钮(2026-08-19 缺陷)。compaction 卡的「防递归」仍由 operationKind
41
+ * 不到按钮。compaction 卡的「防递归」仍由 operationKind
42
42
  * 保证,两者是不同质的判断。
43
43
  */
44
44
  function shouldShowCompactButton(state, options) {
@@ -121,37 +121,21 @@ function buildGoalSection(state) {
121
121
  * This unifies timestamp position across all block types (thinking/plan/file_change/tool/text).
122
122
  *
123
123
  * Panel title: `💬 **输出** (ts)` when timestamp present, `💬 **输出**` otherwise.
124
- * Running: expanded=true; completed: expanded=false (symmetric with thinking).
124
+ * The panel is always expanded — streaming and finalized states render identically.
125
125
  *
126
126
  * @param content - The text content to render
127
127
  * @param timestamp - Optional timestamp for the block
128
- * @param finalized - Whether the run is in a terminal state
129
128
  * @returns elements
130
129
  */
131
- function renderTextBlock(content, timestamp, finalized = false) {
130
+ function renderTextBlock(content, timestamp) {
132
131
  if (!content.trim()) {
133
132
  return [];
134
133
  }
135
134
  const ts = formatTimestamp(timestamp);
136
- // Terminal state: output is the card body, render in collapsible_panel with expanded=true.
135
+ // Render in collapsible_panel with expanded=true.
137
136
  // NOTE: collapsible_panel does NOT shield markdown tables from Feishu's 11310 table count
138
137
  // limit — tables inside lark_md text are still counted regardless of container.
139
138
  // We must explicitly truncate excess tables via truncateMarkdownTables before rendering.
140
- if (finalized) {
141
- const title = '💬 **输出**';
142
- const header = ts ? `${title} (${ts})` : title;
143
- const safeContent = truncateMarkdownTables(content);
144
- return [
145
- collapsibleMarkdownPanel({
146
- title: header,
147
- expanded: true, // Always expanded in finalized state for full visibility
148
- border: 'grey',
149
- content: safeContent,
150
- textSize: 'notation',
151
- }),
152
- ];
153
- }
154
- // Running/finalizing: panel provides visual grouping with timestamp in header
155
139
  const title = '💬 **输出**';
156
140
  const header = ts ? `${title} (${ts})` : title;
157
141
  const safeContent = truncateMarkdownTables(content);
@@ -302,7 +286,7 @@ function groupBlocks(blocks) {
302
286
  *
303
287
  * 早先试过"一行 + 服务端「详情」按钮":折叠不省字节所以正文不入 payload,展开
304
288
  * 只能回服务端重出卡。代价是手机上要多点一次、按钮还占掉标题宽度(长 URL 被挤
305
- * 成两行,2026-10-19 反馈)。现在改成折叠面板 + **按工具数量缩放的正文截断**:
289
+ * 成两行)。现在改成折叠面板 + **按工具数量缩放的正文截断**:
306
290
  * 正文始终随卡下发(面板折叠时不显示),点标题即展开,无往返、无按钮。
307
291
  *
308
292
  * 展开态:运行中/失败的默认展开(用户在等结果、错误要立刻可见),其余折叠。
@@ -493,8 +477,6 @@ function assembleRunCard(state, options, elements) {
493
477
  function insertAfterStatusRow(elements, extra) {
494
478
  if (extra.length === 0)
495
479
  return elements;
496
- if (elements.length === 0)
497
- return extra;
498
480
  return [elements[0], ...extra, ...elements.slice(1)];
499
481
  }
500
482
  /**
@@ -573,16 +555,13 @@ function buildFallbackElements(state, options, tier) {
573
555
  else if (group.kind === 'text') {
574
556
  const content = truncateUtf8(group.content, tier.textBytes, true, '…(已截断)\n');
575
557
  const ts = formatTimestamp(group.timestamp);
576
- elements.push(...renderTextBlock(content, ts, true));
558
+ elements.push(...renderTextBlock(content, ts));
577
559
  }
578
560
  else if (group.kind === 'tool') {
579
561
  // Show tool individually (each tool is independent now)
580
562
  elements.push(renderTool(group.tool, outputMax));
581
563
  }
582
564
  }
583
- if (elements.length === 0 && tier === EXTREME_TIER) {
584
- elements.push(markdownDiv('_暂无输出_'));
585
- }
586
565
  elements.push(...buildSummaryContent(state, { includeNotices: false }));
587
566
  elements.push(...actionRow(state, options));
588
567
  elements.push(...approvalArea(state));
@@ -606,7 +585,7 @@ function buildExtremeFallbackElements(state, options) {
606
585
  /**
607
586
  * 状态行内容:**一行、一个 div、窄屏不换行**。
608
587
  *
609
- * 设计约束(2026-10-19 手机端实测):旧版把「状态标签」和「耗时/指标」放进
588
+ * 设计约束(手机端):旧版把「状态标签」和「耗时/指标」放进
610
589
  * `column_set` 的 auto + grow 两列,窄屏上 auto 列被挤到只剩两个字宽,
611
590
  * "运行中" 被拆成 "运行 / 中"。列式布局的可用宽度取决于对端渲染,服务端无法
612
591
  * 保证;所以这里改成单 div,段间用 " · " 分隔——换行只会发生在分隔符处,
@@ -615,8 +594,8 @@ function buildExtremeFallbackElements(state, options) {
615
594
  * 状态标签(运行中/已完成…)不再重复:header 已经写了(`🔧 dsh · 调用工具`
616
595
  * / `✅ dsh · 已完成`),终态 summary 还有一次。
617
596
  *
618
- * 终态也保留状态行:耗时定格,DSH 的步数/上下文投影展示最后一次快照
619
- * (2026-10-21 用户要求)。summary 的最终 usage 与状态行并存。
597
+ * 终态也保留状态行:耗时定格,DSH 的步数/上下文投影展示最后一次快照。
598
+ * summary 的最终 usage 与状态行并存。
620
599
  *
621
600
  * 导出供窄屏守卫测试断言分段宽度(与 `estimateCardBytes` 同一惯例)。
622
601
  */
@@ -629,7 +608,7 @@ export function statusLine(state) {
629
608
  const elapsed = elapsedLabel(state);
630
609
  if (elapsed)
631
610
  parts.push(`⏱ ${elapsed}`);
632
- // 运行期投影在终态也保留(2026-10-21 用户要求):DSH 卡片完成时,耗时/步数/
611
+ // 运行期投影在终态也保留:DSH 卡片完成时,耗时/步数/
633
612
  // 上下文百分比是这一跑留下的最后快照,撤掉后用户只能从 summary 的累计口径
634
613
  // 重新推算。summary 仍展示最终 usage,两者口径不同、并存不冲突。
635
614
  const live = state.liveStatus;
@@ -644,12 +623,12 @@ export function statusLine(state) {
644
623
  parts.push(`📊 ~${pct}%`);
645
624
  }
646
625
  else if (live.contextUsed !== undefined) {
647
- parts.push(`📊 ${formatTokens(live.contextUsed)}`);
626
+ parts.push(`📊 ${formatTokenK(live.contextUsed)}`);
648
627
  }
649
628
  }
650
629
  if (parts.length === 0) {
651
630
  // 没有任何可用指标时退回状态词(骨架卡/测试构造的状态没有时间戳)。
652
- return statusTagLabel(state.terminal);
631
+ return terminalToLabel(state.terminal);
653
632
  }
654
633
  return parts.join(' · ');
655
634
  }
@@ -672,15 +651,6 @@ function elapsedLabel(state) {
672
651
  return `${hours}h${String(minutes % 60).padStart(2, '0')}m`;
673
652
  return `${Math.floor(hours / 24)}d${String(hours % 24).padStart(2, '0')}h`;
674
653
  }
675
- /** token 数量的紧凑写法:`950` / `12K` / `1.0M`。 */
676
- function formatTokens(tokens) {
677
- if (tokens < 1000)
678
- return String(tokens);
679
- // 千位边界取 M 档:999_500 起四舍五入会得到 "1000K",直接按 M 档展示。
680
- if (tokens < 999_500)
681
- return `${Math.round(tokens / 1000)}K`;
682
- return `${(tokens / 1_000_000).toFixed(1)}M`;
683
- }
684
654
  /**
685
655
  * 状态行元素(full/degraded/extreme/skeleton 共用)。
686
656
  *
@@ -955,7 +925,7 @@ export const FINAL_ANSWER_OVERFLOW_MARKER = '_最终回答较长,完整内容
955
925
  * 「卡片装不下正文」与「表格数超上限」是两件事,只有前者才走补发通道:
956
926
  * 飞书单卡最多 5 张 markdown 表(11310),多出来的最旧表格会被删掉并在原位
957
927
  * 留 `_💡 前 N 个表格已省略_`——正文仍在卡片里,再补发一遍就是把用户已经看到
958
- * 的整篇回答重复(2026-11-02 实机反馈)。
928
+ * 的整篇回答重复。
959
929
  */
960
930
  function buildFinalAnswerPriorityCard(state, options) {
961
931
  const finalAnswer = state.finalAnswer;
@@ -963,12 +933,12 @@ function buildFinalAnswerPriorityCard(state, options) {
963
933
  const prepared = prepareGroupContent(groups);
964
934
  const safeAnswer = truncateMarkdownTables(finalAnswer.content);
965
935
  const ts = formatTimestamp(finalAnswer.timestamp);
966
- const answerElements = renderTextBlock(safeAnswer, ts, state.terminal !== 'running');
936
+ const answerElements = renderTextBlock(safeAnswer, ts);
967
937
  const fixedStart = [statusRow(state), ...omittedBlocksHint(state)];
968
938
  const summary = buildSummaryContent(state, { includeNotices: false });
969
939
  const controls = [...actionRow(state, options), ...approvalArea(state)];
970
940
  const buildWithProgress = (start, includeSummary) => {
971
- const renderedProgress = buildAnswerProgressContent(state, prepared, groups.slice(start));
941
+ const renderedProgress = renderContentGroups(state, prepared, groups.slice(start));
972
942
  const progress = start < groups.length
973
943
  ? [markdownDiv(`_💡 已省略 ${start} 个早期步骤_`), ...renderedProgress]
974
944
  : renderedProgress;
@@ -1009,8 +979,17 @@ function buildFinalAnswerPriorityCard(state, options) {
1009
979
  finalAnswerText: finalAnswer.content,
1010
980
  };
1011
981
  }
1012
- /** Render a contiguous progress suffix for the answer-priority card. */
1013
- function buildAnswerProgressContent(state, prepared, visible) {
982
+ /**
983
+ * 正常路径内容循环,时间线卡(buildChronologicalContent)与答案优先卡的进度
984
+ * 后缀(buildFinalAnswerPriorityCard)共用:notices 作为普通消息按到达时间
985
+ * 插入内容流(有内容时不再沉到 summary),tool/text/panel 组逐个渲染。
986
+ *
987
+ * prepared 的键是组对象引用:传入的组数组必须保持元素引用(slice 可以,重建
988
+ * 数组会让 prepared.get 永远 miss,退化为二次截断)。面板组描述经
989
+ * describePanelGroup 与估算共享同一份模板。
990
+ * 「已省略 N 个早期步骤」提示与空态兜底 `_暂无输出_` 由调用方各自负责。
991
+ */
992
+ function renderContentGroups(state, prepared, visible) {
1014
993
  const elements = [];
1015
994
  const outputMax = toolOutputBudget(state);
1016
995
  const contentItems = mergeNoticesIntoContent(visible, state.notices);
@@ -1027,7 +1006,7 @@ function buildAnswerProgressContent(state, prepared, visible) {
1027
1006
  if (group.kind === 'text') {
1028
1007
  const content = prepared.get(group) ?? '';
1029
1008
  const ts = formatTimestamp(group.timestamp);
1030
- elements.push(...renderTextBlock(content, ts, state.terminal !== 'running'));
1009
+ elements.push(...renderTextBlock(content, ts));
1031
1010
  continue;
1032
1011
  }
1033
1012
  const spec = describePanelGroup(group, prepared);
@@ -1058,51 +1037,97 @@ function withinRunCardBudget(card) {
1058
1037
  }
1059
1038
  /** Build content in chronological order using groupBlocks to interleave thinking/text/tools. */
1060
1039
  function buildChronologicalContent(state, prepared, groups) {
1061
- const elements = [];
1062
- // finalized = 非 running(含 finalizing:主结果已出,thinking 折叠)
1063
- const finalized = state.terminal !== 'running';
1064
1040
  // 节点预算内从最新往回选择可见组:超限时只省略最旧的几步,不整卡降级。
1065
1041
  const { visible, omitted } = selectVisibleGroups(groups);
1066
- const outputMax = toolOutputBudget(state);
1042
+ const elements = [];
1067
1043
  if (omitted > 0)
1068
1044
  elements.push(markdownDiv(`_💡 已省略 ${omitted} 个早期步骤_`));
1069
- // renderRunCard 传入同一 groups 数组时直接复用(prepared 的键是组对象
1070
- // 引用,重建数组会让 prepared.get 永远 miss,退化为二次截断)。
1071
- // 面板组描述经 describePanelGroup 与估算共享同一份模板。
1072
- // notices 作为普通消息按到达时间插入内容流(有内容时不再沉到 summary)。
1073
- const contentItems = mergeNoticesIntoContent(visible, state.notices);
1074
- for (const item of contentItems) {
1075
- if (item.kind === 'notice') {
1076
- elements.push(renderNoticeRow(item.notice));
1077
- continue;
1078
- }
1079
- const group = item;
1080
- if (group.kind === 'tool') {
1081
- elements.push(renderTool(group.tool, outputMax));
1082
- continue;
1083
- }
1084
- if (group.kind === 'text') {
1085
- const content = prepared.get(group) ?? '';
1086
- const ts = formatTimestamp(group.timestamp);
1087
- elements.push(...renderTextBlock(content, ts, finalized));
1088
- continue;
1089
- }
1090
- const spec = describePanelGroup(group, prepared);
1091
- if (!spec)
1092
- continue;
1093
- elements.push(collapsibleMarkdownPanel({
1094
- title: spec.title,
1095
- expanded: spec.expanded,
1096
- border: spec.border,
1097
- content: spec.content,
1098
- textSize: 'notation',
1099
- }));
1100
- }
1045
+ elements.push(...renderContentGroups(state, prepared, visible));
1101
1046
  if (elements.length === 0) {
1102
1047
  elements.push(markdownDiv('_暂无输出_'));
1103
1048
  }
1104
1049
  return elements;
1105
1050
  }
1051
+ /** running 时不显示统计(仍在生成);原分支直接 return,不追加元素。 */
1052
+ function summaryRunning(_state, _elements) { }
1053
+ function summaryFinalizing(_state, elements) {
1054
+ elements.push(markdownDiv('⏳ **等待进程退出…**'));
1055
+ }
1056
+ function summaryError(state, elements) {
1057
+ // 服务方内容审核截断(多为误报)不是「程序出错」:照抄英文内部报文会让
1058
+ // 用户以为代码或配置坏了。文案说清三件事——谁拦的、改动还在、下一步。
1059
+ if (isContentFilterFailure(state.errorMsg)) {
1060
+ elements.push(markdownDiv('⚠️ **服务方内容审核拦截**\n\n' +
1061
+ '模型输出被上游安全策略截断(`content_filter`),多数是误报,不是代码或配置问题。\n' +
1062
+ '本回合已中止;已写入磁盘的改动都还在。重试仍被拦截时,可换模型或稍后再试。'));
1063
+ }
1064
+ else {
1065
+ elements.push(markdownDiv(`⚠️ **运行出错**\n\n${state.errorMsg ?? '未知错误'}`));
1066
+ }
1067
+ }
1068
+ function summaryInterrupted(state, elements) {
1069
+ // 中断来源三元:审批超时 / 用户取消审批 / 用户主动终止。
1070
+ // 显式原因优先:approvalExpired 是粘性标记(cancel 语义下 agent 可能继续跑),
1071
+ // 用户随后主动 /stop 时若仍按过期渲染,就把「我自己停的」误报成自动取消。
1072
+ const expiredApproval = state.interruptedReason === 'approval_timeout' ||
1073
+ (state.interruptedReason === undefined &&
1074
+ (state.approvalExpired === true || (state.approvals ?? []).some((a) => a.expired)));
1075
+ elements.push(markdownDiv(expiredApproval
1076
+ ? '⏰ **审批超时未响应,已自动取消**'
1077
+ : state.interruptedReason === 'approval_cancelled'
1078
+ ? '⏹ **已取消审批,任务终止**'
1079
+ : '⏹ **已被用户终止**'));
1080
+ }
1081
+ function summaryIdleTimeout(state, elements) {
1082
+ elements.push(markdownDiv(`⏱ **已超时**\n\n${state.idleTimeoutMinutes ?? 0} 分钟无响应,已自动终止`));
1083
+ }
1084
+ /** 成功/目标继续/其它终态:usage 统计 + 后台任务(原 else 兜底分支)。 */
1085
+ function summaryResult(state, elements) {
1086
+ const result = state.resultSubtype ?? 'success';
1087
+ const hasContent = state.blocks.length > 0 || !!state.finalAnswer?.content.trim();
1088
+ // 压缩(operationKind='compaction')不会返回 agent 正文——引擎只压缩上下文
1089
+ // 窗口,空内容属正常,不能沿用普通 run 的「(未返回内容)」异常信号。
1090
+ // 所有会压缩的 agent(codex/claude/kimi/opencode/pi)
1091
+ // 都经 streamCompact 走这条共享渲染路径,一处覆盖全部。
1092
+ const empty = hasContent
1093
+ ? ''
1094
+ : state.operationKind === 'compaction'
1095
+ ? '\n\n🗜 Compact 完成'
1096
+ : '\n\n(未返回内容)';
1097
+ // goal 本 run 读不到(known=false)时不能写「已完成」:这是 GoalStateEvent
1098
+ // 契约里明令禁止的推断(读不到 ≠ 没有目标)。run 本身成功,只是目标维度未知。
1099
+ const goalUnknown = state.terminal === 'done' && state.goalKnown === false;
1100
+ // RunState 的用量字段就是 formatUsageStats 的入参形态(同为
1101
+ // `UsageSnapshot`)——原先这里逐字段抄了 17 行,加一个字段要同时改
1102
+ // RunState、FinishMeta 和这张清单(§9.21)。
1103
+ const usageStatsStr = formatUsageStats(state, {
1104
+ showResult: true,
1105
+ result,
1106
+ // 本轮结束但目标继续:不能报"已完成",也不能说"出错"。
1107
+ ...(state.terminal === 'goal_continuing'
1108
+ ? { completedLabel: '⏳ 本轮结束 · 目标仍在继续' }
1109
+ : goalUnknown
1110
+ ? { completedLabel: '✅ 本轮完成 · 目标状态未确认' }
1111
+ : {}),
1112
+ });
1113
+ elements.push(markdownDiv(usageStatsStr + empty));
1114
+ // 后台任务:跨 turn 生命周期,必须显式告知并可逐个停止。
1115
+ elements.push(...buildBackgroundJobsSection(state));
1116
+ }
1117
+ /**
1118
+ * terminal → summary handler。用 Map 而非对象字面量(constructor/toString 等
1119
+ * 键会命中 Object.prototype);未登记的 terminal 走 summaryResult,与原 else
1120
+ * 兜底分支一致。
1121
+ */
1122
+ const SUMMARY_HANDLERS = new Map([
1123
+ ['running', summaryRunning],
1124
+ ['finalizing', summaryFinalizing],
1125
+ ['error', summaryError],
1126
+ ['interrupted', summaryInterrupted],
1127
+ ['idle_timeout', summaryIdleTimeout],
1128
+ ['done', summaryResult],
1129
+ ['goal_continuing', summaryResult],
1130
+ ]);
1106
1131
  /** Build summary tab content */
1107
1132
  function buildSummaryContent(state, options = {}) {
1108
1133
  const elements = [];
@@ -1114,74 +1139,8 @@ function buildSummaryContent(state, options = {}) {
1114
1139
  elements.push(renderNoticeRow(notice));
1115
1140
  }
1116
1141
  }
1117
- // running 时不显示统计(仍在生成);finalizing 显示等待提示
1118
- if (state.terminal === 'running') {
1119
- return elements;
1120
- }
1121
- if (state.terminal === 'finalizing') {
1122
- elements.push(markdownDiv('⏳ **等待进程退出…**'));
1123
- return elements;
1124
- }
1125
- if (state.terminal === 'error') {
1126
- // 服务方内容审核截断(多为误报)不是「程序出错」:照抄英文内部报文会让
1127
- // 用户以为代码或配置坏了。文案说清三件事——谁拦的、改动还在、下一步。
1128
- if (isContentFilterFailure(state.errorMsg)) {
1129
- elements.push(markdownDiv('⚠️ **服务方内容审核拦截**\n\n' +
1130
- '模型输出被上游安全策略截断(`content_filter`),多数是误报,不是代码或配置问题。\n' +
1131
- '本回合已中止;已写入磁盘的改动都还在。重试仍被拦截时,可换模型或稍后再试。'));
1132
- }
1133
- else {
1134
- elements.push(markdownDiv(`⚠️ **运行出错**\n\n${state.errorMsg ?? '未知错误'}`));
1135
- }
1136
- }
1137
- else if (state.terminal === 'interrupted') {
1138
- // 中断来源三元:审批超时 / 用户取消审批 / 用户主动终止。
1139
- // 显式原因优先:approvalExpired 是粘性标记(cancel 语义下 agent 可能继续跑),
1140
- // 用户随后主动 /stop 时若仍按过期渲染,就把「我自己停的」误报成自动取消。
1141
- const expiredApproval = state.interruptedReason === 'approval_timeout' ||
1142
- (state.interruptedReason === undefined &&
1143
- (state.approvalExpired === true || (state.approvals ?? []).some((a) => a.expired)));
1144
- elements.push(markdownDiv(expiredApproval
1145
- ? '⏰ **审批超时未响应,已自动取消**'
1146
- : state.interruptedReason === 'approval_cancelled'
1147
- ? '⏹ **已取消审批,任务终止**'
1148
- : '⏹ **已被用户终止**'));
1149
- }
1150
- else if (state.terminal === 'idle_timeout') {
1151
- elements.push(markdownDiv(`⏱ **已超时**\n\n${state.idleTimeoutMinutes ?? 0} 分钟无响应,已自动终止`));
1152
- }
1153
- else {
1154
- const result = state.resultSubtype ?? 'success';
1155
- const hasContent = state.blocks.length > 0 || !!state.finalAnswer?.content.trim();
1156
- // 压缩(operationKind='compaction')不会返回 agent 正文——引擎只压缩上下文
1157
- // 窗口,空内容属正常,不能沿用普通 run 的「(未返回内容)」异常信号
1158
- // (2026-09 用户反馈)。所有会压缩的 agent(codex/claude/kimi/opencode/pi)
1159
- // 都经 streamCompact 走这条共享渲染路径,一处覆盖全部。
1160
- const empty = hasContent
1161
- ? ''
1162
- : state.operationKind === 'compaction'
1163
- ? '\n\n🗜 Compact 完成'
1164
- : '\n\n(未返回内容)';
1165
- // goal 本 run 读不到(known=false)时不能写「已完成」:这是 GoalStateEvent
1166
- // 契约里明令禁止的推断(读不到 ≠ 没有目标)。run 本身成功,只是目标维度未知。
1167
- const goalUnknown = state.terminal === 'done' && state.goalKnown === false;
1168
- // RunState 的用量字段就是 formatUsageStats 的入参形态(同为
1169
- // `UsageSnapshot`)——原先这里逐字段抄了 17 行,加一个字段要同时改
1170
- // RunState、FinishMeta 和这张清单(§9.21)。
1171
- const usageStatsStr = formatUsageStats(state, {
1172
- showResult: true,
1173
- result,
1174
- // 本轮结束但目标继续:不能报"已完成",也不能说"出错"。
1175
- ...(state.terminal === 'goal_continuing'
1176
- ? { completedLabel: '⏳ 本轮结束 · 目标仍在继续' }
1177
- : goalUnknown
1178
- ? { completedLabel: '✅ 本轮完成 · 目标状态未确认' }
1179
- : {}),
1180
- });
1181
- elements.push(markdownDiv(usageStatsStr + empty));
1182
- // 后台任务:跨 turn 生命周期,必须显式告知并可逐个停止。
1183
- elements.push(...buildBackgroundJobsSection(state));
1184
- }
1142
+ const handler = SUMMARY_HANDLERS.get(state.terminal) ?? summaryResult;
1143
+ handler(state, elements);
1185
1144
  return elements;
1186
1145
  }
1187
1146
  /** 终态卡最多列几个后台任务(多出来的只报数)。 */
@@ -1243,7 +1202,7 @@ function codeFenceFor(content) {
1243
1202
  /**
1244
1203
  * 后台任务小节:命令独占一行,停止按钮单独一行。
1245
1204
  *
1246
- * 布局约束(2026-10-21 手机端反馈):
1205
+ * 布局约束(手机端):
1247
1206
  * - 命令 + 按钮不再并排(grow + auto):窄屏下 auto 列会被挤成「🟥…」,
1248
1207
  * 命令也被截断。按钮现在独占一个单列 column_set,宽度由按钮自身决定。
1249
1208
  * - 命令不 truncate:短命令/单行直接内联;中等长度的多行命令用代码块
@@ -1326,10 +1285,6 @@ function buildBackgroundJobsSection(state) {
1326
1285
  }
1327
1286
  return elements;
1328
1287
  }
1329
- /** Status tag label */
1330
- function statusTagLabel(terminal) {
1331
- return terminalToLabel(terminal);
1332
- }
1333
1288
  /** CardKit 2.0 header title — agent-aware via options.agentKind. */
1334
1289
  function headerTitle(state, options = {}) {
1335
1290
  const agent = agentDisplayName(options.agentKind ?? 'claude');
@@ -1359,7 +1314,7 @@ function headerTitle(state, options = {}) {
1359
1314
  if (state.terminal === 'finalizing')
1360
1315
  return `⏳ ${agent} · 完成中`;
1361
1316
  // 审批等待期间 server 暂停 turn,命令工具停在 tool_running——标题必须提示
1362
- // 正在等人工决策,而不是「调用工具」(2026-08-14 UX)。
1317
+ // 正在等人工决策,而不是「调用工具」。
1363
1318
  if (hasPendingApproval(state))
1364
1319
  return `✋ ${agent} · 等待审批`;
1365
1320
  if (state.footer === 'thinking')
@@ -154,7 +154,7 @@ export interface RunState extends UsageSnapshot {
154
154
  * 审批是否曾过期(桥侧 timer 触发,approval_expired 事件置位)。
155
155
  * 独立于 approvals 数组持久保存:cancel 送达后 server 会回
156
156
  * approval_resolved,把对应审批条目从数组移除,但过期原因必须活到
157
- * 终态渲染(2026-08-15 事故:标记被消费后卡片误报「已被用户终止」)。
157
+ * 终态渲染(否则标记被消费后卡片误报「已被用户终止」)。
158
158
  */
159
159
  approvalExpired?: boolean;
160
160
  /**
@@ -213,20 +213,6 @@ export declare function removeBackgroundJob(state: RunState, jobId: string): Run
213
213
  * 按钮,等于逼用户拿「停止」当查询用。
214
214
  */
215
215
  export declare function isLiveBackgroundJob(job: BackgroundJobRoster): boolean;
216
- /**
217
- * 收尾后把宿主推来的整份任务名单同步进终态卡状态。
218
- *
219
- * 与 reducer 的分工:reducer 处理的是"收尾那一刻的名单",只在 run 还在消费
220
- * 事件时有效;终态之后 `reduceRunState` 直接早退(终态幂等),名单只能由
221
- * 这里更新。
222
- *
223
- * 三条规则:
224
- * - 名单里消失 = 宿主已结算(`job/list` 是整份替换,摘掉即结束)→ `ended`;
225
- * - 名单里还在、宿主却给了终态词 → `ended`(有些宿主会把已结束的行留在名单里);
226
- * - `ended` 是终态,不回退成 running —— 同一个 job id 不会重新开始跑。
227
- *
228
- * 无变化时返回**同一个 state 引用**:bridge 靠它跳过无谓的卡片 patch。
229
- */
230
216
  export declare function syncBackgroundJobs(state: RunState, roster: ReadonlyArray<BackgroundJobRoster>): RunState;
231
217
  /**
232
218
  * 订阅断了但任务仍未结算:把在跑的任务标成「状态未知」。