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
@@ -1,4 +1,4 @@
1
- import type { AgentEvent, ApprovalView } from '../runner/index.js';
1
+ import type { AgentEvent, ApprovalView, ToolHint } from '../runner/index.js';
2
2
  export type RunTerminal = 'running' | 'finalizing' | 'done' | 'error' | 'interrupted' | 'idle_timeout';
3
3
  export type RunFooter = 'thinking' | 'tool_running' | 'streaming' | null;
4
4
  /** 中断来源(interrupted 终态的原因),供卡片如实归因。 */
@@ -22,6 +22,10 @@ export interface ToolEntry {
22
22
  status: ToolStatus;
23
23
  startedAt?: string;
24
24
  completedAt?: string;
25
+ /** 工具渲染 hint(turn_diff 首见时携带;渲染层只消费不推断)。 */
26
+ toolHint?: ToolHint;
27
+ /** 工具原始 title(ACP kind 映射后与 name 不同时存在;渲染做副标题)。 */
28
+ summary?: string;
25
29
  }
26
30
  export type RunBlock = {
27
31
  kind: 'thinking';
@@ -56,6 +60,14 @@ export type RunBlock = {
56
60
  timestamp?: string;
57
61
  completedAt?: string;
58
62
  };
63
+ /** 运行期通知(warning / 重路由 / 压缩提示)。保留到达时间,渲染层可将其
64
+ * 按时间线插入内容流当普通消息;无时间戳时兜底置于内容流末尾。 */
65
+ export interface RunNotice {
66
+ level: 'info' | 'warn' | 'error';
67
+ code?: string;
68
+ text: string;
69
+ timestamp?: string;
70
+ }
59
71
  export interface RunState {
60
72
  runId: string;
61
73
  terminal: RunTerminal;
@@ -90,6 +102,20 @@ export interface RunState {
90
102
  /** 会话累计 cache creation token(所有 run 之和)。 */
91
103
  cumulativeCacheCreationTokens?: number;
92
104
  plan?: string;
105
+ /** 实际生效模型(system.init 携带;失败兜底 synthetic init 为 '',渲染须容忍)。 */
106
+ model?: string;
107
+ /** 本次 run 花费(USD,result 事件 total_cost_usd)。 */
108
+ costUsd?: number;
109
+ /** 运行期通知(warning / 重路由 / 压缩提示),按到达顺序。 */
110
+ notices?: RunNotice[];
111
+ /** 被 MAX_BLOCKS 静默丢弃的早期块计数(>0 时渲染「已省略 N 个早期步骤」)。 */
112
+ omittedBlocks?: number;
113
+ /** 会话标题(ACP session_info_update.title)。 */
114
+ sessionTitle?: string;
115
+ /** 当前模式(ACP current_mode_update.currentModeId)。 */
116
+ mode?: string;
117
+ /** 推理 token(pi usage.reasoning 累加,经 finish meta 写入)。 */
118
+ reasoningTokens?: number;
93
119
  /** Codex 操作类型:'turn' 表示普通 run,'compaction' 表示 compact 运行。 */
94
120
  operationKind?: 'turn' | 'compaction';
95
121
  /** 压缩前上下文水位(codex compact 卡展示「压缩前 X → 压缩后 Y」)。 */
@@ -135,6 +161,8 @@ export interface FinishMeta {
135
161
  cumulativeOutputTokens?: number;
136
162
  cumulativeCacheReadTokens?: number;
137
163
  cumulativeCacheCreationTokens?: number;
164
+ /** 推理 token(pi usage.reasoning 累加,经 usageMeta 透传)。 */
165
+ reasoningTokens?: number;
138
166
  }
139
167
  export declare function createInitialRunState(runId: string): RunState;
140
168
  export declare function reduceRunState(state: RunState, event: AgentEvent): RunState;
@@ -33,6 +33,10 @@ export function reduceRunState(state, event) {
33
33
  return reducePlanEvent(state, event);
34
34
  if (event.type === 'file_change')
35
35
  return reduceFileChangeEvent(state, event);
36
+ if (event.type === 'notice')
37
+ return reduceNoticeEvent(state, event);
38
+ if (event.type === 'session_info')
39
+ return reduceSessionInfoEvent(state, event);
36
40
  if (event.type === 'turn_started')
37
41
  return reduceTurnStartedEvent(state, event);
38
42
  if (event.type === 'turn_diff')
@@ -97,96 +101,116 @@ function reduceTurnDiffEvent(state, event) {
97
101
  const complete = event.complete === true;
98
102
  if (event.text !== undefined) {
99
103
  const blocks = markThinkingInactive(next.blocks);
104
+ const capped = upsertSnapshotBlock(blocks, (b) => b.kind === 'text' && b.itemId === event.itemId, () => ({
105
+ kind: 'text',
106
+ itemId: event.itemId,
107
+ content: keepTailMarked(event.text, MAX_TEXT_CHARS),
108
+ timestamp: ts,
109
+ ...(complete ? { completedAt: ts } : {}),
110
+ }), (existing) => {
111
+ const textBlock = existing;
112
+ return {
113
+ ...textBlock,
114
+ content: keepTailMarked(event.text, MAX_TEXT_CHARS),
115
+ // kimi:块锚点时间改为本次 diff 时间(整轮累积流不断续写,
116
+ // 标题应反映“最后写入时刻”而非首见时刻;move-to-end 后位置
117
+ // 与标题才一致)。codex app-server 不置 refreshTimestamp,
118
+ // 保持“首见时间戳不刷新”的既有契约。
119
+ ...(event.refreshTimestamp ? { timestamp: ts } : {}),
120
+ ...(complete ? { completedAt: ts } : {}),
121
+ };
122
+ }, !complete);
100
123
  next = {
101
124
  ...next,
102
- blocks: keepLatestBlocks(upsertSnapshotBlock(blocks, (b) => b.kind === 'text' && b.itemId === event.itemId, () => ({
103
- kind: 'text',
104
- itemId: event.itemId,
105
- content: keepTail(event.text, MAX_TEXT_CHARS),
106
- timestamp: ts,
107
- ...(complete ? { completedAt: ts } : {}),
108
- }), (existing) => {
109
- const textBlock = existing;
110
- return {
111
- ...textBlock,
112
- content: keepTail(event.text, MAX_TEXT_CHARS),
113
- ...(complete ? { completedAt: ts } : {}),
114
- };
115
- }, !complete)),
125
+ blocks: keepLatestBlocks(capped),
126
+ omittedBlocks: (next.omittedBlocks ?? 0) + Math.max(0, capped.length - MAX_BLOCKS),
116
127
  footer: 'streaming',
117
128
  };
118
129
  }
119
130
  if (event.reasoning !== undefined) {
120
- next = {
121
- ...next,
122
- blocks: keepLatestBlocks(upsertSnapshotBlock(next.blocks, (b) => b.kind === 'thinking' && b.itemId === event.itemId, () => ({
123
- kind: 'thinking',
124
- itemId: event.itemId,
125
- content: keepTail(event.reasoning, MAX_REASONING_CHARS),
131
+ const capped = upsertSnapshotBlock(next.blocks, (b) => b.kind === 'thinking' && b.itemId === event.itemId, () => ({
132
+ kind: 'thinking',
133
+ itemId: event.itemId,
134
+ content: keepTailMarked(event.reasoning, MAX_REASONING_CHARS),
135
+ active: !complete,
136
+ timestamp: ts,
137
+ ...(complete ? { completedAt: ts } : {}),
138
+ }), (existing) => {
139
+ const thinkingBlock = existing;
140
+ return {
141
+ ...thinkingBlock,
142
+ content: keepTailMarked(event.reasoning, MAX_REASONING_CHARS),
126
143
  active: !complete,
127
- timestamp: ts,
144
+ ...(event.refreshTimestamp ? { timestamp: ts } : {}),
128
145
  ...(complete ? { completedAt: ts } : {}),
129
- }), (existing) => {
130
- const thinkingBlock = existing;
131
- return {
132
- ...thinkingBlock,
133
- content: keepTail(event.reasoning, MAX_REASONING_CHARS),
134
- active: !complete,
135
- ...(complete ? { completedAt: ts } : {}),
136
- };
137
- }, !complete)),
146
+ };
147
+ }, !complete);
148
+ next = {
149
+ ...next,
150
+ blocks: keepLatestBlocks(capped),
151
+ omittedBlocks: (next.omittedBlocks ?? 0) + Math.max(0, capped.length - MAX_BLOCKS),
138
152
  footer: 'thinking',
139
153
  };
140
154
  }
141
155
  if (event.toolOutput !== undefined) {
142
- next = {
143
- ...next,
144
- blocks: keepLatestBlocks(upsertSnapshotBlock(next.blocks, (b) => b.kind === 'tool' && b.tool.id === event.itemId, () => ({
156
+ const capped = upsertSnapshotBlock(next.blocks, (b) => b.kind === 'tool' && b.tool.id === event.itemId, () => {
157
+ // 身份只在首见时写入(existing 分支不动):toolName/toolInput/toolHint。
158
+ const inputStr = truncateDetail(stringifyUnknown(event.toolInput ?? {}));
159
+ return {
145
160
  kind: 'tool',
146
161
  tool: {
147
162
  id: event.itemId,
148
- name: 'command',
149
- input: '',
163
+ name: event.toolName ?? 'command',
164
+ input: inputStr,
165
+ parsedInput: tryParseRecord(inputStr),
166
+ ...(event.toolHint !== undefined ? { toolHint: event.toolHint } : {}),
150
167
  output: keepTail(event.toolOutput, MAX_TOOL_DETAIL_CHARS),
151
168
  status: complete ? (event.toolStatus ?? 'ok') : 'running',
152
169
  startedAt: ts,
153
170
  ...(complete ? { completedAt: ts } : {}),
154
171
  },
155
- }), (existing) => ({
156
- kind: 'tool',
157
- tool: {
158
- ...existing.tool,
159
- output: keepTail(event.toolOutput, MAX_TOOL_DETAIL_CHARS),
160
- status: complete
161
- ? (event.toolStatus ?? 'ok')
162
- : existing.tool.status,
163
- ...(complete ? { completedAt: ts } : {}),
164
- },
165
- }), !complete)),
172
+ };
173
+ }, (existing) => ({
174
+ kind: 'tool',
175
+ tool: {
176
+ ...existing.tool,
177
+ output: keepTail(event.toolOutput, MAX_TOOL_DETAIL_CHARS),
178
+ status: complete
179
+ ? (event.toolStatus ?? 'ok')
180
+ : existing.tool.status,
181
+ ...(complete ? { completedAt: ts } : {}),
182
+ },
183
+ }), !complete);
184
+ next = {
185
+ ...next,
186
+ blocks: keepLatestBlocks(capped),
187
+ omittedBlocks: (next.omittedBlocks ?? 0) + Math.max(0, capped.length - MAX_BLOCKS),
166
188
  footer: 'tool_running',
167
189
  };
168
190
  }
169
191
  if (event.plan !== undefined) {
170
- const newPlan = keepTail(event.plan, MAX_REASONING_CHARS * 2);
171
- next = {
172
- ...next,
173
- plan: newPlan,
174
- blocks: keepLatestBlocks(upsertSnapshotBlock(next.blocks, (b) => b.kind === 'plan' && b.itemId === event.itemId, () => ({
175
- kind: 'plan',
176
- itemId: event.itemId,
192
+ const newPlan = keepTailMarked(event.plan, MAX_REASONING_CHARS * 2);
193
+ const capped = upsertSnapshotBlock(next.blocks, (b) => b.kind === 'plan' && b.itemId === event.itemId, () => ({
194
+ kind: 'plan',
195
+ itemId: event.itemId,
196
+ content: newPlan,
197
+ active: !complete,
198
+ timestamp: ts,
199
+ ...(complete ? { completedAt: ts } : {}),
200
+ }), (existing) => {
201
+ const planBlock = existing;
202
+ return {
203
+ ...planBlock,
177
204
  content: newPlan,
178
205
  active: !complete,
179
- timestamp: ts,
180
206
  ...(complete ? { completedAt: ts } : {}),
181
- }), (existing) => {
182
- const planBlock = existing;
183
- return {
184
- ...planBlock,
185
- content: newPlan,
186
- active: !complete,
187
- ...(complete ? { completedAt: ts } : {}),
188
- };
189
- }, !complete)),
207
+ };
208
+ }, !complete);
209
+ next = {
210
+ ...next,
211
+ plan: newPlan,
212
+ blocks: keepLatestBlocks(capped),
213
+ omittedBlocks: (next.omittedBlocks ?? 0) + Math.max(0, capped.length - MAX_BLOCKS),
190
214
  };
191
215
  }
192
216
  if (event.fileChanges !== undefined) {
@@ -204,7 +228,12 @@ function reduceTurnDiffEvent(state, event) {
204
228
  // 同一 item 的同一路径原地替换(权威 diff 校正),新路径/新 item 追加。
205
229
  blocks = upsertSnapshotBlock(blocks, (b) => b.kind === 'file_change' && b.itemId === event.itemId && b.path === fc.path, () => fileBlock, () => fileBlock, !complete);
206
230
  }
207
- next = { ...next, blocks: keepLatestBlocks(blocks) };
231
+ const capped = blocks;
232
+ next = {
233
+ ...next,
234
+ blocks: keepLatestBlocks(capped),
235
+ omittedBlocks: (next.omittedBlocks ?? 0) + Math.max(0, capped.length - MAX_BLOCKS),
236
+ };
208
237
  }
209
238
  return next;
210
239
  }
@@ -237,7 +266,7 @@ function reduceSystemEvent(state, event) {
237
266
  if (event.type !== 'system')
238
267
  return state;
239
268
  if (event.subtype === 'init') {
240
- return { ...state, sessionId: event.session_id, footer: 'thinking' };
269
+ return { ...state, sessionId: event.session_id, model: event.model, footer: 'thinking' };
241
270
  }
242
271
  if (event.subtype === 'compact_boundary' && event.compactMetadata) {
243
272
  return {
@@ -285,6 +314,10 @@ function reduceResultEvent(state, event) {
285
314
  blocks: markThinkingInactive(state.blocks),
286
315
  resultSubtype: mergedSubtype,
287
316
  errorMsg: mergedErrorMsg,
317
+ // costUsd:首个非 undefined 胜出(与 errorMsg 合并语义一致)
318
+ ...(event.total_cost_usd !== undefined && state.costUsd === undefined
319
+ ? { costUsd: event.total_cost_usd }
320
+ : {}),
288
321
  };
289
322
  }
290
323
  /** Handle assistant thinking/text/tool_use content blocks. */
@@ -300,7 +333,7 @@ function reduceAssistantEvent(state, event) {
300
333
  ...next.blocks.slice(0, -1),
301
334
  {
302
335
  kind: 'thinking',
303
- content: keepTail(last.content + '\n' + content.thinking, MAX_REASONING_CHARS),
336
+ content: keepTailMarked(last.content + '\n' + content.thinking, MAX_REASONING_CHARS),
304
337
  active: true,
305
338
  timestamp: last.timestamp ?? event.timestamp,
306
339
  },
@@ -309,7 +342,7 @@ function reduceAssistantEvent(state, event) {
309
342
  ...next.blocks,
310
343
  {
311
344
  kind: 'thinking',
312
- content: keepTail(content.thinking, MAX_REASONING_CHARS),
345
+ content: keepTailMarked(content.thinking, MAX_REASONING_CHARS),
313
346
  active: true,
314
347
  timestamp: event.timestamp,
315
348
  },
@@ -317,6 +350,7 @@ function reduceAssistantEvent(state, event) {
317
350
  next = {
318
351
  ...next,
319
352
  blocks: keepLatestBlocks(newBlocks),
353
+ omittedBlocks: (next.omittedBlocks ?? 0) + Math.max(0, newBlocks.length - MAX_BLOCKS),
320
354
  footer: 'thinking',
321
355
  };
322
356
  }
@@ -328,7 +362,7 @@ function reduceAssistantEvent(state, event) {
328
362
  ...blocks.slice(0, -1),
329
363
  {
330
364
  kind: 'text',
331
- content: keepTail(last.content + content.text, MAX_TEXT_CHARS),
365
+ content: keepTailMarked(last.content + content.text, MAX_TEXT_CHARS),
332
366
  timestamp: last.timestamp ?? event.timestamp,
333
367
  },
334
368
  ]
@@ -336,13 +370,14 @@ function reduceAssistantEvent(state, event) {
336
370
  ...blocks,
337
371
  {
338
372
  kind: 'text',
339
- content: keepTail(content.text, MAX_TEXT_CHARS),
373
+ content: keepTailMarked(content.text, MAX_TEXT_CHARS),
340
374
  timestamp: event.timestamp,
341
375
  },
342
376
  ];
343
377
  next = {
344
378
  ...next,
345
379
  blocks: keepLatestBlocks(newBlocks),
380
+ omittedBlocks: (next.omittedBlocks ?? 0) + Math.max(0, newBlocks.length - MAX_BLOCKS),
346
381
  footer: 'streaming',
347
382
  };
348
383
  }
@@ -353,22 +388,25 @@ function reduceAssistantEvent(state, event) {
353
388
  // string as the old per-render parse, so truncation-broken JSON still
354
389
  // yields null (over-cap input renders no summary — behavior preserved).
355
390
  const inputStr = truncateDetail(stringifyUnknown(content.input));
391
+ const capped = [
392
+ ...markThinkingInactive(next.blocks),
393
+ {
394
+ kind: 'tool',
395
+ tool: {
396
+ id: content.id,
397
+ name: content.name,
398
+ input: inputStr,
399
+ parsedInput: tryParseRecord(inputStr),
400
+ ...(content.summary !== undefined ? { summary: content.summary } : {}),
401
+ status: 'running',
402
+ startedAt: event.timestamp,
403
+ },
404
+ },
405
+ ];
356
406
  next = {
357
407
  ...next,
358
- blocks: keepLatestBlocks([
359
- ...markThinkingInactive(next.blocks),
360
- {
361
- kind: 'tool',
362
- tool: {
363
- id: content.id,
364
- name: content.name,
365
- input: inputStr,
366
- parsedInput: tryParseRecord(inputStr),
367
- status: 'running',
368
- startedAt: event.timestamp,
369
- },
370
- },
371
- ]),
408
+ blocks: keepLatestBlocks(capped),
409
+ omittedBlocks: (next.omittedBlocks ?? 0) + Math.max(0, capped.length - MAX_BLOCKS),
372
410
  footer: 'tool_running',
373
411
  };
374
412
  }
@@ -409,12 +447,39 @@ function reduceToolResultEvent(state, event) {
409
447
  }
410
448
  return next;
411
449
  }
450
+ /** Handle notice event — append a runtime notice (warning / reroute / compaction). */
451
+ function reduceNoticeEvent(state, event) {
452
+ if (event.type !== 'notice')
453
+ return state;
454
+ return {
455
+ ...state,
456
+ notices: [
457
+ ...(state.notices ?? []),
458
+ {
459
+ level: event.level,
460
+ ...(event.code !== undefined ? { code: event.code } : {}),
461
+ text: event.text,
462
+ ...(event.timestamp !== undefined ? { timestamp: event.timestamp } : {}),
463
+ },
464
+ ],
465
+ };
466
+ }
467
+ /** Handle session_info event — update sessionTitle / mode only when present. */
468
+ function reduceSessionInfoEvent(state, event) {
469
+ if (event.type !== 'session_info')
470
+ return state;
471
+ return {
472
+ ...state,
473
+ ...(event.title !== undefined ? { sessionTitle: event.title } : {}),
474
+ ...(event.mode !== undefined ? { mode: event.mode } : {}),
475
+ };
476
+ }
412
477
  /** Handle plan event — accumulate plan text for Codex-style agents. */
413
478
  function reducePlanEvent(state, event) {
414
479
  if (event.type !== 'plan')
415
480
  return state;
416
481
  const currentPlan = state.plan ?? '';
417
- const newPlan = keepTail(currentPlan + '\n' + event.plan, MAX_REASONING_CHARS * 2);
482
+ const newPlan = keepTailMarked(currentPlan + '\n' + event.plan, MAX_REASONING_CHARS * 2);
418
483
  // Plan is a single evolving document — replace the existing plan block
419
484
  // instead of appending a new one (avoids N overlapping blocks after N events).
420
485
  const withoutOldPlan = state.blocks.filter((b) => b.kind !== 'plan');
@@ -426,6 +491,7 @@ function reducePlanEvent(state, event) {
426
491
  ...state,
427
492
  plan: newPlan,
428
493
  blocks: keepLatestBlocks(newBlocks),
494
+ omittedBlocks: (state.omittedBlocks ?? 0) + Math.max(0, newBlocks.length - MAX_BLOCKS),
429
495
  };
430
496
  }
431
497
  /** Handle file_change event — emit a file_change block for rendering (Codex-style agents). */
@@ -446,6 +512,7 @@ function reduceFileChangeEvent(state, event) {
446
512
  return {
447
513
  ...state,
448
514
  blocks: keepLatestBlocks(newBlocks),
515
+ omittedBlocks: (state.omittedBlocks ?? 0) + Math.max(0, newBlocks.length - MAX_BLOCKS),
449
516
  };
450
517
  }
451
518
  export function finishRun(state, terminal, meta = {}) {
@@ -522,6 +589,27 @@ function tryParseRecord(input) {
522
589
  function keepLatestBlocks(blocks) {
523
590
  return blocks.length <= MAX_BLOCKS ? blocks : blocks.slice(-MAX_BLOCKS);
524
591
  }
592
+ /**
593
+ * keepTail + 截断标记:被去头的存储内容以前缀如实标注(信息保真红线:禁止静默丢弃)。
594
+ * 标记计入 maxChars 预算(存储总量仍 ≤ maxChars,保持既有长度不变量)。
595
+ */
596
+ function keepTailMarked(value, maxChars) {
597
+ if (value.length <= maxChars)
598
+ return value;
599
+ // marker 计入 maxChars 预算(总量不变量)。省略数 = value.length - kept.length,
600
+ // 而 kept 长度取决于 marker 长度、marker 长度又取决于省略数的位数——
601
+ // 不动点迭代(marker 固定部分 12 字符,位数至多几次即收敛)。
602
+ let dropped = value.length - maxChars + 12;
603
+ for (;;) {
604
+ const next = value.length - maxChars + 12 + String(dropped).length;
605
+ if (next === dropped)
606
+ break;
607
+ dropped = next;
608
+ }
609
+ const marker = `…(前 ${dropped} 字符已省略)\n`;
610
+ const kept = keepTail(value, Math.max(0, maxChars - marker.length));
611
+ return marker + kept;
612
+ }
525
613
  function truncateDetail(value) {
526
614
  return value.length <= MAX_TOOL_DETAIL_CHARS
527
615
  ? value
@@ -22,15 +22,6 @@ export declare const FEISHU_MAX_TABLES = 5;
22
22
  * 共用,避免三处各自定义后数值漂移。
23
23
  */
24
24
  export declare const CARD_BUDGET_BYTES = 28000;
25
- /**
26
- * Max byte budget for a single session `tool_result` event's content when
27
- * replaying an opencode session (L2 pre-fold). The static card budget enforcer
28
- * (`enforceCardBudget`, maxPanelContentBytes = 2000) would truncate it again at
29
- * render time anyway; pre-folding here bounds the in-memory `events[]` array
30
- * and the intermediate card JSON so a single pathological tool_result (e.g. a
31
- * 500KB file listing) never inflates the replay payload to megabytes.
32
- */
33
- export declare const TOOL_RESULT_MAX_BYTES = 4000;
34
25
  /**
35
26
  * Count the number of markdown tables in a string.
36
27
  *
@@ -22,15 +22,6 @@ export const FEISHU_MAX_TABLES = 5;
22
22
  * 共用,避免三处各自定义后数值漂移。
23
23
  */
24
24
  export const CARD_BUDGET_BYTES = 28_000;
25
- /**
26
- * Max byte budget for a single session `tool_result` event's content when
27
- * replaying an opencode session (L2 pre-fold). The static card budget enforcer
28
- * (`enforceCardBudget`, maxPanelContentBytes = 2000) would truncate it again at
29
- * render time anyway; pre-folding here bounds the in-memory `events[]` array
30
- * and the intermediate card JSON so a single pathological tool_result (e.g. a
31
- * 500KB file listing) never inflates the replay payload to megabytes.
32
- */
33
- export const TOOL_RESULT_MAX_BYTES = 4000;
34
25
  /**
35
26
  * Count the number of markdown tables in a string.
36
27
  *
@@ -20,6 +20,9 @@ export declare function toolHeaderText(tool: ToolEntry): string;
20
20
  /**
21
21
  * Structured body markdown for a tool call. Renders input fields by tool name
22
22
  * (Bash → command, Read/Edit/Write → file_path, etc.) and output in a code
23
- * block. Falls back to a raw JSON dump for unknown tools.
23
+ * block. An explicit `toolHint` (declared by the runner, 信息保真 C5) selects
24
+ * the same rendering by family instead of name. There is no raw-JSON fallback
25
+ * for unknown tools: `default` returns '' and unknown tools only surface their
26
+ * args through the panel header's `summarizeInput` fallback.
24
27
  */
25
28
  export declare function toolBodyMd(tool: ToolEntry): string;
@@ -9,6 +9,7 @@
9
9
  * under formatting noise. `toolHeaderText` produces a compact one-liner header
10
10
  * like `✅ **Bash** — pwd` so collapsed panels stay informative.
11
11
  */
12
+ import { truncate } from '../common/truncate.js';
12
13
  const HEADER_SUMMARY_MAX = 80;
13
14
  const BODY_FIELD_MAX = 600;
14
15
  const OUTPUT_MAX = 1200;
@@ -31,12 +32,25 @@ export function toolHeaderText(tool) {
31
32
  // P3-6: use the cached parsed record when available (avoids per-render parse).
32
33
  const input = tool.parsedInput !== undefined ? tool.parsedInput : tool.input;
33
34
  const summary = summarizeInput(tool.name, input);
34
- return summary ? `${icon} **${tool.name}** — ${summary}` : `${icon} **${tool.name}**`;
35
+ // 信息保真 C3.1:ACP kind 映射后 name 与原始 title 不同时,title 以
36
+ // ` — {summary}` 追加在标题末尾(截断沿用 HEADER_SUMMARY_MAX)。
37
+ // 用 truncate(suffix 为空)而非裸 slice:裸 slice 按 UTF-16 码元切,
38
+ // 切点落在 emoji 代理对中间会留下孤立代理项(P2-31 同族)。
39
+ const blockSummary = tool.summary
40
+ ? truncate(tool.summary.replace(/\s+/g, ' ').trim(), HEADER_SUMMARY_MAX, { suffix: '' })
41
+ : '';
42
+ const parts = [summary, blockSummary].filter(Boolean);
43
+ return parts.length > 0
44
+ ? `${icon} **${tool.name}** — ${parts.join(' — ')}`
45
+ : `${icon} **${tool.name}**`;
35
46
  }
36
47
  /**
37
48
  * Structured body markdown for a tool call. Renders input fields by tool name
38
49
  * (Bash → command, Read/Edit/Write → file_path, etc.) and output in a code
39
- * block. Falls back to a raw JSON dump for unknown tools.
50
+ * block. An explicit `toolHint` (declared by the runner, 信息保真 C5) selects
51
+ * the same rendering by family instead of name. There is no raw-JSON fallback
52
+ * for unknown tools: `default` returns '' and unknown tools only surface their
53
+ * args through the panel header's `summarizeInput` fallback.
40
54
  */
41
55
  export function toolBodyMd(tool) {
42
56
  const parts = [];
@@ -58,7 +72,8 @@ export function toolBodyMd(tool) {
58
72
  const body = parts.join('\n\n');
59
73
  if (body.length <= BODY_TOTAL_MAX)
60
74
  return body;
61
- return `${body.slice(0, BODY_TOTAL_MAX)}…\n\n_(body 已截断,完整内容查日志)_`;
75
+ // 同上:走 truncate 保证切点不落在代理对中间(末尾 '…' 由 truncate 附加以对齐预算)。
76
+ return `${truncate(body, BODY_TOTAL_MAX)}\n\n_(body 已截断,完整内容查日志)_`;
62
77
  }
63
78
  function summarizeInput(name, input) {
64
79
  const rec = asRecord(input);
@@ -105,6 +120,37 @@ function summarizeInput(name, input) {
105
120
  return pick('command') || pick('file_path') || pick('path') || pick('query');
106
121
  }
107
122
  }
123
+ // --- Named render bodies (shared by tool-name cases and toolHint branches) ---
124
+ function renderBashInput(str) {
125
+ const cmd = str('command');
126
+ return cmd ? `**Command**\n\`\`\`bash\n${truncate(cmd, BODY_FIELD_MAX)}\n\`\`\`` : '';
127
+ }
128
+ function renderFileInput(str) {
129
+ const fp = str('file_path') || str('path');
130
+ return fp ? `**File** \`${fp}\`` : '';
131
+ }
132
+ function renderGrepInput(str) {
133
+ const lines = [];
134
+ if (str('pattern'))
135
+ lines.push(`**Pattern** \`${str('pattern')}\``);
136
+ if (str('path'))
137
+ lines.push(`**Path** \`${str('path')}\``);
138
+ return lines.join('\n');
139
+ }
140
+ function renderGlobInput(str) {
141
+ const p = str('pattern');
142
+ return p ? `**Pattern** \`${truncate(p, BODY_FIELD_MAX)}\`` : '';
143
+ }
144
+ function renderLsInput(str) {
145
+ const p = str('path');
146
+ return p ? `**Path** \`${p}\`` : '';
147
+ }
148
+ function renderWebFetchInput(str) {
149
+ return str('url') ? `**URL** ${str('url')}` : '';
150
+ }
151
+ function renderWebSearchInput(str) {
152
+ return str('query') ? `**Query** \`${truncate(str('query'), BODY_FIELD_MAX)}\`` : '';
153
+ }
108
154
  function renderInput(tool) {
109
155
  // P3-6: use the cached parsed record when available (avoids per-render parse).
110
156
  const input = tool.parsedInput !== undefined ? tool.parsedInput : tool.input;
@@ -112,44 +158,45 @@ function renderInput(tool) {
112
158
  if (!rec)
113
159
  return '';
114
160
  const str = (k) => (typeof rec[k] === 'string' ? rec[k] : '');
161
+ // 信息保真 C5:hint 优先(runner 自愿声明,只消费不推断);缺失时与现状
162
+ // 完全一致。hint 分支与 name case 共用同一渲染函数,无复制粘贴。
163
+ switch (tool.toolHint) {
164
+ case 'shell':
165
+ return renderBashInput(str);
166
+ case 'file':
167
+ case 'patch':
168
+ return renderFileInput(str);
169
+ case 'search':
170
+ return renderGrepInput(str);
171
+ case 'web':
172
+ return renderWebFetchInput(str) || renderWebSearchInput(str);
173
+ default:
174
+ break;
175
+ }
115
176
  switch (tool.name) {
116
177
  case 'Bash':
117
- case 'bash': {
118
- const cmd = str('command');
119
- return cmd ? `**Command**\n\`\`\`bash\n${truncate(cmd, BODY_FIELD_MAX)}\n\`\`\`` : '';
120
- }
178
+ case 'bash':
179
+ return renderBashInput(str);
121
180
  case 'Read':
122
181
  case 'Edit':
123
182
  case 'Write':
124
183
  case 'NotebookEdit':
125
184
  case 'read':
126
185
  case 'edit':
127
- case 'write': {
128
- const fp = str('file_path') || str('path');
129
- return fp ? `**File** \`${fp}\`` : '';
130
- }
186
+ case 'write':
187
+ return renderFileInput(str);
131
188
  case 'Grep':
132
- case 'grep': {
133
- const lines = [];
134
- if (str('pattern'))
135
- lines.push(`**Pattern** \`${str('pattern')}\``);
136
- if (str('path'))
137
- lines.push(`**Path** \`${str('path')}\``);
138
- return lines.join('\n');
139
- }
189
+ case 'grep':
190
+ return renderGrepInput(str);
140
191
  case 'Glob':
141
- case 'find': {
142
- const p = str('pattern');
143
- return p ? `**Pattern** \`${truncate(p, BODY_FIELD_MAX)}\`` : '';
144
- }
145
- case 'ls': {
146
- const p = str('path');
147
- return p ? `**Path** \`${p}\`` : '';
148
- }
192
+ case 'find':
193
+ return renderGlobInput(str);
194
+ case 'ls':
195
+ return renderLsInput(str);
149
196
  case 'WebFetch':
150
- return str('url') ? `**URL** ${str('url')}` : '';
197
+ return renderWebFetchInput(str);
151
198
  case 'WebSearch':
152
- return str('query') ? `**Query** \`${truncate(str('query'), BODY_FIELD_MAX)}\`` : '';
199
+ return renderWebSearchInput(str);
153
200
  default:
154
201
  return '';
155
202
  }
@@ -170,7 +217,4 @@ function asRecord(input) {
170
217
  }
171
218
  return null;
172
219
  }
173
- function truncate(s, max) {
174
- return s.length > max ? `${s.slice(0, max)}…` : s;
175
- }
176
220
  //# sourceMappingURL=tool-render.js.map