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
@@ -7,9 +7,21 @@ import { type AgentKind } from '../runner/index.js';
7
7
  * wire up the registry (notably unit tests of pure render helpers).
8
8
  */
9
9
  export declare function agentDisplayName(kind: string): string;
10
- export { truncate } from '../common/truncate.js';
11
10
  export declare function terminalToLabel(terminal: string): string;
12
11
  export declare function terminalToColor(terminal: string): string;
12
+ /**
13
+ * Create a CardKit 2.0 button with a callback behavior. Shared constructor
14
+ * for every callback button in the card module (stop/new-session/compact/
15
+ * approval decisions/question options/…) — previously each render site
16
+ * hand-rolled the `{tag, text, type, behaviors}` object and they had drifted
17
+ * (plain_text tag sometimes present, sometimes omitted).
18
+ */
19
+ export declare function cardButton(text: string, value: Record<string, unknown>, opts?: {
20
+ type?: 'primary' | 'default' | 'danger';
21
+ size?: 'small' | 'medium' | 'large';
22
+ /** Fixed width, e.g. '100px' (keeps option-row buttons left-aligned). */
23
+ width?: string;
24
+ }): object;
13
25
  /**
14
26
  * Create a CardKit 2.0 stop button with behaviors callback.
15
27
  */
@@ -25,7 +25,6 @@ export function agentDisplayName(kind) {
25
25
  return 'Kimi';
26
26
  return kind;
27
27
  }
28
- export { truncate } from '../common/truncate.js';
29
28
  const TERMINAL_LABELS = {
30
29
  done: '已完成',
31
30
  error: '出错',
@@ -53,27 +52,34 @@ export function terminalToColor(terminal) {
53
52
  return 'red';
54
53
  }
55
54
  /**
56
- * Create a CardKit 2.0 stop button with behaviors callback.
55
+ * Create a CardKit 2.0 button with a callback behavior. Shared constructor
56
+ * for every callback button in the card module (stop/new-session/compact/
57
+ * approval decisions/question options/…) — previously each render site
58
+ * hand-rolled the `{tag, text, type, behaviors}` object and they had drifted
59
+ * (plain_text tag sometimes present, sometimes omitted).
57
60
  */
58
- export function stopButton(runId) {
61
+ export function cardButton(text, value, opts = {}) {
59
62
  return {
60
63
  tag: 'button',
61
- text: { content: '⏹ 停止' },
62
- type: 'danger',
63
- behaviors: [{ type: 'callback', value: { cmd: 'stop', runId } }],
64
+ text: { tag: 'plain_text', content: text },
65
+ ...(opts.type ? { type: opts.type } : {}),
66
+ ...(opts.size ? { size: opts.size } : {}),
67
+ ...(opts.width ? { width: opts.width } : {}),
68
+ behaviors: [{ type: 'callback', value }],
64
69
  };
65
70
  }
71
+ /**
72
+ * Create a CardKit 2.0 stop button with behaviors callback.
73
+ */
74
+ export function stopButton(runId) {
75
+ return cardButton('⏹ 停止', { cmd: 'stop', runId }, { type: 'danger' });
76
+ }
66
77
  /**
67
78
  * Create a CardKit 2.0 "new session" button with behaviors callback.
68
79
  * Standardized: ✨ 新会话 + primary type
69
80
  */
70
81
  export function newSessionButton() {
71
- return {
72
- tag: 'button',
73
- text: { content: '✨ 新会话' },
74
- type: 'primary',
75
- behaviors: [{ type: 'callback', value: { cmd: 'new-session' } }],
76
- };
82
+ return cardButton('✨ 新会话', { cmd: 'new-session' }, { type: 'primary' });
77
83
  }
78
84
  /**
79
85
  * Create a CardKit 2.0 Compact button for any runCompact-capable runner
@@ -82,12 +88,7 @@ export function newSessionButton() {
82
88
  * runId 解析 agentKind,claude 走 stream-json 内建 /compact)。
83
89
  */
84
90
  export function compactButton(runId) {
85
- return {
86
- tag: 'button',
87
- text: { content: '🗜 Compact' },
88
- type: 'primary',
89
- behaviors: [{ type: 'callback', value: { cmd: 'codex.compact', runId } }],
90
- };
91
+ return cardButton('🗜 Compact', { cmd: 'codex.compact', runId }, { type: 'primary' });
91
92
  }
92
93
  /**
93
94
  * Create a CardKit 2.0 Compact button for resume cards (auto-resume / `/resume <id>`).
@@ -98,19 +99,9 @@ export function compactButton(runId) {
98
99
  * non-default agent.
99
100
  */
100
101
  export function resumeCompactButton(sessionId, agent) {
101
- return {
102
- tag: 'button',
103
- text: { content: '🗜 Compact' },
104
- type: 'primary',
105
- behaviors: [{ type: 'callback', value: { cmd: 'resume.compact', sessionId, agent } }],
106
- };
102
+ return cardButton('🗜 Compact', { cmd: 'resume.compact', sessionId, agent }, { type: 'primary' });
107
103
  }
108
104
  export function resumeUseButton(sessionId, agent) {
109
- return {
110
- tag: 'button',
111
- text: { tag: 'plain_text', content: '🔁 切换到此会话' },
112
- type: 'primary',
113
- behaviors: [{ type: 'callback', value: { cmd: 'resume.use', sessionId, agent } }],
114
- };
105
+ return cardButton('🔁 切换到此会话', { cmd: 'resume.use', sessionId, agent }, { type: 'primary' });
115
106
  }
116
107
  //# sourceMappingURL=card-shared.js.map
@@ -1,8 +1,5 @@
1
1
  import type { RunState, ToolEntry } from './run-state.js';
2
2
  export interface RunCardRenderOptions {
3
- showThinking?: boolean;
4
- showToolUse?: boolean;
5
- showToolResult?: boolean;
6
3
  /**
7
4
  * Agent kind for the run card header title (e.g. "Claude · 思考中").
8
5
  * Defaults to 'claude' when not provided; the bridge passes `config.defaultAgent`.
@@ -42,25 +39,6 @@ type BlockGroup = {
42
39
  };
43
40
  /** 每个块的截断后内容(thinking/plan/text),测量与渲染共用(P1-2 建议项)。 */
44
41
  type GroupContentPrepared = Map<BlockGroup, string>;
45
- /**
46
- * 影子测量:精确复刻 normal 路径(buildChronologicalContent + renderRunCard 的
47
- * 骨架)产物 stringify 后的字节数,避免"先建完整卡再丢弃"的浪费。
48
- *
49
- * 第一性原理:估算应当测量"将要渲染的内容",而非近似它。
50
- * - 骨架(schema/config/header/statusRow/summary/buttons/spacer)用小对象实测
51
- * (measureJson),不依赖固定常数。
52
- * - 块内容经与渲染完全相同的变换后精确测量:truncateUtf8 截断 →
53
- * markdownDiv 的 escapeMarkdown(反斜杠 2×)→ JSON.stringify。因此 CJK
54
- * (3 字节/字符无转义)与 ASCII 高转义(\n/" 膨胀)双向精确,无需转义因子。
55
- * - tool 块直接 measureJson(renderTool(...))——与渲染共用同一函数,无复制。
56
- * - 块结构基于 groupBlocks(与渲染一致,连续 text 合并),保证合并语义一致。
57
- *
58
- * 唯一系统性低估:elements 数组元素间逗号/括号 ~n+1 字节(忽略,4KB 裕度内)。
59
- *
60
- * 注意:估算只是优化路径选择,不替代最终 stringify 兜底。即使估算说"低于阈值"
61
- * 走正常路径,renderRunCard 仍会 stringify 确认 ≤28KB;若估算低估,仍 fallback
62
- * 到 degraded(安全网不丢)。
63
- */
64
42
  /** 导出供测试断言估算精度(估算 ≈ 实际渲染字节,见 tests/anchor/run-card/run-card.test.ts)。 */
65
43
  export declare function estimateCardBytes(state: RunState, options?: RunCardRenderOptions, shared?: {
66
44
  groups?: BlockGroup[];
@@ -95,6 +95,58 @@ function renderTextBlock(content, timestamp, finalized = false) {
95
95
  }),
96
96
  ];
97
97
  }
98
+ /** 运行期通知作为普通消息渲染:与内容流同级的一行斜体提示。 */
99
+ function renderNoticeRow(notice) {
100
+ const icon = notice.level === 'info' ? 'ℹ️' : '⚠️';
101
+ return markdownDiv(`_${icon} ${notice.text}_`);
102
+ }
103
+ /** 内容组用于时间线插值的锚点时刻(与面板标题展示的时间戳一致)。 */
104
+ function groupTimeMs(group) {
105
+ if (group.kind === 'tool') {
106
+ const ts = group.tool.completedAt ?? group.tool.startedAt;
107
+ return ts ? Date.parse(ts) : undefined;
108
+ }
109
+ return group.timestamp ? Date.parse(group.timestamp) : undefined;
110
+ }
111
+ /**
112
+ * 把 notices 作为普通消息并入内容时间线:按到达时间插到对应内容组之间。
113
+ * 内容组自身的相对顺序不被改动(仍由 run-state 的块数组决定),时间戳只决定
114
+ * notice 的落点;无时间戳的 notice 无法定位,兜底置于内容流末尾。
115
+ */
116
+ function mergeNoticesIntoContent(groups, notices) {
117
+ if (!notices || notices.length === 0)
118
+ return groups;
119
+ const timed = [];
120
+ const untimed = [];
121
+ for (const notice of notices) {
122
+ if (notice.timestamp)
123
+ timed.push(notice);
124
+ else
125
+ untimed.push(notice);
126
+ }
127
+ const items = [];
128
+ let cursor = 0;
129
+ for (const notice of timed) {
130
+ const t = Date.parse(notice.timestamp);
131
+ if (!Number.isNaN(t)) {
132
+ while (cursor < groups.length &&
133
+ groupTimeMs(groups[cursor]) !== undefined &&
134
+ groupTimeMs(groups[cursor]) <= t) {
135
+ items.push(groups[cursor]);
136
+ cursor++;
137
+ }
138
+ }
139
+ items.push({ kind: 'notice', notice });
140
+ }
141
+ while (cursor < groups.length) {
142
+ items.push(groups[cursor]);
143
+ cursor++;
144
+ }
145
+ for (const notice of untimed) {
146
+ items.push({ kind: 'notice', notice });
147
+ }
148
+ return items;
149
+ }
98
150
  /**
99
151
  * 单次截断:把每个超限块(thinking/plan/text)的最终渲染内容算出来,供
100
152
  * estimateCardBytes 与 buildChronologicalContent 共用。此前影子测量与正式渲染
@@ -180,9 +232,9 @@ function groupBlocks(blocks) {
180
232
  * - Running tool: expanded (user can watch live)
181
233
  * - Completed tool: collapsed (click to inspect)
182
234
  */
183
- function renderTool(tool, finalized, showResult) {
235
+ function renderTool(tool, finalized) {
184
236
  const border = tool.status === 'error' ? 'red' : 'grey';
185
- const body = showResult ? toolBodyMd(tool) : toolBodyMd({ ...tool, output: undefined });
237
+ const body = toolBodyMd(tool);
186
238
  return collapsibleMarkdownPanel({
187
239
  title: toolTitle(tool),
188
240
  expanded: !finalized && tool.status === 'running',
@@ -208,8 +260,8 @@ const EXTREME_TIER = {
208
260
  toolKeep: EXTREME_TOOL_KEEP,
209
261
  textBytes: DEGRADED_TEXT_BYTES,
210
262
  };
211
- /** Bottom action row: stop (if running) + compact (if applicable) + new session (always). */
212
- function actionRow(state, options) {
263
+ /** Bottom action buttons: stop (if running) + compact (if applicable) + new session (always). */
264
+ function actionButtonList(state, options) {
213
265
  const actionButtons = [];
214
266
  // finalizing 也显示停止按钮(进程未退出,用户可 /stop)
215
267
  const showStop = state.terminal === 'running' || state.terminal === 'finalizing';
@@ -221,6 +273,11 @@ function actionRow(state, options) {
221
273
  actionButtons.push(compactButton(state.runId));
222
274
  }
223
275
  actionButtons.push(newSessionButton());
276
+ return actionButtons;
277
+ }
278
+ /** Bottom action row: spacer + column_set of actionButtonList (估算与渲染共用,W2.2)。 */
279
+ function actionRow(state, options) {
280
+ const actionButtons = actionButtonList(state, options);
224
281
  if (actionButtons.length === 0)
225
282
  return [];
226
283
  return [
@@ -235,7 +292,9 @@ function actionRow(state, options) {
235
292
  },
236
293
  ];
237
294
  }
238
- /** 审批区位于 body 最底部(底部操作行之后):待审批时决策按钮不遮挡内容流。 */
295
+ /** 审批区位于 body 最底部(底部操作行之后):待审批时决策按钮不遮挡内容流。
296
+ * 例外:skeleton 兜底路径中 approvalArea 在 actionRow 之前(信息保真 C4.3),
297
+ * 保证极端降级时审批入口与 stop/new-session 按钮同时可见。 */
239
298
  function approvalArea(state) {
240
299
  const elements = [];
241
300
  for (const slot of state.approvals ?? []) {
@@ -247,6 +306,12 @@ function approvalArea(state) {
247
306
  }
248
307
  return elements;
249
308
  }
309
+ /** 底部操作行之前的内容区提示:被 MAX_BLOCKS 截掉的早期块计数(信息保真 C4.1)。 */
310
+ function omittedBlocksHint(state) {
311
+ return state.omittedBlocks !== undefined && state.omittedBlocks > 0
312
+ ? [markdownDiv(`_💡 已省略 ${state.omittedBlocks} 个早期步骤_`)]
313
+ : [];
314
+ }
250
315
  /** Assemble the card shell (schema/config/header) around body elements. */
251
316
  function assembleRunCard(state, options, elements) {
252
317
  return {
@@ -267,6 +332,7 @@ function assembleRunCard(state, options, elements) {
267
332
  function buildFallbackElements(state, options, tier) {
268
333
  const elements = [];
269
334
  elements.push(statusRow(state));
335
+ elements.push(...omittedBlocksHint(state));
270
336
  // 单遍分桶:thinking / tool(保持原相对顺序)
271
337
  const { thinking: allThinkingBlocks, tool: allToolBlocks } = bucketThinkingAndTool(state.blocks);
272
338
  if (allThinkingBlocks.length > tier.thinkingKeep) {
@@ -298,11 +364,26 @@ function buildFallbackElements(state, options, tier) {
298
364
  }
299
365
  return true; // keep all non-thinking, non-tool blocks
300
366
  });
301
- // Render thinking/text/tool in chronological order using groupBlocks
302
- for (const group of groupBlocks(filteredBlocks)) {
367
+ // 信息保真 C4.2:plan/file_change 块在降级路径不渲染内容(正常路径已可见,
368
+ // 降级预算紧张),但必须计数留痕,禁止静默丢弃。
369
+ // 计数在渲染前完成,提示与 thinking/tool 省略提示一并置于卡片顶部——不能等
370
+ // 内容循环结束再追加,否则提示会落在最后一条输出之后,打乱「输出收尾」观感。
371
+ const groupsToRender = groupBlocks(filteredBlocks);
372
+ const omittedPlanOrFileChange = groupsToRender.filter((g) => g.kind === 'plan' || g.kind === 'file_change').length;
373
+ if (omittedPlanOrFileChange > 0) {
374
+ elements.push(markdownDiv(`_💡 另外 ${omittedPlanOrFileChange} 个计划/文件变更已省略_`));
375
+ }
376
+ // notices 作为普通消息按时间插入内容流(与正常路径同语义);降级只裁剪
377
+ // 内容块,不裁剪运行期通知。
378
+ const contentItems = mergeNoticesIntoContent(groupsToRender, state.notices);
379
+ // Render thinking/text/tool/notice in chronological order
380
+ for (const item of contentItems) {
381
+ if (item.kind === 'notice') {
382
+ elements.push(renderNoticeRow(item.notice));
383
+ continue;
384
+ }
385
+ const group = item;
303
386
  if (group.kind === 'thinking') {
304
- if (options.showThinking === false)
305
- continue;
306
387
  const ts = formatTimestamp(group.timestamp);
307
388
  const title = '💭 **思考完成**';
308
389
  const content = truncateUtf8(group.content, tier.thinkingBytes);
@@ -322,15 +403,13 @@ function buildFallbackElements(state, options, tier) {
322
403
  }
323
404
  else if (group.kind === 'tool') {
324
405
  // Show tool individually (each tool is independent now)
325
- if (options.showToolUse === false)
326
- continue;
327
- elements.push(renderTool(group.tool, true, options.showToolResult !== false)); // finalized=true
406
+ elements.push(renderTool(group.tool, true)); // finalized=true
328
407
  }
329
408
  }
330
409
  if (elements.length === 0 && tier === EXTREME_TIER) {
331
410
  elements.push(markdownDiv('_暂无输出_'));
332
411
  }
333
- elements.push(...buildSummaryContent(state));
412
+ elements.push(...buildSummaryContent(state, { includeNotices: false }));
334
413
  elements.push(...actionRow(state, options));
335
414
  elements.push(...approvalArea(state));
336
415
  return elements;
@@ -388,6 +467,9 @@ function buildSkeletonElements(state, options) {
388
467
  elements.push(markdownDiv('_⚠️ 输出过大且含大量转义字符,已省略全部内容以避免超限;完整内容请查日志_'));
389
468
  // Summary (token stats etc.) — static, bounded size
390
469
  elements.push(...buildSummaryContent(state));
470
+ // 信息保真 C4.3:skeleton 也渲染审批区——极端兜底不丢审批入口
471
+ //(待审批被静默吞掉 = 用户永远看不到审批请求)。
472
+ elements.push(...approvalArea(state));
391
473
  // Bottom action row: stop (if running/finalizing) + compact (if applicable) + new session (always).
392
474
  // Degraded paths must keep the action buttons reachable (design constraint).
393
475
  elements.push(...actionRow(state, options));
@@ -431,85 +513,67 @@ function bucketThinkingAndTool(blocks) {
431
513
  * 走正常路径,renderRunCard 仍会 stringify 确认 ≤28KB;若估算低估,仍 fallback
432
514
  * 到 degraded(安全网不丢)。
433
515
  */
516
+ /**
517
+ * W2.2 镜像对单源:thinking/plan/file_change 三个 collapsible 面板组的
518
+ * { title, expanded, border, content } 描述,估算(measurePanelBytes)与渲染
519
+ * (collapsibleMarkdownPanel)共用——此前同一套标题/opIcon/内容模板写两遍。
520
+ * text(renderTextBlock)与 tool(renderTool)形态特殊,由调用方各自处理;
521
+ * 返回 null 表示该组不是 collapsible 面板。
522
+ */
523
+ function describePanelGroup(group, prepared) {
524
+ if (group.kind === 'tool')
525
+ return null;
526
+ const ts = formatTimestamp(group.timestamp);
527
+ const suffix = ts ? ` (${ts})` : '';
528
+ if (group.kind === 'thinking') {
529
+ const title = (group.active ? '💭 **思考中**' : '💭 **思考完成**') + suffix;
530
+ return { title, expanded: group.active, border: 'grey', content: prepared.get(group) ?? '' };
531
+ }
532
+ if (group.kind === 'plan') {
533
+ const title = (group.active ? '📋 **执行计划**' : '📋 **计划完成**') + suffix;
534
+ return { title, expanded: group.active, border: 'blue', content: prepared.get(group) ?? '' };
535
+ }
536
+ if (group.kind === 'file_change') {
537
+ const opIcon = group.operation === 'create'
538
+ ? '🆕'
539
+ : group.operation === 'edit'
540
+ ? '✏️'
541
+ : group.operation === 'delete'
542
+ ? '🗑️'
543
+ : '📖';
544
+ const title = `${opIcon} **文件改动**${suffix}`;
545
+ const content = group.diff
546
+ ? `**${group.path}**\n\n\`\`\`\n${group.diff}\n\`\`\``
547
+ : `**${group.path}** (${group.operation})`;
548
+ return { title, expanded: false, border: 'grey', content };
549
+ }
550
+ return null;
551
+ }
434
552
  /** 导出供测试断言估算精度(估算 ≈ 实际渲染字节,见 tests/anchor/run-card/run-card.test.ts)。 */
435
553
  export function estimateCardBytes(state, options = {}, shared) {
436
554
  const finalized = state.terminal !== 'running';
437
- const showResult = options.showToolResult !== false;
438
- const showThinking = options.showThinking !== false;
439
- const showToolUse = options.showToolUse !== false;
440
555
  // P1-2:renderRunCard 传入共享 groups/prepared 时直接复用(截断只做一次);
441
556
  // 独立调用(测试/预算探针)时自建,行为与原先一致。
442
557
  const groups = shared?.groups ?? groupBlocks(state.blocks);
443
558
  const prepared = shared?.prepared ?? prepareGroupContent(groups);
444
- // 卡片外壳 + 状态行 + summary(小对象实测,动态文案精确)
445
- let total = measureJson({
446
- schema: '2.0',
447
- config: { wide_screen_mode: true, update_multi: true },
448
- header: {
449
- template: headerTemplate2(state),
450
- title: { content: headerTitle2(state, options), tag: 'plain_text' },
451
- },
452
- body: { elements: [] },
453
- }) +
559
+ // 卡片外壳直接实测 assembleRunCard 空元素产物(schema/config/header/body 结构
560
+ // 与正式渲染完全一致,W2.2);status/summary/操作行同样实测渲染结构。
561
+ // 注:审批区(approvalArea)不参与估算——待审批通常在途且体积小,超预算由
562
+ // renderRunCard 的 stringify 兜底捕获。
563
+ let total = measureJson(assembleRunCard(state, options, [])) +
454
564
  measureJson(statusRow(state)) +
455
- measureJson(buildSummaryContent(state));
456
- // 底部操作行:spacer + 操作按钮(与 renderRunCard normal 分支一致)
457
- const actionButtons = [];
458
- if (state.terminal === 'running' || state.terminal === 'finalizing') {
459
- actionButtons.push(stopButton(state.runId));
460
- }
461
- if (shouldShowCompactButton(state, options)) {
462
- actionButtons.push(compactButton(state.runId));
463
- }
464
- actionButtons.push(newSessionButton());
465
- total += measureJson([
466
- { tag: 'div', text: { content: '‎', tag: 'lark_md' } },
467
- {
468
- tag: 'column_set',
469
- columns: actionButtons.map((btn) => ({
470
- tag: 'column',
471
- width: 'auto',
472
- elements: [btn],
473
- })),
474
- },
475
- ]);
476
- // 块内容:与 buildChronologicalContent 同源(groupBlocks 合并语义一致)
565
+ measureJson(buildSummaryContent(state, { includeNotices: false })) +
566
+ measureJson(actionRow(state, options));
567
+ // 块内容:与 buildChronologicalContent 同源(describePanelGroup 单源,W2.2)
477
568
  let renderedAny = false;
478
569
  for (const group of groups) {
479
- if (group.kind === 'thinking') {
480
- if (!showThinking)
481
- continue;
570
+ if (group.kind === 'tool') {
482
571
  renderedAny = true;
483
- const ts = formatTimestamp(group.timestamp);
484
- const title = group.active ? '💭 **思考中**' : '💭 **思考完成**';
485
- const header = ts ? `${title} (${ts})` : title;
486
- total += measurePanelBytes(header, group.active, 'grey', prepared.get(group) ?? '');
487
- }
488
- else if (group.kind === 'plan') {
489
- renderedAny = true;
490
- const ts = formatTimestamp(group.timestamp);
491
- const title = group.active ? '📋 **执行计划**' : '📋 **计划完成**';
492
- const header = ts ? `${title} (${ts})` : title;
493
- total += measurePanelBytes(header, group.active, 'blue', prepared.get(group) ?? '');
494
- }
495
- else if (group.kind === 'file_change') {
496
- renderedAny = true;
497
- const ts = formatTimestamp(group.timestamp);
498
- const opIcon = group.operation === 'create'
499
- ? '🆕'
500
- : group.operation === 'edit'
501
- ? '✏️'
502
- : group.operation === 'delete'
503
- ? '🗑️'
504
- : '📖';
505
- const title = `${opIcon} **文件改动**`;
506
- const content = group.diff
507
- ? `**${group.path}**\n\n\`\`\`\n${group.diff}\n\`\`\``
508
- : `**${group.path}** (${group.operation})`;
509
- const header = ts ? `${title} (${ts})` : title;
510
- total += measurePanelBytes(header, false, 'grey', content);
572
+ // tool 直接实测真实渲染元素(复用 renderTool,output ≤1200 字符,物化廉价)
573
+ total += measureJson(renderTool(group.tool, finalized));
574
+ continue;
511
575
  }
512
- else if (group.kind === 'text') {
576
+ if (group.kind === 'text') {
513
577
  const content = prepared.get(group) ?? '';
514
578
  if (!content.trim())
515
579
  continue;
@@ -517,14 +581,19 @@ export function estimateCardBytes(state, options = {}, shared) {
517
581
  const ts = formatTimestamp(group.timestamp);
518
582
  const header = `💬 **输出**${ts ? ` (${ts})` : ''}`;
519
583
  total += measurePanelBytes(header, true, 'grey', content);
584
+ continue;
520
585
  }
521
- else if (group.kind === 'tool') {
522
- if (!showToolUse)
523
- continue;
524
- renderedAny = true;
525
- // tool 直接实测真实渲染元素(复用 renderTool,output ≤1200 字符,物化廉价)
526
- total += measureJson(renderTool(group.tool, finalized, showResult));
527
- }
586
+ const spec = describePanelGroup(group, prepared);
587
+ if (!spec)
588
+ continue;
589
+ renderedAny = true;
590
+ total += measurePanelBytes(spec.title, spec.expanded, spec.border, spec.content);
591
+ }
592
+ // notices 在正常路径作为普通消息内联渲染(与 buildChronologicalContent 同源,
593
+ // renderNoticeRow 单源);行体积与位置无关,直接按条累加。
594
+ for (const notice of state.notices ?? []) {
595
+ renderedAny = true;
596
+ total += measureJson(renderNoticeRow(notice));
528
597
  }
529
598
  if (!renderedAny) {
530
599
  total += measureJson(markdownDiv('_暂无输出_'));
@@ -560,8 +629,9 @@ export function renderRunCard(state, options = {}) {
560
629
  if (estimate < DEGRADED_THRESHOLD) {
561
630
  const elements = [
562
631
  statusRow(state),
563
- ...buildChronologicalContent(state, options, prepared, groups),
564
- ...buildSummaryContent(state),
632
+ ...omittedBlocksHint(state),
633
+ ...buildChronologicalContent(state, prepared, groups),
634
+ ...buildSummaryContent(state, { includeNotices: false }),
565
635
  ...actionRow(state, options),
566
636
  ...approvalArea(state),
567
637
  ];
@@ -599,77 +669,41 @@ export function renderRunCard(state, options = {}) {
599
669
  return truncatedSkeleton;
600
670
  }
601
671
  /** Build content in chronological order using groupBlocks to interleave thinking/text/tools. */
602
- function buildChronologicalContent(state, options, prepared, groups) {
672
+ function buildChronologicalContent(state, prepared, groups) {
603
673
  const elements = [];
604
674
  // finalized = 非 running(含 finalizing:主结果已出,thinking 折叠)
605
675
  const finalized = state.terminal !== 'running';
606
- const showResult = options.showToolResult !== false;
607
676
  // P1-2:renderRunCard 传入同一 groups 数组时直接复用(prepared 的键是组对象
608
677
  // 引用,重建数组会让 prepared.get 永远 miss,退化为二次截断)。
609
- for (const group of groups ?? groupBlocks(state.blocks)) {
610
- if (group.kind === 'thinking') {
611
- if (options.showThinking === false)
612
- continue;
613
- const ts = formatTimestamp(group.timestamp);
614
- const title = group.active ? '💭 **思考中**' : '💭 **思考完成**';
615
- const content = prepared?.get(group) ?? truncateUtf8(group.content, REASONING_BYTES);
616
- const header = ts ? `${title} (${ts})` : title;
617
- elements.push(collapsibleMarkdownPanel({
618
- title: header,
619
- expanded: group.active,
620
- border: 'grey',
621
- content,
622
- textSize: 'notation',
623
- }));
624
- }
625
- else if (group.kind === 'plan') {
626
- // Plan blocks: render as collapsible panel (default collapsed)
627
- const ts = formatTimestamp(group.timestamp);
628
- const title = group.active ? '📋 **执行计划**' : '📋 **计划完成**';
629
- const content = prepared?.get(group) ?? truncateUtf8(group.content, REASONING_BYTES * 2);
630
- const header = ts ? `${title} (${ts})` : title;
631
- elements.push(collapsibleMarkdownPanel({
632
- title: header,
633
- expanded: group.active, // expand while active, collapse when done
634
- border: 'blue',
635
- content,
636
- textSize: 'notation',
637
- }));
678
+ // W2.2:面板组描述经 describePanelGroup 与估算共享同一份模板。
679
+ // notices 作为普通消息按到达时间插入内容流(有内容时不再沉到 summary)。
680
+ const contentItems = mergeNoticesIntoContent(groups, state.notices);
681
+ for (const item of contentItems) {
682
+ if (item.kind === 'notice') {
683
+ elements.push(renderNoticeRow(item.notice));
684
+ continue;
638
685
  }
639
- else if (group.kind === 'file_change') {
640
- // File change blocks: render as collapsible panel (default collapsed)
641
- const ts = formatTimestamp(group.timestamp);
642
- const opIcon = group.operation === 'create'
643
- ? '🆕'
644
- : group.operation === 'edit'
645
- ? '✏️'
646
- : group.operation === 'delete'
647
- ? '🗑️'
648
- : '📖';
649
- const title = `${opIcon} **文件改动**`;
650
- const content = group.diff
651
- ? `**${group.path}**\n\n\`\`\`\n${group.diff}\n\`\`\``
652
- : `**${group.path}** (${group.operation})`;
653
- const header = ts ? `${title} (${ts})` : title;
654
- elements.push(collapsibleMarkdownPanel({
655
- title: header,
656
- expanded: false,
657
- border: 'grey',
658
- content,
659
- textSize: 'notation',
660
- }));
686
+ const group = item;
687
+ if (group.kind === 'tool') {
688
+ elements.push(renderTool(group.tool, finalized));
689
+ continue;
661
690
  }
662
- else if (group.kind === 'text') {
663
- const content = prepared?.get(group) ?? truncateUtf8(group.content, TEXT_BYTES, true, '…(已截断)\n');
691
+ if (group.kind === 'text') {
692
+ const content = prepared.get(group) ?? '';
664
693
  const ts = formatTimestamp(group.timestamp);
665
- const els = renderTextBlock(content, ts, finalized);
666
- elements.push(...els);
667
- }
668
- else if (group.kind === 'tool') {
669
- if (options.showToolUse === false)
670
- continue;
671
- elements.push(renderTool(group.tool, finalized, showResult));
694
+ elements.push(...renderTextBlock(content, ts, finalized));
695
+ continue;
672
696
  }
697
+ const spec = describePanelGroup(group, prepared);
698
+ if (!spec)
699
+ continue;
700
+ elements.push(collapsibleMarkdownPanel({
701
+ title: spec.title,
702
+ expanded: spec.expanded,
703
+ border: spec.border,
704
+ content: spec.content,
705
+ textSize: 'notation',
706
+ }));
673
707
  }
674
708
  if (elements.length === 0) {
675
709
  elements.push(markdownDiv('_暂无输出_'));
@@ -677,8 +711,16 @@ function buildChronologicalContent(state, options, prepared, groups) {
677
711
  return elements;
678
712
  }
679
713
  /** Build summary tab content */
680
- function buildSummaryContent(state) {
714
+ function buildSummaryContent(state, options = {}) {
681
715
  const elements = [];
716
+ // 运行期通知默认仍走 summary(skeleton 兜底:内容区整体缺失时唯一可见位置)。
717
+ // 正常/degraded/extreme 路径传入 includeNotices:false——通知已作为普通消息
718
+ // 按时间线内联渲染(renderNoticeRow),避免同一通知渲染两遍。
719
+ if (options.includeNotices !== false) {
720
+ for (const notice of state.notices ?? []) {
721
+ elements.push(renderNoticeRow(notice));
722
+ }
723
+ }
682
724
  // running 时不显示统计(仍在生成);finalizing 显示等待提示
683
725
  if (state.terminal === 'running') {
684
726
  return elements;
@@ -710,7 +752,15 @@ function buildSummaryContent(state) {
710
752
  else {
711
753
  const result = state.resultSubtype ?? 'success';
712
754
  const hasContent = state.blocks.length > 0;
713
- const empty = !hasContent ? '\n\n(未返回内容)' : '';
755
+ // 压缩(operationKind='compaction')不会返回 agent 正文——引擎只压缩上下文
756
+ // 窗口,空内容属正常,不能沿用普通 run 的「(未返回内容)」异常信号
757
+ // (2026-09 用户反馈)。所有会压缩的 agent(codex/claude/kimi/opencode/pi)
758
+ // 都经 streamCodexCompact 走这条共享渲染路径,一处覆盖全部。
759
+ const empty = hasContent
760
+ ? ''
761
+ : state.operationKind === 'compaction'
762
+ ? '\n\n🗜 Compact 完成'
763
+ : '\n\n(未返回内容)';
714
764
  const usageStatsStr = formatUsageStats({
715
765
  contextLength: state.contextLength,
716
766
  contextLimit: state.contextLimit,
@@ -726,6 +776,9 @@ function buildSummaryContent(state) {
726
776
  cumulativeOutputTokens: state.cumulativeOutputTokens,
727
777
  cumulativeCacheReadTokens: state.cumulativeCacheReadTokens,
728
778
  cumulativeCacheCreationTokens: state.cumulativeCacheCreationTokens,
779
+ model: state.model,
780
+ costUsd: state.costUsd,
781
+ reasoningTokens: state.reasoningTokens,
729
782
  }, { showResult: true, result });
730
783
  elements.push(markdownDiv(usageStatsStr + empty));
731
784
  }