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 +11 -0
- package/dist/card/run-state.js +81 -31
- package/dist/card/tool-render.js +6 -2
- package/dist/runner/common/acp/base-translator.d.ts +43 -0
- package/dist/runner/common/acp/base-translator.js +83 -18
- package/dist/runner/opencode/acp/translator.d.ts +22 -1
- package/dist/runner/opencode/acp/translator.js +60 -0
- package/package.json +1 -1
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
|
### 新增
|
package/dist/card/run-state.js
CHANGED
|
@@ -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
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
600
|
-
//
|
|
601
|
-
//
|
|
602
|
-
|
|
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 =
|
|
605
|
-
if (next ===
|
|
654
|
+
const next = Math.max(priorDropped, K + String(totalDropped).length);
|
|
655
|
+
if (next === totalDropped)
|
|
606
656
|
break;
|
|
607
|
-
|
|
657
|
+
totalDropped = next;
|
|
608
658
|
}
|
|
609
|
-
const
|
|
610
|
-
const kept =
|
|
611
|
-
return
|
|
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
|
package/dist/card/tool-render.js
CHANGED
|
@@ -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
|
-
|
|
44
|
-
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
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 {
|
|
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
|
+
"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",
|