lark-remote 0.4.3 → 0.4.4

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.
package/CHANGELOG.md CHANGED
@@ -5,6 +5,17 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.4.4] - 2026-10-04
9
+
10
+ ### 修复
11
+
12
+ - **opencode 的工具卡片能显示完整命令了**:opencode 的 ACP 在参数流式阶段先发 `tool_call`(参数为空、标题只回落成工具名),真正的命令/路径随后由 `tool_call_update` 携带。此前卡片只显示 `Bash` 看不到执行了什么;现在按调用 id 原地回填参数,命令与路径正常展示。顺带修掉同一命令被重复渲染成 `ls — ls` 的摘要去重问题
13
+ - **思考内容的省略计数不再虚增**:增量刷新时,旧逻辑把「前 N 字符已省略」标记自身的文本也计进被丢弃的正文,每帧约虚增 14 字符。现在只累计真实丢弃的正文,显示量始终不超过上限
14
+
15
+ ### 变更
16
+
17
+ - **`!` 命令失败用例去掉宿主 locale 依赖**:原先匹配 stderr 文案(`command not found`),中文 locale 下 bash 输出「未找到命令」导致用例稳定误红。改为断言确定性信号——命令名回显、非零退出码、诊断块呈现
18
+
8
19
  ## [0.4.3] - 2026-09-24
9
20
 
10
21
  ### 新增
@@ -388,31 +388,67 @@ function reduceAssistantEvent(state, event) {
388
388
  // string as the old per-render parse, so truncation-broken JSON still
389
389
  // yields null (over-cap input renders no summary — behavior preserved).
390
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
- ];
406
- next = {
407
- ...next,
408
- blocks: keepLatestBlocks(capped),
409
- omittedBlocks: (next.omittedBlocks ?? 0) + Math.max(0, capped.length - MAX_BLOCKS),
410
- footer: 'tool_running',
391
+ const incomingTool = {
392
+ id: content.id,
393
+ name: content.name,
394
+ input: inputStr,
395
+ parsedInput: tryParseRecord(inputStr),
396
+ ...(content.summary !== undefined ? { summary: content.summary } : {}),
397
+ status: 'running',
398
+ startedAt: event.timestamp,
411
399
  };
400
+ const blocks = markThinkingInactive(next.blocks);
401
+ // A tool_use patch (same id) enriches an existing tool block in place:
402
+ // opencode emits the initial tool_call while args are still streaming,
403
+ // then re-reports the command/path in later tool_call_updates. Merging
404
+ // keeps one panel per tool call; a first-seen id still appends.
405
+ const existingIndex = blocks.findIndex((block) => block.kind === 'tool' && block.tool.id === content.id);
406
+ if (existingIndex >= 0) {
407
+ const existing = blocks[existingIndex];
408
+ const merged = blocks.slice();
409
+ merged[existingIndex] = {
410
+ kind: 'tool',
411
+ tool: mergeToolUse(existing.tool, incomingTool),
412
+ };
413
+ next = { ...next, blocks: keepLatestBlocks(merged), footer: 'tool_running' };
414
+ }
415
+ else {
416
+ const capped = [...blocks, { kind: 'tool', tool: incomingTool }];
417
+ next = {
418
+ ...next,
419
+ blocks: keepLatestBlocks(capped),
420
+ omittedBlocks: (next.omittedBlocks ?? 0) + Math.max(0, capped.length - MAX_BLOCKS),
421
+ footer: 'tool_running',
422
+ };
423
+ }
412
424
  }
413
425
  }
414
426
  return next;
415
427
  }
428
+ /**
429
+ * Merge a same-id tool_use patch into the existing tool block. Only fields the
430
+ * patch can legitimately enrich are overwritten:
431
+ * - `input`/`parsedInput` — replaced only when the patch carries a non-empty
432
+ * object (an empty `{}` patch must not erase a command already rendered);
433
+ * - `summary` — replaced only when the patch supplies one;
434
+ * - `name` — kept from first sight (a completed opencode update has no kind,
435
+ * so its patch cannot know the mapped name).
436
+ * Everything else (`status`, `output`, `completedAt`, `toolHint`) is preserved,
437
+ * so an out-of-order patch after the tool_result cannot reset status to running.
438
+ */
439
+ function mergeToolUse(existing, incoming) {
440
+ const incomingHasInput = incoming.parsedInput !== undefined &&
441
+ incoming.parsedInput !== null &&
442
+ Object.keys(incoming.parsedInput).length > 0;
443
+ return {
444
+ ...existing,
445
+ input: incomingHasInput ? incoming.input : existing.input,
446
+ parsedInput: incomingHasInput ? incoming.parsedInput : existing.parsedInput,
447
+ name: existing.name || incoming.name,
448
+ ...(incoming.summary !== undefined ? { summary: incoming.summary } : {}),
449
+ startedAt: existing.startedAt ?? incoming.startedAt,
450
+ };
451
+ }
416
452
  /** Handle tool_result content blocks (user message with tool results).
417
453
  * P3-4: rebuild ONLY the block matching `tool_use_id`, copying every other
418
454
  * block by reference. The old `.map()` recreated all block objects per
@@ -589,26 +625,40 @@ function tryParseRecord(input) {
589
625
  function keepLatestBlocks(blocks) {
590
626
  return blocks.length <= MAX_BLOCKS ? blocks : blocks.slice(-MAX_BLOCKS);
591
627
  }
628
+ /** 截断标记前缀(`keepTailMarked` 产出,含已丢弃正文计数)。 */
629
+ const TRUNCATION_MARKER_RE = /^…(前 (\d+) 字符已省略)\n/;
592
630
  /**
593
631
  * keepTail + 截断标记:被去头的存储内容以前缀如实标注(信息保真红线:禁止静默丢弃)。
594
632
  * 标记计入 maxChars 预算(存储总量仍 ≤ maxChars,保持既有长度不变量)。
633
+ *
634
+ * 计数口径(2026-10 修正):只统计**真实丢弃的正文**,不含标记自身开销。
635
+ * 关键点是增量流下 `value` 往往已带上一次的标记(`marker + kept`);若直接
636
+ * 用 `value.length - kept.length` 计费,会把上一帧的标记文本也算进「已省略」,
637
+ * 造成每帧显示十几字符的虚高。这里先剥离旧标记,把 N 定义为**从该块起始累计
638
+ * 丢弃的正文数**(单调不减),再按「marker 计入预算」的不动点求最小 N。
595
639
  */
596
640
  function keepTailMarked(value, maxChars) {
597
- if (value.length <= maxChars)
641
+ const prior = TRUNCATION_MARKER_RE.exec(value);
642
+ const priorDropped = prior ? Number(prior[1]) : 0;
643
+ const body = prior ? value.slice(prior[0].length) : value;
644
+ // 从未截断且当前正文未超预算:原样返回(不误标)。
645
+ if (priorDropped === 0 && body.length <= maxChars)
598
646
  return value;
599
- // marker 计入 maxChars 预算(总量不变量)。省略数 = value.length - kept.length,
600
- // 而 kept 长度取决于 marker 长度、marker 长度又取决于省略数的位数——
601
- // 不动点迭代(marker 固定部分 12 字符,位数至多几次即收敛)。
602
- let dropped = value.length - maxChars + 12;
647
+ // 求最小累计丢弃数 N(≥ priorDropped),使 marker(N) + 保留正文 ≤ maxChars:
648
+ // body.length - (N - priorDropped) + (12 + digits(N)) ≤ maxChars
649
+ // ⟺ N ≥ priorDropped + body.length - maxChars + 12 + digits(N)
650
+ // 即 N = K + digits(N) 的不动点(K 为固定项,位数至多几次即收敛)。
651
+ const K = priorDropped + body.length - maxChars + 12;
652
+ let totalDropped = Math.max(priorDropped, K);
603
653
  for (;;) {
604
- const next = value.length - maxChars + 12 + String(dropped).length;
605
- if (next === dropped)
654
+ const next = Math.max(priorDropped, K + String(totalDropped).length);
655
+ if (next === totalDropped)
606
656
  break;
607
- dropped = next;
657
+ totalDropped = next;
608
658
  }
609
- const marker = `…(前 ${dropped} 字符已省略)\n`;
610
- const kept = keepTail(value, Math.max(0, maxChars - marker.length));
611
- return marker + kept;
659
+ const newlyDropped = totalDropped - priorDropped;
660
+ const kept = body.slice(newlyDropped);
661
+ return `…(前 ${totalDropped} 字符已省略)\n${kept}`;
612
662
  }
613
663
  function truncateDetail(value) {
614
664
  return value.length <= MAX_TOOL_DETAIL_CHARS
@@ -39,9 +39,13 @@ export function toolHeaderText(tool) {
39
39
  const blockSummary = tool.summary
40
40
  ? truncate(tool.summary.replace(/\s+/g, ' ').trim(), HEADER_SUMMARY_MAX, { suffix: '' })
41
41
  : '';
42
+ // opencode carries the same value in both slots (title = command/path, and
43
+ // the args-derived summary resolves to it too); dedupe so the header reads
44
+ // `✅ **Bash** — ls` instead of `✅ **Bash** — ls — ls`.
42
45
  const parts = [summary, blockSummary].filter(Boolean);
43
- return parts.length > 0
44
- ? `${icon} **${tool.name}** — ${parts.join(' — ')}`
46
+ const uniqueParts = parts.filter((part, i) => parts.indexOf(part) === i);
47
+ return uniqueParts.length > 0
48
+ ? `${icon} **${tool.name}** — ${uniqueParts.join(' — ')}`
45
49
  : `${icon} **${tool.name}**`;
46
50
  }
47
51
  /**
@@ -15,6 +15,8 @@
15
15
  * handleRequestPermission — approval rendering (abstract)
16
16
  * handleOtherServerRequest — extra reverse RPC (kimi: elicitation)
17
17
  * extractToolResultContent — tool_result content (default: rawOutput)
18
+ * extractToolCallArgsText — streaming-args source (opencode: none)
19
+ * buildToolUseUpdate — tool_use patch from tool_call_update
18
20
  * onUsageSample — extra usage bookkeeping (opencode: cost)
19
21
  */
20
22
  import type { AgentEvent, ApprovalRequestedEvent, ResultEvent } from '../../types.js';
@@ -69,6 +71,13 @@ export declare abstract class BaseAcpTranslator {
69
71
  * approval handler correlate by toolCallId and recover the real command.
70
72
  */
71
73
  private toolCallArgsByCallId;
74
+ /**
75
+ * tool_use name recorded at tool_call time (kind mapping wins, else title).
76
+ * A later tool_call_update may carry only a title (opencode's completed
77
+ * update has neither kind nor rawInput); pairing it with the recorded name
78
+ * lets a tool_use patch render the original title as a subtitle.
79
+ */
80
+ private toolCallNameByCallId;
72
81
  /**
73
82
  * Handle a notification from the ACP server and return translated events.
74
83
  * Wire envelope: {sessionId, update: {sessionUpdate: '<kind>', ...}} — the
@@ -132,8 +141,42 @@ export declare abstract class BaseAcpTranslator {
132
141
  * command (see toolCallArgsByCallId).
133
142
  */
134
143
  protected getTrackedToolCallArgs(toolCallId: string): string | undefined;
144
+ /** tool_use name recorded at tool_call time (kind mapping wins, else title). */
145
+ protected getToolCallName(toolCallId: string): string | undefined;
146
+ /**
147
+ * kind → canonical tool name (信息保真 C3.1). Shared by the initial tool_call
148
+ * handler and the tool_call_update patch builder so both agree on the name.
149
+ */
150
+ protected toolNameFromKind(kind: string | undefined): string | undefined;
151
+ /**
152
+ * Streaming-args text carried by a tool_call/_update notification.
153
+ *
154
+ * Default: the notification's content text — kimi's events-map.ts encodes the
155
+ * accumulated args there (toolCallDeltaToSessionUpdate: content.text =
156
+ * accumulator.args), and the approval handler relies on it.
157
+ *
158
+ * opencode overrides this to return undefined: its tool_call/_update content
159
+ * is the shell OUTPUT snapshot (decompiled acp service `WP`), never args
160
+ * (args ride rawInput in `XC`/`BP`). Treating that output as args would
161
+ * corrupt approval command recovery — the hazard this hook exists to close.
162
+ */
163
+ protected extractToolCallArgsText(event: ToolCallEvent | ToolCallUpdateEvent): string | undefined;
164
+ /**
165
+ * tool_call_update → optional tool_use patch event. When an update carries
166
+ * richer identity than the initial tool_call did (opencode emits the
167
+ * tool_call while args are still streaming, then delivers the real title /
168
+ * rawInput in later updates), the returned `assistant`/`tool_use` block is
169
+ * merged into the existing tool block by the card reducer (matched by id)
170
+ * instead of creating a duplicate panel.
171
+ *
172
+ * Default: null (kimi self-produces its Bash tool_use from terminal/create
173
+ * with the real command, so it needs no patch). opencode overrides.
174
+ */
175
+ protected buildToolUseUpdate(_event: ToolCallUpdateEvent): AgentEvent | null;
135
176
  /** Record latest args text, bounded so a long session cannot grow it forever. */
136
177
  private trackToolCallArgs;
178
+ /** Record tool_use name, bounded to keep a long session from growing it. */
179
+ private trackToolCallName;
137
180
  /**
138
181
  * tool_result content extraction. Default: rawOutput passthrough (kimi
139
182
  * semantics). opencode overrides — its rawOutput is an OBJECT
@@ -65,6 +65,13 @@ export class BaseAcpTranslator {
65
65
  * approval handler correlate by toolCallId and recover the real command.
66
66
  */
67
67
  toolCallArgsByCallId = new Map();
68
+ /**
69
+ * tool_use name recorded at tool_call time (kind mapping wins, else title).
70
+ * A later tool_call_update may carry only a title (opencode's completed
71
+ * update has neither kind nor rawInput); pairing it with the recorded name
72
+ * lets a tool_use patch render the original title as a subtitle.
73
+ */
74
+ toolCallNameByCallId = new Map();
68
75
  /**
69
76
  * Handle a notification from the ACP server and return translated events.
70
77
  * Wire envelope: {sessionId, update: {sessionUpdate: '<kind>', ...}} — the
@@ -231,15 +238,16 @@ export class BaseAcpTranslator {
231
238
  // Keep raw string as-is
232
239
  }
233
240
  }
234
- const argsText = extractToolCallContentText(event.content);
241
+ const argsText = this.extractToolCallArgsText(event);
235
242
  if (argsText !== undefined) {
236
243
  this.trackToolCallArgs(event.toolCallId, argsText);
237
244
  }
238
245
  else if (event.rawInput !== undefined && event.rawInput !== null) {
239
246
  this.trackToolCallArgs(event.toolCallId, stringifyUnknown(event.rawInput));
240
247
  }
241
- const mapped = event.kind ? KIND_TO_TOOL[event.kind] : undefined;
248
+ const mapped = this.toolNameFromKind(event.kind);
242
249
  const name = mapped ?? event.title;
250
+ this.trackToolCallName(event.toolCallId, name);
243
251
  const summary = mapped && event.title && event.title !== name ? event.title : undefined;
244
252
  return [
245
253
  {
@@ -265,6 +273,11 @@ export class BaseAcpTranslator {
265
273
  handleToolCallUpdate(event, _sessionId) {
266
274
  const isError = event.status === 'failed';
267
275
  const content = this.extractToolResultContent(event);
276
+ // Patch the existing tool block with identity that was not available at the
277
+ // initial tool_call (opencode streams args after emitting tool_call). The
278
+ // reducer merges by id, so this never duplicates the panel. Built before
279
+ // the terminal cleanup below so it can still resolve the recorded name.
280
+ const useUpdate = this.buildToolUseUpdate(event);
268
281
  // Terminal updates end the args-streaming phase — drop the tracked entry.
269
282
  // In-progress delta text is cumulative (events-map.ts
270
283
  // toolCallDeltaToSessionUpdate: content.text = accumulator.args), so the
@@ -273,7 +286,7 @@ export class BaseAcpTranslator {
273
286
  this.toolCallArgsByCallId.delete(event.toolCallId);
274
287
  }
275
288
  else {
276
- const argsText = extractToolCallContentText(event.content);
289
+ const argsText = this.extractToolCallArgsText(event);
277
290
  if (argsText !== undefined) {
278
291
  this.trackToolCallArgs(event.toolCallId, argsText);
279
292
  }
@@ -281,22 +294,24 @@ export class BaseAcpTranslator {
281
294
  this.trackToolCallArgs(event.toolCallId, stringifyUnknown(event.rawInput));
282
295
  }
283
296
  }
284
- return [
285
- {
286
- type: 'user',
287
- message: {
288
- content: [
289
- {
290
- type: 'tool_result',
291
- tool_use_id: event.toolCallId,
292
- content,
293
- is_error: isError,
294
- },
295
- ],
296
- },
297
- timestamp: new Date().toISOString(),
297
+ const events = [];
298
+ if (useUpdate)
299
+ events.push(useUpdate);
300
+ events.push({
301
+ type: 'user',
302
+ message: {
303
+ content: [
304
+ {
305
+ type: 'tool_result',
306
+ tool_use_id: event.toolCallId,
307
+ content,
308
+ is_error: isError,
309
+ },
310
+ ],
298
311
  },
299
- ];
312
+ timestamp: new Date().toISOString(),
313
+ });
314
+ return events;
300
315
  }
301
316
  /**
302
317
  * Latest tracked streaming-args text for a tool call (undefined when never
@@ -307,6 +322,46 @@ export class BaseAcpTranslator {
307
322
  getTrackedToolCallArgs(toolCallId) {
308
323
  return this.toolCallArgsByCallId.get(toolCallId);
309
324
  }
325
+ /** tool_use name recorded at tool_call time (kind mapping wins, else title). */
326
+ getToolCallName(toolCallId) {
327
+ return this.toolCallNameByCallId.get(toolCallId);
328
+ }
329
+ /**
330
+ * kind → canonical tool name (信息保真 C3.1). Shared by the initial tool_call
331
+ * handler and the tool_call_update patch builder so both agree on the name.
332
+ */
333
+ toolNameFromKind(kind) {
334
+ return kind ? KIND_TO_TOOL[kind] : undefined;
335
+ }
336
+ /**
337
+ * Streaming-args text carried by a tool_call/_update notification.
338
+ *
339
+ * Default: the notification's content text — kimi's events-map.ts encodes the
340
+ * accumulated args there (toolCallDeltaToSessionUpdate: content.text =
341
+ * accumulator.args), and the approval handler relies on it.
342
+ *
343
+ * opencode overrides this to return undefined: its tool_call/_update content
344
+ * is the shell OUTPUT snapshot (decompiled acp service `WP`), never args
345
+ * (args ride rawInput in `XC`/`BP`). Treating that output as args would
346
+ * corrupt approval command recovery — the hazard this hook exists to close.
347
+ */
348
+ extractToolCallArgsText(event) {
349
+ return extractToolCallContentText(event.content);
350
+ }
351
+ /**
352
+ * tool_call_update → optional tool_use patch event. When an update carries
353
+ * richer identity than the initial tool_call did (opencode emits the
354
+ * tool_call while args are still streaming, then delivers the real title /
355
+ * rawInput in later updates), the returned `assistant`/`tool_use` block is
356
+ * merged into the existing tool block by the card reducer (matched by id)
357
+ * instead of creating a duplicate panel.
358
+ *
359
+ * Default: null (kimi self-produces its Bash tool_use from terminal/create
360
+ * with the real command, so it needs no patch). opencode overrides.
361
+ */
362
+ buildToolUseUpdate(_event) {
363
+ return null;
364
+ }
310
365
  /** Record latest args text, bounded so a long session cannot grow it forever. */
311
366
  trackToolCallArgs(toolCallId, argsText) {
312
367
  this.toolCallArgsByCallId.delete(toolCallId);
@@ -317,6 +372,16 @@ export class BaseAcpTranslator {
317
372
  this.toolCallArgsByCallId.delete(oldest);
318
373
  }
319
374
  }
375
+ /** Record tool_use name, bounded to keep a long session from growing it. */
376
+ trackToolCallName(toolCallId, name) {
377
+ this.toolCallNameByCallId.delete(toolCallId);
378
+ this.toolCallNameByCallId.set(toolCallId, name);
379
+ if (this.toolCallNameByCallId.size > 100) {
380
+ const oldest = this.toolCallNameByCallId.keys().next().value;
381
+ if (oldest !== undefined)
382
+ this.toolCallNameByCallId.delete(oldest);
383
+ }
384
+ }
320
385
  /**
321
386
  * tool_result content extraction. Default: rawOutput passthrough (kimi
322
387
  * semantics). opencode overrides — its rawOutput is an OBJECT
@@ -24,7 +24,8 @@
24
24
  * prompt other stopReason/error → result error
25
25
  * session/request_permission → approval_requested (optionId echo)
26
26
  */
27
- import type { RequestPermissionParams, AgentMessageChunkEvent, AgentThoughtChunkEvent, ToolCallUpdateEvent, UsageUpdateEvent } from '../../common/acp/protocol-types.js';
27
+ import type { AgentEvent } from '../../types.js';
28
+ import type { RequestPermissionParams, AgentMessageChunkEvent, AgentThoughtChunkEvent, ToolCallEvent, ToolCallUpdateEvent, UsageUpdateEvent } from '../../common/acp/protocol-types.js';
28
29
  import { BaseAcpTranslator, type AcpTranslatorEvent } from '../../common/acp/base-translator.js';
29
30
  export declare class OpencodeAcpTranslator extends BaseAcpTranslator {
30
31
  protected readonly logTag = "opencode-acp-translator";
@@ -36,6 +37,26 @@ export declare class OpencodeAcpTranslator extends BaseAcpTranslator {
36
37
  * content[] with shell output snapshot.
37
38
  */
38
39
  protected extractToolResultContent(event: ToolCallUpdateEvent): unknown;
40
+ /**
41
+ * opencode tool args always arrive as rawInput (`XC`/`BP`), never as content
42
+ * text. A tool_call_update's content is the shell OUTPUT snapshot (`WP`), so
43
+ * inheriting the base args-tracking would record output as if it were the
44
+ * command — the corruption this override prevents. Return undefined: nothing
45
+ * to track from content (rawInput tracking in the base still applies).
46
+ */
47
+ protected extractToolCallArgsText(_event: ToolCallEvent | ToolCallUpdateEvent): string | undefined;
48
+ /**
49
+ * opencode emits the tool_call while args are still streaming: the part is
50
+ * created with `state:{status:'pending',input:{}}` (decompiled acp service),
51
+ * so the initial title falls back to the tool name (e.g. `bash`) and the card
52
+ * shows no command. The real identity arrives in later tool_call_updates:
53
+ * - running (`BP`): full rawInput + title = command/path
54
+ * - completed (`HP`): title only (no kind, no rawInput)
55
+ * Emit a tool_use patch when either carries usable data; the reducer merges
56
+ * it into the existing block by id. Name resolves from kind (`BP`) or the
57
+ * name recorded at tool_call time (`HP`).
58
+ */
59
+ protected buildToolUseUpdate(event: ToolCallUpdateEvent): AgentEvent | null;
39
60
  /** opencode usage_update also carries session cost: {cost:{amount}} (USD). */
40
61
  protected onUsageSample(event: UsageUpdateEvent): void;
41
62
  protected handleRequestPermission(requestId: number | string, params: RequestPermissionParams): AcpTranslatorEvent[];
@@ -92,6 +92,66 @@ export class OpencodeAcpTranslator extends BaseAcpTranslator {
92
92
  }
93
93
  return '';
94
94
  }
95
+ /**
96
+ * opencode tool args always arrive as rawInput (`XC`/`BP`), never as content
97
+ * text. A tool_call_update's content is the shell OUTPUT snapshot (`WP`), so
98
+ * inheriting the base args-tracking would record output as if it were the
99
+ * command — the corruption this override prevents. Return undefined: nothing
100
+ * to track from content (rawInput tracking in the base still applies).
101
+ */
102
+ extractToolCallArgsText(_event) {
103
+ return undefined;
104
+ }
105
+ /**
106
+ * opencode emits the tool_call while args are still streaming: the part is
107
+ * created with `state:{status:'pending',input:{}}` (decompiled acp service),
108
+ * so the initial title falls back to the tool name (e.g. `bash`) and the card
109
+ * shows no command. The real identity arrives in later tool_call_updates:
110
+ * - running (`BP`): full rawInput + title = command/path
111
+ * - completed (`HP`): title only (no kind, no rawInput)
112
+ * Emit a tool_use patch when either carries usable data; the reducer merges
113
+ * it into the existing block by id. Name resolves from kind (`BP`) or the
114
+ * name recorded at tool_call time (`HP`).
115
+ */
116
+ buildToolUseUpdate(event) {
117
+ const raw = event.rawInput;
118
+ let input = {};
119
+ if (raw !== undefined && raw !== null) {
120
+ if (typeof raw === 'string') {
121
+ try {
122
+ input = JSON.parse(raw);
123
+ }
124
+ catch {
125
+ input = raw;
126
+ }
127
+ }
128
+ else {
129
+ input = raw;
130
+ }
131
+ }
132
+ const inputHasKeys = input !== null && typeof input === 'object' && Object.keys(input).length > 0;
133
+ const title = typeof event.title === 'string' && event.title.length > 0 ? event.title : undefined;
134
+ if (!inputHasKeys && !title)
135
+ return null;
136
+ const kind = typeof event.kind === 'string' ? event.kind : undefined;
137
+ const name = this.toolNameFromKind(kind) ?? this.getToolCallName(event.toolCallId) ?? '';
138
+ const summary = title && title !== name ? title : undefined;
139
+ return {
140
+ type: 'assistant',
141
+ timestamp: new Date().toISOString(),
142
+ message: {
143
+ content: [
144
+ {
145
+ type: 'tool_use',
146
+ id: event.toolCallId,
147
+ name,
148
+ input: inputHasKeys ? input : {},
149
+ ...(summary !== undefined ? { summary } : {}),
150
+ },
151
+ ],
152
+ },
153
+ };
154
+ }
95
155
  /** opencode usage_update also carries session cost: {cost:{amount}} (USD). */
96
156
  onUsageSample(event) {
97
157
  const cost = event.cost;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lark-remote",
3
- "version": "0.4.3",
3
+ "version": "0.4.4",
4
4
  "description": "Feishu (Lark) private-chat bridge to local coding agents — Claude Code / Codex / opencode / pi / kimi, with live-streaming CardKit 2.0 cards",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",