lark-remote 0.1.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 (201) hide show
  1. package/CHANGELOG.md +52 -0
  2. package/LICENSE +21 -0
  3. package/README.en.md +162 -0
  4. package/README.md +161 -0
  5. package/dist/binder.d.ts +57 -0
  6. package/dist/binder.js +105 -0
  7. package/dist/bridge/index.d.ts +338 -0
  8. package/dist/bridge/index.js +1281 -0
  9. package/dist/bridge/queue-manager.d.ts +277 -0
  10. package/dist/bridge/queue-manager.js +729 -0
  11. package/dist/card/bash-card-session.d.ts +78 -0
  12. package/dist/card/bash-card-session.js +174 -0
  13. package/dist/card/bash-renderer.d.ts +24 -0
  14. package/dist/card/bash-renderer.js +187 -0
  15. package/dist/card/card-budget.d.ts +31 -0
  16. package/dist/card/card-budget.js +280 -0
  17. package/dist/card/card-session.d.ts +83 -0
  18. package/dist/card/card-session.js +185 -0
  19. package/dist/card/card-shared.d.ts +28 -0
  20. package/dist/card/card-shared.js +99 -0
  21. package/dist/card/collapsible.d.ts +26 -0
  22. package/dist/card/collapsible.js +87 -0
  23. package/dist/card/run-card-session.d.ts +78 -0
  24. package/dist/card/run-card-session.js +165 -0
  25. package/dist/card/run-renderer.d.ts +66 -0
  26. package/dist/card/run-renderer.js +841 -0
  27. package/dist/card/run-state.d.ts +103 -0
  28. package/dist/card/run-state.js +313 -0
  29. package/dist/card/text-truncate.d.ts +56 -0
  30. package/dist/card/text-truncate.js +184 -0
  31. package/dist/card/time.d.ts +1 -0
  32. package/dist/card/time.js +14 -0
  33. package/dist/card/tool-render.d.ts +25 -0
  34. package/dist/card/tool-render.js +176 -0
  35. package/dist/cli.d.ts +28 -0
  36. package/dist/cli.js +89 -0
  37. package/dist/common/guards.d.ts +37 -0
  38. package/dist/common/guards.js +65 -0
  39. package/dist/config/codex-config.d.ts +113 -0
  40. package/dist/config/codex-config.js +428 -0
  41. package/dist/config/dir.d.ts +17 -0
  42. package/dist/config/dir.js +81 -0
  43. package/dist/config/index.d.ts +219 -0
  44. package/dist/config/index.js +388 -0
  45. package/dist/config/kimi-config.d.ts +33 -0
  46. package/dist/config/kimi-config.js +111 -0
  47. package/dist/config/opencode-config.d.ts +20 -0
  48. package/dist/config/opencode-config.js +124 -0
  49. package/dist/config/pi-config.d.ts +49 -0
  50. package/dist/config/pi-config.js +151 -0
  51. package/dist/config/wizard.d.ts +12 -0
  52. package/dist/config/wizard.js +84 -0
  53. package/dist/connector/file-limits.d.ts +7 -0
  54. package/dist/connector/file-limits.js +8 -0
  55. package/dist/connector/index.d.ts +86 -0
  56. package/dist/connector/index.js +340 -0
  57. package/dist/error-classification.d.ts +15 -0
  58. package/dist/error-classification.js +24 -0
  59. package/dist/index.d.ts +1 -0
  60. package/dist/index.js +580 -0
  61. package/dist/instance-lock.d.ts +30 -0
  62. package/dist/instance-lock.js +184 -0
  63. package/dist/logger/index.d.ts +57 -0
  64. package/dist/logger/index.js +178 -0
  65. package/dist/order/index.d.ts +27 -0
  66. package/dist/order/index.js +90 -0
  67. package/dist/persistence/atomic-write.d.ts +10 -0
  68. package/dist/persistence/atomic-write.js +65 -0
  69. package/dist/persistence/load-json-file.d.ts +9 -0
  70. package/dist/persistence/load-json-file.js +23 -0
  71. package/dist/restart.d.ts +24 -0
  72. package/dist/restart.js +73 -0
  73. package/dist/router/card-helpers.d.ts +28 -0
  74. package/dist/router/card-helpers.js +65 -0
  75. package/dist/router/config/claude.d.ts +14 -0
  76. package/dist/router/config/claude.js +72 -0
  77. package/dist/router/config/codex.d.ts +22 -0
  78. package/dist/router/config/codex.js +100 -0
  79. package/dist/router/config/common/render.d.ts +16 -0
  80. package/dist/router/config/common/render.js +158 -0
  81. package/dist/router/config/index.d.ts +18 -0
  82. package/dist/router/config/index.js +38 -0
  83. package/dist/router/config/kimi.d.ts +21 -0
  84. package/dist/router/config/kimi.js +63 -0
  85. package/dist/router/config/opencode.d.ts +12 -0
  86. package/dist/router/config/opencode.js +44 -0
  87. package/dist/router/config/pi.d.ts +12 -0
  88. package/dist/router/config/pi.js +50 -0
  89. package/dist/router/config/types.d.ts +43 -0
  90. package/dist/router/config/types.js +8 -0
  91. package/dist/router/index.d.ts +318 -0
  92. package/dist/router/index.js +2715 -0
  93. package/dist/router/order-exec-dispatch.d.ts +30 -0
  94. package/dist/router/order-exec-dispatch.js +48 -0
  95. package/dist/router/utils.d.ts +44 -0
  96. package/dist/router/utils.js +116 -0
  97. package/dist/runner/bash/index.d.ts +4 -0
  98. package/dist/runner/bash/index.js +5 -0
  99. package/dist/runner/bash/runner.d.ts +55 -0
  100. package/dist/runner/bash/runner.js +213 -0
  101. package/dist/runner/claude/index.d.ts +4 -0
  102. package/dist/runner/claude/index.js +5 -0
  103. package/dist/runner/claude/runner.d.ts +29 -0
  104. package/dist/runner/claude/runner.js +83 -0
  105. package/dist/runner/codex/argv.d.ts +33 -0
  106. package/dist/runner/codex/argv.js +40 -0
  107. package/dist/runner/codex/index.d.ts +1 -0
  108. package/dist/runner/codex/index.js +2 -0
  109. package/dist/runner/codex/jsonl.d.ts +37 -0
  110. package/dist/runner/codex/jsonl.js +221 -0
  111. package/dist/runner/codex/runner.d.ts +65 -0
  112. package/dist/runner/codex/runner.js +124 -0
  113. package/dist/runner/common/exec-translator.d.ts +73 -0
  114. package/dist/runner/common/exec-translator.js +105 -0
  115. package/dist/runner/common/jsonl-stream.d.ts +39 -0
  116. package/dist/runner/common/jsonl-stream.js +203 -0
  117. package/dist/runner/common/process-stopper.d.ts +28 -0
  118. package/dist/runner/common/process-stopper.js +79 -0
  119. package/dist/runner/common/runner-utils.d.ts +21 -0
  120. package/dist/runner/common/runner-utils.js +65 -0
  121. package/dist/runner/common/spawn-heartbeat.d.ts +33 -0
  122. package/dist/runner/common/spawn-heartbeat.js +51 -0
  123. package/dist/runner/common/spawning-runner.d.ts +321 -0
  124. package/dist/runner/common/spawning-runner.js +634 -0
  125. package/dist/runner/common/usage.d.ts +31 -0
  126. package/dist/runner/common/usage.js +29 -0
  127. package/dist/runner/index.d.ts +19 -0
  128. package/dist/runner/index.js +24 -0
  129. package/dist/runner/kimi/index.d.ts +1 -0
  130. package/dist/runner/kimi/index.js +2 -0
  131. package/dist/runner/kimi/jsonl.d.ts +28 -0
  132. package/dist/runner/kimi/jsonl.js +143 -0
  133. package/dist/runner/kimi/runner.d.ts +50 -0
  134. package/dist/runner/kimi/runner.js +105 -0
  135. package/dist/runner/opencode/argv.d.ts +32 -0
  136. package/dist/runner/opencode/argv.js +35 -0
  137. package/dist/runner/opencode/index.d.ts +1 -0
  138. package/dist/runner/opencode/index.js +2 -0
  139. package/dist/runner/opencode/jsonl.d.ts +68 -0
  140. package/dist/runner/opencode/jsonl.js +295 -0
  141. package/dist/runner/opencode/runner.d.ts +63 -0
  142. package/dist/runner/opencode/runner.js +93 -0
  143. package/dist/runner/pi/index.d.ts +1 -0
  144. package/dist/runner/pi/index.js +2 -0
  145. package/dist/runner/pi/jsonl.d.ts +128 -0
  146. package/dist/runner/pi/jsonl.js +249 -0
  147. package/dist/runner/pi/runner.d.ts +37 -0
  148. package/dist/runner/pi/runner.js +76 -0
  149. package/dist/runner/registry.d.ts +54 -0
  150. package/dist/runner/registry.js +75 -0
  151. package/dist/runner/resolve-agent-choices.d.ts +7 -0
  152. package/dist/runner/resolve-agent-choices.js +76 -0
  153. package/dist/runner/sync-agent-choices.d.ts +6 -0
  154. package/dist/runner/sync-agent-choices.js +73 -0
  155. package/dist/runner/types.d.ts +250 -0
  156. package/dist/runner/types.js +8 -0
  157. package/dist/session/claude/index.d.ts +1 -0
  158. package/dist/session/claude/index.js +2 -0
  159. package/dist/session/claude/session-reader.d.ts +34 -0
  160. package/dist/session/claude/session-reader.js +46 -0
  161. package/dist/session/claude/sessions.d.ts +53 -0
  162. package/dist/session/claude/sessions.js +528 -0
  163. package/dist/session/codex/index.d.ts +1 -0
  164. package/dist/session/codex/index.js +2 -0
  165. package/dist/session/codex/rollout-reader.d.ts +88 -0
  166. package/dist/session/codex/rollout-reader.js +473 -0
  167. package/dist/session/codex/sessions.d.ts +21 -0
  168. package/dist/session/codex/sessions.js +40 -0
  169. package/dist/session/common/catch-up.d.ts +12 -0
  170. package/dist/session/common/catch-up.js +31 -0
  171. package/dist/session/common/constants.d.ts +11 -0
  172. package/dist/session/common/constants.js +12 -0
  173. package/dist/session/common/content-blocks.d.ts +37 -0
  174. package/dist/session/common/content-blocks.js +68 -0
  175. package/dist/session/common/jsonl.d.ts +77 -0
  176. package/dist/session/common/jsonl.js +351 -0
  177. package/dist/session/common/usage-accumulator.d.ts +58 -0
  178. package/dist/session/common/usage-accumulator.js +72 -0
  179. package/dist/session/index.d.ts +7 -0
  180. package/dist/session/index.js +11 -0
  181. package/dist/session/kimi/index.d.ts +1 -0
  182. package/dist/session/kimi/index.js +2 -0
  183. package/dist/session/kimi/sessions.d.ts +81 -0
  184. package/dist/session/kimi/sessions.js +527 -0
  185. package/dist/session/opencode/index.d.ts +1 -0
  186. package/dist/session/opencode/index.js +2 -0
  187. package/dist/session/opencode/sessions.d.ts +81 -0
  188. package/dist/session/opencode/sessions.js +388 -0
  189. package/dist/session/pi/index.d.ts +1 -0
  190. package/dist/session/pi/index.js +2 -0
  191. package/dist/session/pi/sessions.d.ts +27 -0
  192. package/dist/session/pi/sessions.js +399 -0
  193. package/dist/session/registry.d.ts +12 -0
  194. package/dist/session/registry.js +20 -0
  195. package/dist/session/session-store.d.ts +86 -0
  196. package/dist/session/session-store.js +450 -0
  197. package/dist/startup-contact.d.ts +18 -0
  198. package/dist/startup-contact.js +51 -0
  199. package/dist/workspace/index.d.ts +19 -0
  200. package/dist/workspace/index.js +53 -0
  201. package/package.json +93 -0
@@ -0,0 +1,841 @@
1
+ import { terminalToColor, terminalToLabel, newSessionButton, stopButton, agentDisplayName, } from './card-shared.js';
2
+ import { collapsibleMarkdownPanel, markdownDiv } from './collapsible.js';
3
+ import { toolBodyMd, toolHeaderText } from './tool-render.js';
4
+ import { truncateUtf8, truncateMarkdownTables } from './text-truncate.js';
5
+ import { formatTimestamp } from './time.js';
6
+ import { formatUsageStats } from '../router/index.js';
7
+ const CARD_BUDGET_BYTES = 28_000;
8
+ const REASONING_BYTES = 4_500;
9
+ const TEXT_BYTES = 10_000;
10
+ const DEGRADED_THINKING_KEEP = 2;
11
+ const DEGRADED_TEXT_BYTES = 5_000;
12
+ const DEGRADED_THINKING_BYTES = 1_000;
13
+ const DEGRADED_TOOL_KEEP = 3; // Degraded: keep last 3 tool panels
14
+ const EXTREME_TOOL_KEEP = 1; // Extreme: keep last 1 tool panel
15
+ // --- Budget estimate (先估后建) ---
16
+ // 影子测量:估算 = 精确复刻 normal 路径将要渲染的 JSON 字节。面板外壳、statusRow、
17
+ // summary、header、按钮全部实测(measureJson),块内容经与渲染相同的变换
18
+ // (truncateUtf8 截断 + markdownDiv 的 escapeMarkdown + JSON.stringify)精确测量,
19
+ // 不依赖任何转义因子或拍脑袋常数(P1-2 第二轮:CJK/ASCII 转义双向精确)。
20
+ const DEGRADED_THRESHOLD = 24_000; // 估算低于此值走正常路径(留 4KB 裕度,相对 CARD_BUDGET_BYTES=28000);否则直接 degraded。调整时同步评估裕度是否仍覆盖估算偏差
21
+ /** 精确测量对象经 JSON.stringify 后的 UTF-8 字节数。 */
22
+ function measureJson(obj) {
23
+ return Buffer.byteLength(JSON.stringify(obj), 'utf8');
24
+ }
25
+ /** markdownDiv('', 'notation') 的序列化字节(空内容 div 的常量开销,估算时复用)。 */
26
+ const EMPTY_MD_DIV_BYTES = measureJson(markdownDiv('', 'notation'));
27
+ /**
28
+ * Wrap text block in a collapsible panel with timestamp in the header.
29
+ * This unifies timestamp position across all block types (thinking/plan/file_change/tool/text).
30
+ *
31
+ * Panel title: `💬 **输出** (ts)` when timestamp present, `💬 **输出**` otherwise.
32
+ * Running: expanded=true; completed: expanded=false (symmetric with thinking).
33
+ *
34
+ * @param content - The text content to render
35
+ * @param timestamp - Optional timestamp for the block
36
+ * @param finalized - Whether the run is in a terminal state
37
+ * @returns elements
38
+ */
39
+ function renderTextBlock(content, timestamp, finalized = false) {
40
+ if (!content.trim()) {
41
+ return [];
42
+ }
43
+ const ts = formatTimestamp(timestamp);
44
+ // Terminal state: output is the card body, render in collapsible_panel with expanded=true.
45
+ // NOTE: collapsible_panel does NOT shield markdown tables from Feishu's 11310 table count
46
+ // limit — tables inside lark_md text are still counted regardless of container.
47
+ // We must explicitly truncate excess tables via truncateMarkdownTables before rendering.
48
+ if (finalized) {
49
+ const title = '💬 **输出**';
50
+ const header = ts ? `${title} (${ts})` : title;
51
+ const safeContent = truncateMarkdownTables(content);
52
+ return [
53
+ collapsibleMarkdownPanel({
54
+ title: header,
55
+ expanded: true, // Always expanded in finalized state for full visibility
56
+ border: 'grey',
57
+ content: safeContent,
58
+ textSize: 'notation',
59
+ }),
60
+ ];
61
+ }
62
+ // Running/finalizing: panel provides visual grouping with timestamp in header
63
+ const title = '💬 **输出**';
64
+ const header = ts ? `${title} (${ts})` : title;
65
+ const safeContent = truncateMarkdownTables(content);
66
+ return [
67
+ collapsibleMarkdownPanel({
68
+ title: header,
69
+ expanded: true,
70
+ border: 'grey',
71
+ content: safeContent,
72
+ textSize: 'notation',
73
+ }),
74
+ ];
75
+ }
76
+ /**
77
+ * 单次截断:把每个超限块(thinking/plan/text)的最终渲染内容算出来,供
78
+ * estimateCardBytes 与 buildChronologicalContent 共用。此前影子测量与正式渲染
79
+ * 各自 truncateUtf8 一次(双倍截断);现在截断只发生在 prepare 阶段。
80
+ */
81
+ function prepareGroupContent(groups) {
82
+ const prepared = new Map();
83
+ for (const group of groups) {
84
+ if (group.kind === 'thinking') {
85
+ prepared.set(group, truncateUtf8(group.content, REASONING_BYTES));
86
+ }
87
+ else if (group.kind === 'plan') {
88
+ prepared.set(group, truncateUtf8(group.content, REASONING_BYTES * 2));
89
+ }
90
+ else if (group.kind === 'text') {
91
+ prepared.set(group, truncateUtf8(group.content, TEXT_BYTES, true, '…(已截断)\n'));
92
+ }
93
+ }
94
+ return prepared;
95
+ }
96
+ /**
97
+ * Group blocks by type. Each tool is emitted independently.
98
+ * This enables per-tool compression (keep last N, discard older ones).
99
+ */
100
+ function groupBlocks(blocks) {
101
+ const groups = [];
102
+ for (const block of blocks) {
103
+ // Each tool is emitted as its own group (not grouped with other tools)
104
+ if (block.kind === 'tool') {
105
+ groups.push({ kind: 'tool', tool: block.tool });
106
+ continue;
107
+ }
108
+ // Handle thinking block — emit as separate group
109
+ if (block.kind === 'thinking') {
110
+ groups.push({
111
+ kind: 'thinking',
112
+ content: block.content,
113
+ active: block.active,
114
+ timestamp: block.timestamp,
115
+ });
116
+ continue;
117
+ }
118
+ // Handle plan block — emit as separate group (collapsed by default)
119
+ if (block.kind === 'plan') {
120
+ groups.push({
121
+ kind: 'plan',
122
+ content: block.content,
123
+ active: block.active,
124
+ timestamp: block.timestamp,
125
+ });
126
+ continue;
127
+ }
128
+ // Handle file_change block — emit as separate group (collapsed by default)
129
+ if (block.kind === 'file_change') {
130
+ groups.push({
131
+ kind: 'file_change',
132
+ path: block.path,
133
+ operation: block.operation,
134
+ diff: block.diff,
135
+ timestamp: block.timestamp,
136
+ });
137
+ continue;
138
+ }
139
+ // text block: merge with previous text if consecutive
140
+ if (block.kind === 'text') {
141
+ const last = groups[groups.length - 1];
142
+ if (last?.kind === 'text') {
143
+ groups[groups.length - 1] = {
144
+ kind: 'text',
145
+ content: last.content + block.content,
146
+ timestamp: last.timestamp ?? block.timestamp,
147
+ };
148
+ }
149
+ else {
150
+ groups.push({ kind: 'text', content: block.content, timestamp: block.timestamp });
151
+ }
152
+ }
153
+ }
154
+ return groups;
155
+ }
156
+ /**
157
+ * Render a single tool as a collapsible panel.
158
+ * - Running tool: expanded (user can watch live)
159
+ * - Completed tool: collapsed (click to inspect)
160
+ */
161
+ function renderTool(tool, finalized, showResult) {
162
+ const border = tool.status === 'error' ? 'red' : 'grey';
163
+ const body = showResult ? toolBodyMd(tool) : toolBodyMd({ ...tool, output: undefined });
164
+ return collapsibleMarkdownPanel({
165
+ title: toolTitle(tool),
166
+ expanded: !finalized && tool.status === 'running',
167
+ border,
168
+ content: body || '_无输出_',
169
+ textSize: 'notation',
170
+ });
171
+ }
172
+ function toolTitle(tool) {
173
+ const ts = formatTimestamp(tool.completedAt ?? tool.startedAt);
174
+ const title = toolHeaderText(tool);
175
+ return ts ? `${title} (${ts})` : title;
176
+ }
177
+ /**
178
+ * Build degraded elements when full card exceeds 28KB budget.
179
+ * Strategy: text fully preserved, thinking keeps last 2, tools keep last N (compressed).
180
+ *
181
+ * 性能优化:单遍遍历 state.blocks 按 kind 分桶(thinking/tool/other),替代原先
182
+ * 多次 state.blocks.filter() 扫描。分桶时保持原相对顺序,后续用 groupBlocks
183
+ * 渲染时序不变。
184
+ */
185
+ function buildDegradedElements(state, options) {
186
+ const elements = [];
187
+ elements.push(statusRow(state));
188
+ // 单遍分桶:thinking / tool / other(保持原相对顺序)
189
+ const { thinking: allThinkingBlocks, tool: allToolBlocks } = bucketThinkingAndTool(state.blocks);
190
+ if (allThinkingBlocks.length > DEGRADED_THINKING_KEEP) {
191
+ const omitted = allThinkingBlocks.length - DEGRADED_THINKING_KEEP;
192
+ elements.push(markdownDiv(`_💡 ${omitted} 个早期思考已省略_`));
193
+ }
194
+ // Keep only the last N thinking blocks for display
195
+ const thinkingBlocksToShow = allThinkingBlocks.slice(-DEGRADED_THINKING_KEEP);
196
+ const toolsToShow = allToolBlocks.slice(-DEGRADED_TOOL_KEEP);
197
+ const toolsOmitted = allToolBlocks.length - DEGRADED_TOOL_KEEP;
198
+ // Add omit hint for tools if any were omitted
199
+ if (toolsOmitted > 0) {
200
+ elements.push(markdownDiv(`_💡 另外 ${toolsOmitted} 个工具调用已省略_`));
201
+ }
202
+ // Build a filtered blocks list that only includes the thinking blocks we want to show
203
+ // and the tool blocks we want to show.
204
+ // P2-6: use object references (like the extreme path) instead of timestamps.
205
+ // RunBlock.timestamp has millisecond precision; ≥2 thinking blocks sharing a
206
+ // timestamp collapse to one Set entry, so `has(block.timestamp)` lets ALL
207
+ // same-timestamp blocks (including ones meant to be omitted) pass through —
208
+ // inflating the degraded card past 28KB and contradicting the "N omitted"
209
+ // hint. `thinkingBlocksToShow` is a slice of `allThinkingBlocks`, so the
210
+ // blocks retain stable references and the Set membership check is exact.
211
+ const thinkingSetToShow = new Set(thinkingBlocksToShow);
212
+ const toolIdsToShow = new Set(toolsToShow.map((b) => b.tool.id));
213
+ const filteredBlocks = state.blocks.filter((block) => {
214
+ if (block.kind === 'thinking') {
215
+ return thinkingSetToShow.has(block);
216
+ }
217
+ if (block.kind === 'tool') {
218
+ return toolIdsToShow.has(block.tool.id);
219
+ }
220
+ return true; // keep all non-thinking, non-tool blocks
221
+ });
222
+ // Render thinking/text/tool in chronological order using groupBlocks
223
+ for (const group of groupBlocks(filteredBlocks)) {
224
+ if (group.kind === 'thinking') {
225
+ if (options.showThinking === false)
226
+ continue;
227
+ const ts = formatTimestamp(group.timestamp);
228
+ const title = '💭 **思考完成**';
229
+ const content = truncateUtf8(group.content, REASONING_BYTES);
230
+ const header = ts ? `${title} (${ts})` : title;
231
+ elements.push(collapsibleMarkdownPanel({
232
+ title: header,
233
+ expanded: false,
234
+ border: 'grey',
235
+ content,
236
+ textSize: 'notation',
237
+ }));
238
+ }
239
+ else if (group.kind === 'text') {
240
+ const content = truncateUtf8(group.content, TEXT_BYTES, true, '…(已截断)\n');
241
+ const ts = formatTimestamp(group.timestamp);
242
+ const els = renderTextBlock(content, ts, true);
243
+ elements.push(...els);
244
+ }
245
+ else if (group.kind === 'tool') {
246
+ // Degraded: show tool individually (each tool is independent now)
247
+ if (options.showToolUse === false)
248
+ continue;
249
+ elements.push(renderTool(group.tool, true, options.showToolResult !== false)); // finalized=true
250
+ }
251
+ }
252
+ elements.push(...buildSummaryContent(state));
253
+ // Bottom action row: stop (if running) + new session (always)
254
+ const actionButtons = [];
255
+ // finalizing 也显示停止按钮(进程未退出,用户可 /stop)
256
+ const showStop = state.terminal === 'running' || state.terminal === 'finalizing';
257
+ if (showStop) {
258
+ actionButtons.push(stopButton(state.runId));
259
+ }
260
+ actionButtons.push(newSessionButton());
261
+ if (actionButtons.length > 0) {
262
+ elements.push({ tag: 'div', text: { content: '‎', tag: 'lark_md' } }, // spacer
263
+ {
264
+ tag: 'column_set',
265
+ columns: actionButtons.map((btn) => ({
266
+ tag: 'column',
267
+ width: 'auto',
268
+ elements: [btn],
269
+ })),
270
+ });
271
+ }
272
+ return elements;
273
+ }
274
+ /**
275
+ * Extreme fallback when even degraded card exceeds 28KB.
276
+ * Strategy: text truncated to 5KB tail, thinking only last 1 truncated to 1KB,
277
+ * tools keep last N (more aggressive than degraded).
278
+ *
279
+ * 性能优化:单遍分桶替代多次 state.blocks.filter()。
280
+ */
281
+ function buildExtremeFallbackElements(state, options) {
282
+ const elements = [];
283
+ elements.push(statusRow(state));
284
+ // 单遍分桶:thinking / tool
285
+ const { thinking: allThinkingBlocks, tool: allToolBlocks } = bucketThinkingAndTool(state.blocks);
286
+ // Thinking: only keep last 1 for extreme fallback
287
+ if (allThinkingBlocks.length > 1) {
288
+ const omitted = allThinkingBlocks.length - 1;
289
+ elements.push(markdownDiv(`_💡 ${omitted} 个早期思考已省略_`));
290
+ }
291
+ const thinkingBlockToShow = allThinkingBlocks.length > 0 ? allThinkingBlocks[allThinkingBlocks.length - 1] : null;
292
+ const toolsToShow = allToolBlocks.slice(-EXTREME_TOOL_KEEP);
293
+ const toolsOmitted = allToolBlocks.length - EXTREME_TOOL_KEEP;
294
+ // Add omit hint for tools if any were omitted
295
+ if (toolsOmitted > 0) {
296
+ elements.push(markdownDiv(`_💡 另外 ${toolsOmitted} 个工具调用已省略_`));
297
+ }
298
+ // Build filtered blocks: only last thinking, last N tools, all text
299
+ const toolIdsToShow = new Set(toolsToShow.map((b) => b.tool.id));
300
+ const filteredBlocks = state.blocks.filter((block) => {
301
+ if (block.kind === 'thinking') {
302
+ return block === thinkingBlockToShow;
303
+ }
304
+ if (block.kind === 'tool') {
305
+ return toolIdsToShow.has(block.tool.id);
306
+ }
307
+ return true; // keep all text and other blocks
308
+ });
309
+ // Render in chronological order
310
+ for (const group of groupBlocks(filteredBlocks)) {
311
+ if (group.kind === 'thinking') {
312
+ if (options.showThinking === false)
313
+ continue;
314
+ const ts = formatTimestamp(group.timestamp);
315
+ const title = '💭 **思考完成**';
316
+ const content = truncateUtf8(group.content, DEGRADED_THINKING_BYTES);
317
+ const header = ts ? `${title} (${ts})` : title;
318
+ elements.push(collapsibleMarkdownPanel({
319
+ title: header,
320
+ expanded: false,
321
+ border: 'grey',
322
+ content,
323
+ textSize: 'notation',
324
+ }));
325
+ }
326
+ else if (group.kind === 'text') {
327
+ const content = truncateUtf8(group.content, DEGRADED_TEXT_BYTES, true, '…(已截断)\n');
328
+ const ts = formatTimestamp(group.timestamp);
329
+ const els = renderTextBlock(content, ts, true);
330
+ elements.push(...els);
331
+ }
332
+ else if (group.kind === 'tool') {
333
+ // Extreme fallback: show tool individually
334
+ if (options.showToolUse === false)
335
+ continue;
336
+ elements.push(renderTool(group.tool, true, options.showToolResult !== false));
337
+ }
338
+ }
339
+ if (elements.length === 0) {
340
+ elements.push(markdownDiv('_暂无输出_'));
341
+ }
342
+ elements.push(...buildSummaryContent(state));
343
+ // Bottom action row: stop (if running) + new session (always)
344
+ const actionButtons = [];
345
+ // finalizing 也显示停止按钮(进程未退出,用户可 /stop)
346
+ const showStop = state.terminal === 'running' || state.terminal === 'finalizing';
347
+ if (showStop) {
348
+ actionButtons.push(stopButton(state.runId));
349
+ }
350
+ actionButtons.push(newSessionButton());
351
+ if (actionButtons.length > 0) {
352
+ elements.push({ tag: 'div', text: { content: '‎', tag: 'lark_md' } }, // spacer
353
+ {
354
+ tag: 'column_set',
355
+ columns: actionButtons.map((btn) => ({
356
+ tag: 'column',
357
+ width: 'auto',
358
+ elements: [btn],
359
+ })),
360
+ });
361
+ }
362
+ return elements;
363
+ }
364
+ /** Build the status row element (shared across full/degraded/extreme). */
365
+ function statusRow(state) {
366
+ const statusLabel = statusTagLabel(state.terminal);
367
+ const durationInfo = buildDurationInfo(state);
368
+ return {
369
+ tag: 'column_set',
370
+ columns: [
371
+ {
372
+ tag: 'column',
373
+ width: 'auto',
374
+ elements: [{ tag: 'div', text: { content: statusLabel, tag: 'lark_md' } }],
375
+ },
376
+ {
377
+ tag: 'column',
378
+ width: 'grow',
379
+ elements: [{ tag: 'div', text: { content: durationInfo, tag: 'lark_md' } }],
380
+ },
381
+ ],
382
+ };
383
+ }
384
+ /**
385
+ * Minimal skeleton card — the final safety net when even the extreme fallback
386
+ * exceeds the 28KB budget (pathological high-escape content: backslash inflates
387
+ * 4× via escapeMarkdown + JSON.stringify). The extreme fallback is the last
388
+ * degradation layer and used to `return` without a budget check, leaking >28KB
389
+ * cards to Feishu (ErrCode 11310, card unusable).
390
+ *
391
+ * This guarantees a structurally tiny card: status row + a fixed "output too
392
+ * large" notice + summary + the bottom action buttons (new session always;
393
+ * stop while running/finalizing). No block content is rendered, so the size is
394
+ * bounded by static structure regardless of input — closing the safety-net gap.
395
+ */
396
+ function buildSkeletonElements(state) {
397
+ const elements = [];
398
+ elements.push(statusRow(state));
399
+ elements.push(markdownDiv('_⚠️ 输出过大且含大量转义字符,已省略全部内容以避免超限;完整内容请查日志_'));
400
+ // Summary (token stats etc.) — static, bounded size
401
+ elements.push(...buildSummaryContent(state));
402
+ // Bottom action row: stop (if running/finalizing) + new session (always).
403
+ // Degraded paths must keep the action buttons reachable (AGENTS.md red line).
404
+ const actionButtons = [];
405
+ const showStop = state.terminal === 'running' || state.terminal === 'finalizing';
406
+ if (showStop) {
407
+ actionButtons.push(stopButton(state.runId));
408
+ }
409
+ actionButtons.push(newSessionButton());
410
+ elements.push({ tag: 'div', text: { content: '‎', tag: 'lark_md' } }, // spacer
411
+ {
412
+ tag: 'column_set',
413
+ columns: actionButtons.map((btn) => ({
414
+ tag: 'column',
415
+ width: 'auto',
416
+ elements: [btn],
417
+ })),
418
+ });
419
+ return elements;
420
+ }
421
+ /**
422
+ * Single-pass bucket of thinking/tool blocks, preserving original relative
423
+ * order. Shared by degraded and extreme fallback (replaces repeated
424
+ * `state.blocks.filter()` scans). Returning precise element types lets
425
+ * callers access `.timestamp` / `.tool.id` without a redundant type guard.
426
+ */
427
+ function bucketThinkingAndTool(blocks) {
428
+ const thinking = [];
429
+ const tool = [];
430
+ for (const block of blocks) {
431
+ if (block.kind === 'thinking') {
432
+ thinking.push(block);
433
+ }
434
+ else if (block.kind === 'tool') {
435
+ tool.push(block);
436
+ }
437
+ }
438
+ return { thinking, tool };
439
+ }
440
+ /**
441
+ * 影子测量:精确复刻 normal 路径(buildChronologicalContent + renderRunCard 的
442
+ * 骨架)产物 stringify 后的字节数,避免"先建完整卡再丢弃"的浪费。
443
+ *
444
+ * 第一性原理:估算应当测量"将要渲染的内容",而非近似它。
445
+ * - 骨架(schema/config/header/statusRow/summary/buttons/spacer)用小对象实测
446
+ * (measureJson),不依赖固定常数。
447
+ * - 块内容经与渲染完全相同的变换后精确测量:truncateUtf8 截断 →
448
+ * markdownDiv 的 escapeMarkdown(反斜杠 2×)→ JSON.stringify。因此 CJK
449
+ * (3 字节/字符无转义)与 ASCII 高转义(\n/" 膨胀)双向精确,无需转义因子。
450
+ * - tool 块直接 measureJson(renderTool(...))——与渲染共用同一函数,无复制。
451
+ * - 块结构基于 groupBlocks(与渲染一致,连续 text 合并),保证合并语义一致。
452
+ *
453
+ * 唯一系统性低估:elements 数组元素间逗号/括号 ~n+1 字节(忽略,4KB 裕度内)。
454
+ *
455
+ * 注意:估算只是优化路径选择,不替代最终 stringify 兜底。即使估算说"低于阈值"
456
+ * 走正常路径,renderRunCard 仍会 stringify 确认 ≤28KB;若估算低估,仍 fallback
457
+ * 到 degraded(安全网不丢)。
458
+ */
459
+ /** 导出供测试断言估算精度(估算 ≈ 实际渲染字节,见 tests/anchor/run-card/run-card.test.ts)。 */
460
+ export function estimateCardBytes(state, options = {}, shared) {
461
+ const finalized = state.terminal !== 'running';
462
+ const showResult = options.showToolResult !== false;
463
+ const showThinking = options.showThinking !== false;
464
+ const showToolUse = options.showToolUse !== false;
465
+ // P1-2:renderRunCard 传入共享 groups/prepared 时直接复用(截断只做一次);
466
+ // 独立调用(测试/预算探针)时自建,行为与原先一致。
467
+ const groups = shared?.groups ?? groupBlocks(state.blocks);
468
+ const prepared = shared?.prepared ?? prepareGroupContent(groups);
469
+ // 卡片外壳 + 状态行 + summary(小对象实测,动态文案精确)
470
+ let total = measureJson({
471
+ schema: '2.0',
472
+ config: { wide_screen_mode: true, update_multi: true },
473
+ header: {
474
+ template: headerTemplate2(state),
475
+ title: { content: headerTitle2(state, options), tag: 'plain_text' },
476
+ },
477
+ body: { elements: [] },
478
+ }) +
479
+ measureJson(statusRow(state)) +
480
+ measureJson(buildSummaryContent(state));
481
+ // 底部操作行:spacer + 操作按钮(与 renderRunCard normal 分支一致)
482
+ const actionButtons = [];
483
+ if (state.terminal === 'running' || state.terminal === 'finalizing') {
484
+ actionButtons.push(stopButton(state.runId));
485
+ }
486
+ actionButtons.push(newSessionButton());
487
+ total += measureJson([
488
+ { tag: 'div', text: { content: '‎', tag: 'lark_md' } },
489
+ {
490
+ tag: 'column_set',
491
+ columns: actionButtons.map((btn) => ({
492
+ tag: 'column',
493
+ width: 'auto',
494
+ elements: [btn],
495
+ })),
496
+ },
497
+ ]);
498
+ // 块内容:与 buildChronologicalContent 同源(groupBlocks 合并语义一致)
499
+ let renderedAny = false;
500
+ for (const group of groups) {
501
+ if (group.kind === 'thinking') {
502
+ if (!showThinking)
503
+ continue;
504
+ renderedAny = true;
505
+ const ts = formatTimestamp(group.timestamp);
506
+ const title = group.active ? '💭 **思考中**' : '💭 **思考完成**';
507
+ const header = ts ? `${title} (${ts})` : title;
508
+ total += measurePanelBytes(header, group.active, 'grey', prepared.get(group) ?? '');
509
+ }
510
+ else if (group.kind === 'plan') {
511
+ renderedAny = true;
512
+ const ts = formatTimestamp(group.timestamp);
513
+ const title = group.active ? '📋 **执行计划**' : '📋 **计划完成**';
514
+ const header = ts ? `${title} (${ts})` : title;
515
+ total += measurePanelBytes(header, group.active, 'blue', prepared.get(group) ?? '');
516
+ }
517
+ else if (group.kind === 'file_change') {
518
+ renderedAny = true;
519
+ const ts = formatTimestamp(group.timestamp);
520
+ const opIcon = group.operation === 'create'
521
+ ? '🆕'
522
+ : group.operation === 'edit'
523
+ ? '✏️'
524
+ : group.operation === 'delete'
525
+ ? '🗑️'
526
+ : '📖';
527
+ const title = `${opIcon} **文件改动**`;
528
+ const content = group.diff
529
+ ? `**${group.path}**\n\n\`\`\`\n${group.diff}\n\`\`\``
530
+ : `**${group.path}** (${group.operation})`;
531
+ const header = ts ? `${title} (${ts})` : title;
532
+ total += measurePanelBytes(header, false, 'grey', content);
533
+ }
534
+ else if (group.kind === 'text') {
535
+ const content = prepared.get(group) ?? '';
536
+ if (!content.trim())
537
+ continue;
538
+ renderedAny = true;
539
+ const ts = formatTimestamp(group.timestamp);
540
+ const header = `💬 **输出**${ts ? ` (${ts})` : ''}`;
541
+ total += measurePanelBytes(header, true, 'grey', content);
542
+ }
543
+ else if (group.kind === 'tool') {
544
+ if (!showToolUse)
545
+ continue;
546
+ renderedAny = true;
547
+ // tool 直接实测真实渲染元素(复用 renderTool,output ≤1200 字符,物化廉价)
548
+ total += measureJson(renderTool(group.tool, finalized, showResult));
549
+ }
550
+ }
551
+ if (!renderedAny) {
552
+ total += measureJson(markdownDiv('_暂无输出_'));
553
+ }
554
+ return Math.ceil(total);
555
+ }
556
+ /**
557
+ * 精确测量一个 collapsible 面板的序列化字节。
558
+ * 外壳(title/expanded/border/elements 结构)用空 content 面板实测,内容单独
559
+ * 经 markdownDiv(含 escapeMarkdown)精确测量——与渲染完全一致。
560
+ */
561
+ function measurePanelBytes(title, expanded, border, content) {
562
+ const shell = collapsibleMarkdownPanel({
563
+ title,
564
+ expanded,
565
+ border,
566
+ content: '',
567
+ textSize: 'notation',
568
+ });
569
+ return measureJson(shell) - EMPTY_MD_DIV_BYTES + measureJson(markdownDiv(content, 'notation'));
570
+ }
571
+ /**
572
+ * CardKit 2.0 renderer - simplified for streaming (no tabs)
573
+ */
574
+ export function renderRunCard(state, options = {}) {
575
+ const { terminal, runId } = state;
576
+ // 先估后建:用廉价估算预测是否超 28KB 预算,明显超预算直接跳过完整卡构建走
577
+ // degraded。P1-2:groups/prepared 只算一次,测量与正式渲染共用同一份截断结果,
578
+ // 避免影子测量 + 正式渲染对每个超限块双倍截断。
579
+ const groups = groupBlocks(state.blocks);
580
+ const prepared = prepareGroupContent(groups);
581
+ const estimate = estimateCardBytes(state, options, { groups, prepared });
582
+ // Stop button: show when running or finalizing (process not yet exited)
583
+ const showStop = terminal === 'running' || terminal === 'finalizing';
584
+ // 构建完整卡(仅在估算低于阈值时,避免大 state 浪费 render + stringify)
585
+ if (estimate < DEGRADED_THRESHOLD) {
586
+ const elements = [
587
+ statusRow(state),
588
+ ...buildChronologicalContent(state, options, prepared, groups),
589
+ ...buildSummaryContent(state),
590
+ ];
591
+ // Bottom action row: stop (if running) + new session (always)
592
+ const actionButtons = [];
593
+ if (showStop) {
594
+ actionButtons.push(stopButton(runId));
595
+ }
596
+ actionButtons.push(newSessionButton());
597
+ if (actionButtons.length > 0) {
598
+ elements.push({ tag: 'div', text: { content: '‎', tag: 'lark_md' } }, // spacer
599
+ {
600
+ tag: 'column_set',
601
+ columns: actionButtons.map((btn) => ({
602
+ tag: 'column',
603
+ width: 'auto',
604
+ elements: [btn],
605
+ })),
606
+ });
607
+ }
608
+ const card = {
609
+ schema: '2.0',
610
+ config: { wide_screen_mode: true, update_multi: true },
611
+ header: {
612
+ template: headerTemplate2(state),
613
+ title: { content: headerTitle2(state, options), tag: 'plain_text' },
614
+ },
615
+ body: { elements },
616
+ };
617
+ // Check budget — stringify 兜底(估算可能低估,仍保留安全网)
618
+ if (Buffer.byteLength(JSON.stringify(card), 'utf8') <= CARD_BUDGET_BYTES)
619
+ return card;
620
+ // 超预算 → 继续走 degraded(不 return,fallthrough)
621
+ }
622
+ // 估算 ≥ 阈值 或 完整卡 stringify 超预算 — 走 degraded 渲染
623
+ const degradedElements = buildDegradedElements(state, options);
624
+ const degradedCard = {
625
+ schema: '2.0',
626
+ config: { wide_screen_mode: true, update_multi: true },
627
+ header: {
628
+ template: headerTemplate2(state),
629
+ title: { content: headerTitle2(state, options), tag: 'plain_text' },
630
+ },
631
+ body: { elements: degradedElements },
632
+ };
633
+ if (Buffer.byteLength(JSON.stringify(degradedCard), 'utf8') <= CARD_BUDGET_BYTES) {
634
+ return degradedCard;
635
+ }
636
+ // Extreme fallback: even degraded exceeds budget
637
+ const extremeElements = buildExtremeFallbackElements(state, options);
638
+ const extremeCard = {
639
+ schema: '2.0',
640
+ config: { wide_screen_mode: true, update_multi: true },
641
+ header: {
642
+ template: headerTemplate2(state),
643
+ title: { content: headerTitle2(state, options), tag: 'plain_text' },
644
+ },
645
+ body: { elements: extremeElements },
646
+ };
647
+ // Final safety net: extreme fallback 是最后一层,但高转义内容(反斜杠 4× 膨胀)
648
+ // 可能让 extreme 产物本身 >28KB。补上 stringify 兜底(与 normal/degraded 对称):
649
+ // 超预算则降级到结构性保证 ≤28KB 的 skeleton 卡,避免飞书 11310 整卡不可用。
650
+ if (Buffer.byteLength(JSON.stringify(extremeCard), 'utf8') <= CARD_BUDGET_BYTES) {
651
+ return extremeCard;
652
+ }
653
+ const skeletonCard = {
654
+ schema: '2.0',
655
+ config: { wide_screen_mode: true, update_multi: true },
656
+ header: {
657
+ template: headerTemplate2(state),
658
+ title: { content: headerTitle2(state, options), tag: 'plain_text' },
659
+ },
660
+ body: { elements: buildSkeletonElements(state) },
661
+ };
662
+ // skeleton 卡是静态结构,理论上必 ≤28KB;保留兜底断言以防未来结构膨胀
663
+ if (Buffer.byteLength(JSON.stringify(skeletonCard), 'utf8') <= CARD_BUDGET_BYTES) {
664
+ return skeletonCard;
665
+ }
666
+ // 理论上不可达:skeleton 已是静态最小结构。若仍超限,硬截断文本提示兜底。
667
+ const truncatedSkeleton = {
668
+ ...skeletonCard,
669
+ body: {
670
+ elements: [statusRow(state), markdownDiv('_⚠️ 输出过大_'), newSessionButton()],
671
+ },
672
+ };
673
+ return truncatedSkeleton;
674
+ }
675
+ /** Build content in chronological order using groupBlocks to interleave thinking/text/tools. */
676
+ function buildChronologicalContent(state, options, prepared, groups) {
677
+ const elements = [];
678
+ // finalized = 非 running(含 finalizing:主结果已出,thinking 折叠)
679
+ const finalized = state.terminal !== 'running';
680
+ const showResult = options.showToolResult !== false;
681
+ // P1-2:renderRunCard 传入同一 groups 数组时直接复用(prepared 的键是组对象
682
+ // 引用,重建数组会让 prepared.get 永远 miss,退化为二次截断)。
683
+ for (const group of groups ?? groupBlocks(state.blocks)) {
684
+ if (group.kind === 'thinking') {
685
+ if (options.showThinking === false)
686
+ continue;
687
+ const ts = formatTimestamp(group.timestamp);
688
+ const title = group.active ? '💭 **思考中**' : '💭 **思考完成**';
689
+ const content = prepared?.get(group) ?? truncateUtf8(group.content, REASONING_BYTES);
690
+ const header = ts ? `${title} (${ts})` : title;
691
+ elements.push(collapsibleMarkdownPanel({
692
+ title: header,
693
+ expanded: group.active,
694
+ border: 'grey',
695
+ content,
696
+ textSize: 'notation',
697
+ }));
698
+ }
699
+ else if (group.kind === 'plan') {
700
+ // Plan blocks: render as collapsible panel (default collapsed)
701
+ const ts = formatTimestamp(group.timestamp);
702
+ const title = group.active ? '📋 **执行计划**' : '📋 **计划完成**';
703
+ const content = prepared?.get(group) ?? truncateUtf8(group.content, REASONING_BYTES * 2);
704
+ const header = ts ? `${title} (${ts})` : title;
705
+ elements.push(collapsibleMarkdownPanel({
706
+ title: header,
707
+ expanded: group.active, // expand while active, collapse when done
708
+ border: 'blue',
709
+ content,
710
+ textSize: 'notation',
711
+ }));
712
+ }
713
+ else if (group.kind === 'file_change') {
714
+ // File change blocks: render as collapsible panel (default collapsed)
715
+ const ts = formatTimestamp(group.timestamp);
716
+ const opIcon = group.operation === 'create'
717
+ ? '🆕'
718
+ : group.operation === 'edit'
719
+ ? '✏️'
720
+ : group.operation === 'delete'
721
+ ? '🗑️'
722
+ : '📖';
723
+ const title = `${opIcon} **文件改动**`;
724
+ const content = group.diff
725
+ ? `**${group.path}**\n\n\`\`\`\n${group.diff}\n\`\`\``
726
+ : `**${group.path}** (${group.operation})`;
727
+ const header = ts ? `${title} (${ts})` : title;
728
+ elements.push(collapsibleMarkdownPanel({
729
+ title: header,
730
+ expanded: false,
731
+ border: 'grey',
732
+ content,
733
+ textSize: 'notation',
734
+ }));
735
+ }
736
+ else if (group.kind === 'text') {
737
+ const content = prepared?.get(group) ?? truncateUtf8(group.content, TEXT_BYTES, true, '…(已截断)\n');
738
+ const ts = formatTimestamp(group.timestamp);
739
+ const els = renderTextBlock(content, ts, finalized);
740
+ elements.push(...els);
741
+ }
742
+ else if (group.kind === 'tool') {
743
+ if (options.showToolUse === false)
744
+ continue;
745
+ elements.push(renderTool(group.tool, finalized, showResult));
746
+ }
747
+ }
748
+ if (elements.length === 0) {
749
+ elements.push(markdownDiv('_暂无输出_'));
750
+ }
751
+ return elements;
752
+ }
753
+ /** Build summary tab content */
754
+ function buildSummaryContent(state) {
755
+ const elements = [];
756
+ // running 时不显示统计(仍在生成);finalizing 显示等待提示
757
+ if (state.terminal === 'running') {
758
+ return elements;
759
+ }
760
+ if (state.terminal === 'finalizing') {
761
+ elements.push(markdownDiv('⏳ **等待进程退出…**'));
762
+ return elements;
763
+ }
764
+ if (state.terminal === 'error') {
765
+ elements.push(markdownDiv(`⚠️ **运行出错**\n\n${state.errorMsg ?? '未知错误'}`));
766
+ }
767
+ else if (state.terminal === 'interrupted') {
768
+ elements.push(markdownDiv('⏹ **已被用户终止**'));
769
+ }
770
+ else if (state.terminal === 'idle_timeout') {
771
+ elements.push(markdownDiv(`⏱ **已超时**\n\n${state.idleTimeoutMinutes ?? 0} 分钟无响应,已自动终止`));
772
+ }
773
+ else {
774
+ const result = state.resultSubtype ?? 'success';
775
+ const hasContent = state.blocks.length > 0;
776
+ const empty = !hasContent ? '\n\n(未返回内容)' : '';
777
+ const usageStatsStr = formatUsageStats({
778
+ contextLength: state.contextLength,
779
+ compactCount: state.compactCount,
780
+ cacheReadTokens: state.cacheReadTokens,
781
+ cacheCreationTokens: state.cacheCreationTokens,
782
+ totalTokens: state.totalTokens,
783
+ inputTokens: state.inputTokens,
784
+ outputTokens: state.outputTokens,
785
+ cumulativeTotalTokens: state.cumulativeTotalTokens,
786
+ cumulativeInputTokens: state.cumulativeInputTokens,
787
+ cumulativeOutputTokens: state.cumulativeOutputTokens,
788
+ cumulativeCacheReadTokens: state.cumulativeCacheReadTokens,
789
+ cumulativeCacheCreationTokens: state.cumulativeCacheCreationTokens,
790
+ }, { showResult: true, result });
791
+ elements.push(markdownDiv(usageStatsStr + empty));
792
+ }
793
+ return elements;
794
+ }
795
+ /** Build duration info string */
796
+ function buildDurationInfo(state) {
797
+ if (state.terminal === 'done' ||
798
+ state.terminal === 'error' ||
799
+ state.terminal === 'interrupted' ||
800
+ state.terminal === 'idle_timeout') {
801
+ return `⏱ ${state.footer || '已完成'}`;
802
+ }
803
+ return footerText2(state.footer);
804
+ }
805
+ function footerText2(footer) {
806
+ if (footer === 'tool_running')
807
+ return '🧰 工具调用中';
808
+ if (footer === 'streaming')
809
+ return '✍️ 输出中';
810
+ return '🧠 思考中';
811
+ }
812
+ /** Status tag label */
813
+ function statusTagLabel(terminal) {
814
+ return terminalToLabel(terminal);
815
+ }
816
+ /** CardKit 2.0 header title — agent-aware via options.agentKind. */
817
+ function headerTitle2(state, options = {}) {
818
+ const agent = agentDisplayName(options.agentKind ?? 'claude');
819
+ if (state.terminal === 'done')
820
+ return `✅ ${agent} · 已完成`;
821
+ if (state.terminal === 'error')
822
+ return `⚠️ ${agent} · 出错`;
823
+ if (state.terminal === 'interrupted')
824
+ return `⏹ ${agent} · 已中断`;
825
+ if (state.terminal === 'idle_timeout')
826
+ return `⏱ ${agent} · 已超时`;
827
+ if (state.terminal === 'finalizing')
828
+ return `⏳ ${agent} · 完成中`;
829
+ if (state.footer === 'thinking')
830
+ return `💭 ${agent} · 思考中`;
831
+ if (state.footer === 'streaming')
832
+ return `💬 ${agent} · 输出中`;
833
+ if (state.footer === 'tool_running')
834
+ return `🔧 ${agent} · 调用工具`;
835
+ return `🔴 ${agent} · 运行中`;
836
+ }
837
+ /** CardKit 2.0 header template color */
838
+ function headerTemplate2(state) {
839
+ return terminalToColor(state.terminal);
840
+ }
841
+ //# sourceMappingURL=run-renderer.js.map