lark-remote 0.5.20 → 0.5.21
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 +10 -0
- package/dist/bridge/inbound-media.js +71 -45
- package/dist/bridge/index.d.ts +9 -0
- package/dist/bridge/index.js +230 -203
- package/dist/card/approval-render.js +32 -33
- package/dist/card/card-budget.js +123 -98
- package/dist/card/run-renderer.js +82 -68
- package/dist/card/run-state.d.ts +0 -14
- package/dist/card/run-state.js +335 -207
- package/dist/card/tool-render.js +20 -16
- package/dist/card/usage-format.d.ts +12 -9
- package/dist/card/usage-format.js +111 -97
- package/dist/common/text-width.js +36 -24
- package/dist/common/tool-summary.js +132 -90
- package/dist/index.js +226 -190
- package/dist/message-handlers.js +253 -236
- package/dist/router/commands/ls.d.ts +3 -0
- package/dist/router/commands/ls.js +256 -175
- package/dist/router/commands/order.js +184 -159
- package/dist/router/commands/resume.js +189 -133
- package/dist/router/commands/ws.js +219 -174
- package/dist/router/index.js +192 -201
- package/dist/runner/bash/runner.js +2 -4
- package/dist/runner/codex/app-server/translator.d.ts +2 -0
- package/dist/runner/codex/app-server/translator.js +287 -221
- package/dist/runner/common/acp/base-translator.js +19 -17
- package/dist/runner/common/jsonl-stream.js +154 -187
- package/dist/runner/common/jsonrpc/client.d.ts +29 -0
- package/dist/runner/common/jsonrpc/client.js +43 -12
- package/dist/runner/common/spawning-runner.d.ts +5 -0
- package/dist/runner/common/spawning-runner.js +8 -3
- package/dist/runner/common/turn-started.d.ts +8 -0
- package/dist/runner/common/turn-started.js +11 -0
- package/dist/runner/dsh/approval.js +84 -51
- package/dist/runner/dsh/client.d.ts +7 -9
- package/dist/runner/dsh/client.js +356 -287
- package/dist/runner/dsh/mux-stream.js +28 -8
- package/dist/runner/dsh/runner.d.ts +4 -3
- package/dist/runner/dsh/runner.js +88 -64
- package/dist/runner/dsh/translator.d.ts +1 -31
- package/dist/runner/dsh/translator.js +180 -178
- package/dist/runner/kimi/acp/runner.js +81 -44
- package/dist/runner/opencode/acp/error-monitor.js +5 -5
- package/dist/runner/pi/rpc/client.js +4 -13
- package/dist/runner/pi/rpc/translator.js +61 -60
- package/dist/session/claude/sessions.js +113 -101
- package/dist/session/codex/rollout-reader.js +179 -180
- package/dist/session/dsh/sessions.js +281 -231
- package/dist/session/kimi/sessions.d.ts +0 -7
- package/dist/session/kimi/sessions.js +129 -109
- package/dist/session/opencode/sessions.d.ts +0 -11
- package/dist/session/opencode/sessions.js +161 -137
- package/dist/session/session-store.d.ts +8 -0
- package/dist/session/session-store.js +98 -98
- package/package.json +1 -1
package/dist/card/run-state.js
CHANGED
|
@@ -51,77 +51,98 @@ export function reduceRunState(state, event) {
|
|
|
51
51
|
const blocks = capStaleTextBlocks(next.blocks);
|
|
52
52
|
return blocks === next.blocks ? next : { ...next, blocks };
|
|
53
53
|
}
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
|
|
54
|
+
// ── 内联分支归约器(模块顶层命名函数)──────────────────────────────────────
|
|
55
|
+
// 原来 goal_state / approval_* 五个分支直接写在 reduceRunStateInner 里,连同
|
|
56
|
+
// 内部 .map/.filter 箭头闭包一起把 CC 顶到 29。抽成命名函数后外层只留查表。
|
|
57
|
+
function reduceGoalStateEvent(state, event) {
|
|
58
|
+
const e = event;
|
|
59
|
+
// 覆盖写:每次观测都是宿主的整份快照(phase/轮次/目标全文),不做增量。
|
|
60
|
+
return { ...state, goal: e.goal ?? undefined, goalKnown: e.known };
|
|
61
|
+
}
|
|
62
|
+
function reduceApprovalRequestedEvent(state, event) {
|
|
63
|
+
const e = event;
|
|
64
|
+
const approvals = state.approvals ?? [];
|
|
65
|
+
const next = { view: e.view, expired: false };
|
|
66
|
+
let idx = -1;
|
|
67
|
+
for (let i = 0; i < approvals.length; i++) {
|
|
68
|
+
if (approvals[i].view.requestId === e.requestId) {
|
|
69
|
+
idx = i;
|
|
70
|
+
break;
|
|
71
|
+
}
|
|
61
72
|
}
|
|
62
|
-
if (
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
// 覆盖写:每次观测都是宿主的整份快照(phase/轮次/目标全文),不做增量。
|
|
68
|
-
return { ...state, goal: event.goal ?? undefined, goalKnown: event.known };
|
|
73
|
+
if (idx >= 0) {
|
|
74
|
+
// 同一 requestId 重复到达:原地替换,保持槽位顺序。
|
|
75
|
+
const replaced = approvals.slice();
|
|
76
|
+
replaced[idx] = next;
|
|
77
|
+
return { ...state, approvals: replaced };
|
|
69
78
|
}
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
return reduceToolResultEvent(state, event);
|
|
80
|
-
if (event.type === 'plan')
|
|
81
|
-
return reducePlanEvent(state, event);
|
|
82
|
-
if (event.type === 'file_change')
|
|
83
|
-
return reduceFileChangeEvent(state, event);
|
|
84
|
-
if (event.type === 'notice')
|
|
85
|
-
return reduceNoticeEvent(state, event);
|
|
86
|
-
if (event.type === 'session_info')
|
|
87
|
-
return reduceSessionInfoEvent(state, event);
|
|
88
|
-
if (event.type === 'turn_started')
|
|
89
|
-
return reduceTurnStartedEvent(state, event);
|
|
90
|
-
if (event.type === 'turn_diff')
|
|
91
|
-
return reduceTurnDiffEvent(state, event);
|
|
92
|
-
if (event.type === 'approval_requested') {
|
|
93
|
-
const approvals = state.approvals ?? [];
|
|
94
|
-
const next = { view: event.view, expired: false };
|
|
95
|
-
const idx = approvals.findIndex((a) => a.view.requestId === event.requestId);
|
|
96
|
-
if (idx >= 0) {
|
|
97
|
-
// 同一 requestId 重复到达:原地替换,保持槽位顺序。
|
|
98
|
-
return {
|
|
99
|
-
...state,
|
|
100
|
-
approvals: approvals.map((a, i) => (i === idx ? next : a)),
|
|
101
|
-
};
|
|
102
|
-
}
|
|
103
|
-
return { ...state, approvals: [...approvals, next] };
|
|
79
|
+
return { ...state, approvals: [...approvals, next] };
|
|
80
|
+
}
|
|
81
|
+
function reduceApprovalResolvedEvent(state, event) {
|
|
82
|
+
const e = event;
|
|
83
|
+
const current = state.approvals ?? [];
|
|
84
|
+
const approvals = [];
|
|
85
|
+
for (const a of current) {
|
|
86
|
+
if (a.view.requestId !== e.requestId)
|
|
87
|
+
approvals.push(a);
|
|
104
88
|
}
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
89
|
+
return { ...state, approvals };
|
|
90
|
+
}
|
|
91
|
+
function reduceApprovalViewUpdatedEvent(state, event) {
|
|
92
|
+
const e = event;
|
|
93
|
+
const current = state.approvals ?? [];
|
|
94
|
+
const approvals = [];
|
|
95
|
+
for (const a of current) {
|
|
96
|
+
approvals.push(a.view.requestId === e.requestId ? { ...a, view: e.view } : a);
|
|
110
97
|
}
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
98
|
+
return { ...state, approvals };
|
|
99
|
+
}
|
|
100
|
+
function reduceApprovalExpiredEvent(state, event) {
|
|
101
|
+
const e = event;
|
|
102
|
+
const current = state.approvals ?? [];
|
|
103
|
+
const approvals = [];
|
|
104
|
+
for (const a of current) {
|
|
105
|
+
approvals.push(a.view.requestId === e.requestId ? { ...a, expired: true } : a);
|
|
116
106
|
}
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
107
|
+
return { ...state, approvalExpired: true, approvals };
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* event.type → 归约器。用 Map 而非对象字面量:`constructor`/`toString` 这类
|
|
111
|
+
* 事件名会命中 Object.prototype。
|
|
112
|
+
*
|
|
113
|
+
* 表里的归约器各自持有窄事件参数(如 `ResultEvent`),而查表调用处按
|
|
114
|
+
* `event.type` 精确路由后传入的运行时值一定是该窄类型;这里用一次整体类型
|
|
115
|
+
* 断言把「窄参数函数」装进统一签名的表,运行时零影响(不改变任何行为)。
|
|
116
|
+
*/
|
|
117
|
+
const EVENT_REDUCERS = new Map([
|
|
118
|
+
['system', reduceSystemEvent],
|
|
119
|
+
['run_status', reduceRunStatusEvent],
|
|
120
|
+
['goal_state', reduceGoalStateEvent],
|
|
121
|
+
['background_jobs', reduceBackgroundJobsEvent],
|
|
122
|
+
['result', reduceResultEvent],
|
|
123
|
+
['final_answer', reduceFinalAnswerEvent],
|
|
124
|
+
['assistant', reduceAssistantEvent],
|
|
125
|
+
['user', reduceToolResultEvent],
|
|
126
|
+
['plan', reducePlanEvent],
|
|
127
|
+
['file_change', reduceFileChangeEvent],
|
|
128
|
+
['notice', reduceNoticeEvent],
|
|
129
|
+
['session_info', reduceSessionInfoEvent],
|
|
130
|
+
['turn_started', reduceTurnStartedEvent],
|
|
131
|
+
['turn_diff', reduceTurnDiffEvent],
|
|
132
|
+
['approval_requested', reduceApprovalRequestedEvent],
|
|
133
|
+
['approval_resolved', reduceApprovalResolvedEvent],
|
|
134
|
+
['approval_view_updated', reduceApprovalViewUpdatedEvent],
|
|
135
|
+
['approval_expired', reduceApprovalExpiredEvent],
|
|
136
|
+
]);
|
|
137
|
+
/** 单事件 → 单次状态迁移。窗口后置裁剪统一由 `reduceRunState` 做,这里不管。 */
|
|
138
|
+
function reduceRunStateInner(state, event) {
|
|
139
|
+
// 只有终态才阻止事件处理。finalizing 是非终态,
|
|
140
|
+
// 仍需累积 assistant 内容(thinking、text、tool_use、后台任务输出)。
|
|
141
|
+
if (isRunTerminal(state.terminal)) {
|
|
142
|
+
return state;
|
|
123
143
|
}
|
|
124
|
-
|
|
144
|
+
const reduce = EVENT_REDUCERS.get(event.type);
|
|
145
|
+
return reduce ? reduce(state, event) : state;
|
|
125
146
|
}
|
|
126
147
|
/** Handle app-server turn_started — records the operation kind (turn vs compact). */
|
|
127
148
|
function reduceTurnStartedEvent(state, event) {
|
|
@@ -131,130 +152,211 @@ function reduceTurnStartedEvent(state, event) {
|
|
|
131
152
|
footer: state.footer ?? 'thinking',
|
|
132
153
|
};
|
|
133
154
|
}
|
|
155
|
+
/** 恒等返回:file_change 的 fallback 与 update 都返回同一个已构造块。 */
|
|
156
|
+
function constantBlock(block) {
|
|
157
|
+
return block;
|
|
158
|
+
}
|
|
159
|
+
// -- text --
|
|
160
|
+
function textDiffMatches(event, b) {
|
|
161
|
+
return b.kind === 'text' && b.itemId === event.itemId;
|
|
162
|
+
}
|
|
163
|
+
function makeTextDiffBlock(event, ts, complete) {
|
|
164
|
+
return {
|
|
165
|
+
kind: 'text',
|
|
166
|
+
itemId: event.itemId,
|
|
167
|
+
// 最新那条正文不设窗口(它是「输出」),capStaleTextBlocks 负责在它被
|
|
168
|
+
// 更新的正文顶替后补上进度窗口。
|
|
169
|
+
content: event.text,
|
|
170
|
+
timestamp: ts,
|
|
171
|
+
...(complete ? { completedAt: ts } : {}),
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
function updateTextDiffBlock(event, ts, complete, existing) {
|
|
175
|
+
const textBlock = existing;
|
|
176
|
+
return {
|
|
177
|
+
...textBlock,
|
|
178
|
+
content: event.text,
|
|
179
|
+
// kimi:块锚点时间改为本次 diff 时间(整轮累积流不断续写,
|
|
180
|
+
// 标题应反映“最后写入时刻”而非首见时刻;move-to-end 后位置
|
|
181
|
+
// 与标题才一致)。codex app-server 不置 refreshTimestamp,
|
|
182
|
+
// 保持“首见时间戳不刷新”的既有契约。
|
|
183
|
+
...(event.refreshTimestamp ? { timestamp: ts } : {}),
|
|
184
|
+
...(complete ? { completedAt: ts } : {}),
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
function applyTextDiff(next, event) {
|
|
188
|
+
if (event.text === undefined)
|
|
189
|
+
return next;
|
|
190
|
+
const ts = event.timestamp;
|
|
191
|
+
const complete = event.complete === true;
|
|
192
|
+
const blocks = markThinkingInactive(next.blocks);
|
|
193
|
+
const capped = upsertSnapshotBlock(blocks, textDiffMatches.bind(null, event), makeTextDiffBlock.bind(null, event, ts, complete), updateTextDiffBlock.bind(null, event, ts, complete), !complete);
|
|
194
|
+
return withCappedBlocks(next, capped, 'streaming');
|
|
195
|
+
}
|
|
196
|
+
// -- reasoning --
|
|
197
|
+
function reasoningDiffMatches(event, b) {
|
|
198
|
+
return b.kind === 'thinking' && b.itemId === event.itemId;
|
|
199
|
+
}
|
|
200
|
+
function makeReasoningDiffBlock(event, ts, complete) {
|
|
201
|
+
return {
|
|
202
|
+
kind: 'thinking',
|
|
203
|
+
itemId: event.itemId,
|
|
204
|
+
content: keepTailMarked(event.reasoning, MAX_REASONING_CHARS),
|
|
205
|
+
active: !complete,
|
|
206
|
+
timestamp: ts,
|
|
207
|
+
...(complete ? { completedAt: ts } : {}),
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
function updateReasoningDiffBlock(event, ts, complete, existing) {
|
|
211
|
+
const thinkingBlock = existing;
|
|
212
|
+
return {
|
|
213
|
+
...thinkingBlock,
|
|
214
|
+
content: keepTailMarked(event.reasoning, MAX_REASONING_CHARS),
|
|
215
|
+
active: !complete,
|
|
216
|
+
...(event.refreshTimestamp ? { timestamp: ts } : {}),
|
|
217
|
+
...(complete ? { completedAt: ts } : {}),
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
// reasoning 的 applier 由下方 applySnapshotDiff + REASONING_DIFF_SPEC 提供:
|
|
221
|
+
// 原本与 applyToolOutputDiff 逐字重复(仅 guard 字段 / 三个 handler / footer
|
|
222
|
+
// 不同),现合并为单一实现,差异提到数据表。字段顺序、事件顺序逐项保持。
|
|
223
|
+
// -- toolOutput --
|
|
224
|
+
function toolDiffMatches(event, b) {
|
|
225
|
+
return b.kind === 'tool' && b.tool.id === event.itemId;
|
|
226
|
+
}
|
|
227
|
+
function makeToolDiffBlock(event, ts, complete) {
|
|
228
|
+
// 身份只在首见时写入(existing 分支不动):toolName/toolInput/toolHint。
|
|
229
|
+
const inputStr = truncateDetail(stringifyUnknown(event.toolInput ?? {}));
|
|
230
|
+
return {
|
|
231
|
+
kind: 'tool',
|
|
232
|
+
tool: {
|
|
233
|
+
id: event.itemId,
|
|
234
|
+
name: event.toolName ?? 'command',
|
|
235
|
+
input: inputStr,
|
|
236
|
+
parsedInput: asRecord(inputStr),
|
|
237
|
+
...(event.toolHint !== undefined ? { toolHint: event.toolHint } : {}),
|
|
238
|
+
output: keepTail(event.toolOutput, MAX_TOOL_DETAIL_CHARS),
|
|
239
|
+
status: complete ? (event.toolStatus ?? 'ok') : 'running',
|
|
240
|
+
startedAt: ts,
|
|
241
|
+
...(complete ? { completedAt: ts } : {}),
|
|
242
|
+
},
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
function updateToolDiffBlock(event, ts, complete, existing) {
|
|
246
|
+
const tool = existing.tool;
|
|
247
|
+
return {
|
|
248
|
+
kind: 'tool',
|
|
249
|
+
tool: {
|
|
250
|
+
...tool,
|
|
251
|
+
output: keepTail(event.toolOutput, MAX_TOOL_DETAIL_CHARS),
|
|
252
|
+
status: complete ? (event.toolStatus ?? 'ok') : tool.status,
|
|
253
|
+
...(complete ? { completedAt: ts } : {}),
|
|
254
|
+
},
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
// toolOutput 的 applier 同样由 applySnapshotDiff + TOOL_DIFF_SPEC 驱动(见下)。
|
|
258
|
+
// -- plan --
|
|
259
|
+
function planDiffMatches(event, b) {
|
|
260
|
+
return b.kind === 'plan' && b.itemId === event.itemId;
|
|
261
|
+
}
|
|
262
|
+
function makePlanDiffBlock(newPlan, event, ts, complete) {
|
|
263
|
+
return {
|
|
264
|
+
kind: 'plan',
|
|
265
|
+
itemId: event.itemId,
|
|
266
|
+
content: newPlan,
|
|
267
|
+
active: !complete,
|
|
268
|
+
timestamp: ts,
|
|
269
|
+
...(complete ? { completedAt: ts } : {}),
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
function updatePlanDiffBlock(newPlan, complete, ts, existing) {
|
|
273
|
+
const planBlock = existing;
|
|
274
|
+
return {
|
|
275
|
+
...planBlock,
|
|
276
|
+
content: newPlan,
|
|
277
|
+
active: !complete,
|
|
278
|
+
...(complete ? { completedAt: ts } : {}),
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
function applyPlanDiff(next, event) {
|
|
282
|
+
if (event.plan === undefined)
|
|
283
|
+
return next;
|
|
284
|
+
const ts = event.timestamp;
|
|
285
|
+
const complete = event.complete === true;
|
|
286
|
+
const newPlan = keepTailMarked(event.plan, MAX_REASONING_CHARS * 2);
|
|
287
|
+
const capped = upsertSnapshotBlock(next.blocks, planDiffMatches.bind(null, event), makePlanDiffBlock.bind(null, newPlan, event, ts, complete), updatePlanDiffBlock.bind(null, newPlan, complete, ts), !complete);
|
|
288
|
+
return withCappedBlocks({ ...next, plan: newPlan }, capped);
|
|
289
|
+
}
|
|
290
|
+
// -- fileChanges --
|
|
291
|
+
function makeFileChangeBlock(fc, event, ts, complete) {
|
|
292
|
+
return {
|
|
293
|
+
kind: 'file_change',
|
|
294
|
+
itemId: event.itemId,
|
|
295
|
+
path: fc.path,
|
|
296
|
+
operation: fc.kind === 'add' ? 'create' : fc.kind === 'delete' ? 'delete' : 'edit',
|
|
297
|
+
diff: fc.diff,
|
|
298
|
+
timestamp: ts,
|
|
299
|
+
...(complete ? { completedAt: ts } : {}),
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
function fileChangeMatches(fc, event, b) {
|
|
303
|
+
return b.kind === 'file_change' && b.itemId === event.itemId && b.path === fc.path;
|
|
304
|
+
}
|
|
305
|
+
function applyFileChangesDiff(next, event) {
|
|
306
|
+
if (event.fileChanges === undefined)
|
|
307
|
+
return next;
|
|
308
|
+
const ts = event.timestamp;
|
|
309
|
+
const complete = event.complete === true;
|
|
310
|
+
let blocks = next.blocks;
|
|
311
|
+
for (const fc of event.fileChanges) {
|
|
312
|
+
const fileBlock = makeFileChangeBlock(fc, event, ts, complete);
|
|
313
|
+
// 同一 item 的同一路径原地替换(权威 diff 校正),新路径/新 item 追加。
|
|
314
|
+
blocks = upsertSnapshotBlock(blocks, fileChangeMatches.bind(null, fc, event), constantBlock.bind(null, fileBlock), constantBlock.bind(null, fileBlock), !complete);
|
|
315
|
+
}
|
|
316
|
+
return withCappedBlocks(next, blocks);
|
|
317
|
+
}
|
|
318
|
+
const REASONING_DIFF_SPEC = {
|
|
319
|
+
hasPayload: (e) => e.reasoning !== undefined,
|
|
320
|
+
matches: reasoningDiffMatches,
|
|
321
|
+
make: makeReasoningDiffBlock,
|
|
322
|
+
update: updateReasoningDiffBlock,
|
|
323
|
+
footer: 'thinking',
|
|
324
|
+
};
|
|
325
|
+
const TOOL_DIFF_SPEC = {
|
|
326
|
+
hasPayload: (e) => e.toolOutput !== undefined,
|
|
327
|
+
matches: toolDiffMatches,
|
|
328
|
+
make: makeToolDiffBlock,
|
|
329
|
+
update: updateToolDiffBlock,
|
|
330
|
+
footer: 'tool_running',
|
|
331
|
+
};
|
|
332
|
+
function applySnapshotDiff(spec, next, event) {
|
|
333
|
+
if (!spec.hasPayload(event))
|
|
334
|
+
return next;
|
|
335
|
+
const ts = event.timestamp;
|
|
336
|
+
const complete = event.complete === true;
|
|
337
|
+
const capped = upsertSnapshotBlock(next.blocks, spec.matches.bind(null, event), spec.make.bind(null, event, ts, complete), spec.update.bind(null, event, ts, complete), !complete);
|
|
338
|
+
return withCappedBlocks(next, capped, spec.footer);
|
|
339
|
+
}
|
|
340
|
+
/** 五个字段按固定顺序依次应用(与原实现的 5 个 if 分支顺序一致)。 */
|
|
341
|
+
const TURN_DIFF_APPLIERS = [
|
|
342
|
+
applyTextDiff,
|
|
343
|
+
applySnapshotDiff.bind(null, REASONING_DIFF_SPEC),
|
|
344
|
+
applySnapshotDiff.bind(null, TOOL_DIFF_SPEC),
|
|
345
|
+
applyPlanDiff,
|
|
346
|
+
applyFileChangesDiff,
|
|
347
|
+
];
|
|
134
348
|
/**
|
|
135
349
|
* Handle app-server turn_diff events. The translator emits FULL per-item
|
|
136
350
|
* snapshots (accumulated text/reasoning/tool output/plan, current file-change
|
|
137
351
|
* set). Each snapshot is scoped to a thread item (event.itemId); the block is
|
|
138
352
|
* replaced in place to avoid duplication, and a NEW block is appended on the
|
|
139
|
-
* item's first sight — so interleaved items keep their real chronology and
|
|
140
|
-
*
|
|
353
|
+
* item's first sight — so interleaved items keep their real chronology and the
|
|
354
|
+
* active streaming content sits at the bottom, like the exec path.
|
|
141
355
|
*/
|
|
142
356
|
function reduceTurnDiffEvent(state, event) {
|
|
143
357
|
let next = state;
|
|
144
|
-
const
|
|
145
|
-
|
|
146
|
-
if (event.text !== undefined) {
|
|
147
|
-
const blocks = markThinkingInactive(next.blocks);
|
|
148
|
-
const capped = upsertSnapshotBlock(blocks, (b) => b.kind === 'text' && b.itemId === event.itemId, () => ({
|
|
149
|
-
kind: 'text',
|
|
150
|
-
itemId: event.itemId,
|
|
151
|
-
// 最新那条正文不设窗口(它是「输出」),capStaleTextBlocks 负责在它被
|
|
152
|
-
// 更新的正文顶替后补上进度窗口。
|
|
153
|
-
content: event.text,
|
|
154
|
-
timestamp: ts,
|
|
155
|
-
...(complete ? { completedAt: ts } : {}),
|
|
156
|
-
}), (existing) => {
|
|
157
|
-
const textBlock = existing;
|
|
158
|
-
return {
|
|
159
|
-
...textBlock,
|
|
160
|
-
content: event.text,
|
|
161
|
-
// kimi:块锚点时间改为本次 diff 时间(整轮累积流不断续写,
|
|
162
|
-
// 标题应反映“最后写入时刻”而非首见时刻;move-to-end 后位置
|
|
163
|
-
// 与标题才一致)。codex app-server 不置 refreshTimestamp,
|
|
164
|
-
// 保持“首见时间戳不刷新”的既有契约。
|
|
165
|
-
...(event.refreshTimestamp ? { timestamp: ts } : {}),
|
|
166
|
-
...(complete ? { completedAt: ts } : {}),
|
|
167
|
-
};
|
|
168
|
-
}, !complete);
|
|
169
|
-
next = withCappedBlocks(next, capped, 'streaming');
|
|
170
|
-
}
|
|
171
|
-
if (event.reasoning !== undefined) {
|
|
172
|
-
const capped = upsertSnapshotBlock(next.blocks, (b) => b.kind === 'thinking' && b.itemId === event.itemId, () => ({
|
|
173
|
-
kind: 'thinking',
|
|
174
|
-
itemId: event.itemId,
|
|
175
|
-
content: keepTailMarked(event.reasoning, MAX_REASONING_CHARS),
|
|
176
|
-
active: !complete,
|
|
177
|
-
timestamp: ts,
|
|
178
|
-
...(complete ? { completedAt: ts } : {}),
|
|
179
|
-
}), (existing) => {
|
|
180
|
-
const thinkingBlock = existing;
|
|
181
|
-
return {
|
|
182
|
-
...thinkingBlock,
|
|
183
|
-
content: keepTailMarked(event.reasoning, MAX_REASONING_CHARS),
|
|
184
|
-
active: !complete,
|
|
185
|
-
...(event.refreshTimestamp ? { timestamp: ts } : {}),
|
|
186
|
-
...(complete ? { completedAt: ts } : {}),
|
|
187
|
-
};
|
|
188
|
-
}, !complete);
|
|
189
|
-
next = withCappedBlocks(next, capped, 'thinking');
|
|
190
|
-
}
|
|
191
|
-
if (event.toolOutput !== undefined) {
|
|
192
|
-
const capped = upsertSnapshotBlock(next.blocks, (b) => b.kind === 'tool' && b.tool.id === event.itemId, () => {
|
|
193
|
-
// 身份只在首见时写入(existing 分支不动):toolName/toolInput/toolHint。
|
|
194
|
-
const inputStr = truncateDetail(stringifyUnknown(event.toolInput ?? {}));
|
|
195
|
-
return {
|
|
196
|
-
kind: 'tool',
|
|
197
|
-
tool: {
|
|
198
|
-
id: event.itemId,
|
|
199
|
-
name: event.toolName ?? 'command',
|
|
200
|
-
input: inputStr,
|
|
201
|
-
parsedInput: asRecord(inputStr),
|
|
202
|
-
...(event.toolHint !== undefined ? { toolHint: event.toolHint } : {}),
|
|
203
|
-
output: keepTail(event.toolOutput, MAX_TOOL_DETAIL_CHARS),
|
|
204
|
-
status: complete ? (event.toolStatus ?? 'ok') : 'running',
|
|
205
|
-
startedAt: ts,
|
|
206
|
-
...(complete ? { completedAt: ts } : {}),
|
|
207
|
-
},
|
|
208
|
-
};
|
|
209
|
-
}, (existing) => ({
|
|
210
|
-
kind: 'tool',
|
|
211
|
-
tool: {
|
|
212
|
-
...existing.tool,
|
|
213
|
-
output: keepTail(event.toolOutput, MAX_TOOL_DETAIL_CHARS),
|
|
214
|
-
status: complete
|
|
215
|
-
? (event.toolStatus ?? 'ok')
|
|
216
|
-
: existing.tool.status,
|
|
217
|
-
...(complete ? { completedAt: ts } : {}),
|
|
218
|
-
},
|
|
219
|
-
}), !complete);
|
|
220
|
-
next = withCappedBlocks(next, capped, 'tool_running');
|
|
221
|
-
}
|
|
222
|
-
if (event.plan !== undefined) {
|
|
223
|
-
const newPlan = keepTailMarked(event.plan, MAX_REASONING_CHARS * 2);
|
|
224
|
-
const capped = upsertSnapshotBlock(next.blocks, (b) => b.kind === 'plan' && b.itemId === event.itemId, () => ({
|
|
225
|
-
kind: 'plan',
|
|
226
|
-
itemId: event.itemId,
|
|
227
|
-
content: newPlan,
|
|
228
|
-
active: !complete,
|
|
229
|
-
timestamp: ts,
|
|
230
|
-
...(complete ? { completedAt: ts } : {}),
|
|
231
|
-
}), (existing) => {
|
|
232
|
-
const planBlock = existing;
|
|
233
|
-
return {
|
|
234
|
-
...planBlock,
|
|
235
|
-
content: newPlan,
|
|
236
|
-
active: !complete,
|
|
237
|
-
...(complete ? { completedAt: ts } : {}),
|
|
238
|
-
};
|
|
239
|
-
}, !complete);
|
|
240
|
-
next = withCappedBlocks({ ...next, plan: newPlan }, capped);
|
|
241
|
-
}
|
|
242
|
-
if (event.fileChanges !== undefined) {
|
|
243
|
-
let blocks = next.blocks;
|
|
244
|
-
for (const fc of event.fileChanges) {
|
|
245
|
-
const fileBlock = {
|
|
246
|
-
kind: 'file_change',
|
|
247
|
-
itemId: event.itemId,
|
|
248
|
-
path: fc.path,
|
|
249
|
-
operation: fc.kind === 'add' ? 'create' : fc.kind === 'delete' ? 'delete' : 'edit',
|
|
250
|
-
diff: fc.diff,
|
|
251
|
-
timestamp: ts,
|
|
252
|
-
...(complete ? { completedAt: ts } : {}),
|
|
253
|
-
};
|
|
254
|
-
// 同一 item 的同一路径原地替换(权威 diff 校正),新路径/新 item 追加。
|
|
255
|
-
blocks = upsertSnapshotBlock(blocks, (b) => b.kind === 'file_change' && b.itemId === event.itemId && b.path === fc.path, () => fileBlock, () => fileBlock, !complete);
|
|
256
|
-
}
|
|
257
|
-
next = withCappedBlocks(next, blocks);
|
|
358
|
+
for (const applier of TURN_DIFF_APPLIERS) {
|
|
359
|
+
next = applier(next, event);
|
|
258
360
|
}
|
|
259
361
|
return next;
|
|
260
362
|
}
|
|
@@ -452,43 +554,69 @@ function bridgeJobStatus(status) {
|
|
|
452
554
|
*
|
|
453
555
|
* 无变化时返回**同一个 state 引用**:bridge 靠它跳过无谓的卡片 patch。
|
|
454
556
|
*/
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
const
|
|
557
|
+
/** 单个已跟踪 job 与 roster 行的对账(原 tracked.map 回调)。 */
|
|
558
|
+
function syncTrackedJob(job, row) {
|
|
559
|
+
if (!row)
|
|
560
|
+
return job.status === 'ended' ? job : { ...job, status: 'ended' };
|
|
561
|
+
if (job.status === 'ended')
|
|
562
|
+
return job;
|
|
563
|
+
const status = row.status === undefined ? job.status : bridgeJobStatus(row.status);
|
|
564
|
+
const label = row.label.trim() === '' ? job.label : row.label;
|
|
565
|
+
const progress = row.progress ?? job.progress;
|
|
566
|
+
if (status === job.status && label === job.label && progress === job.progress)
|
|
567
|
+
return job;
|
|
568
|
+
return {
|
|
569
|
+
...job,
|
|
570
|
+
label,
|
|
571
|
+
...(status === undefined ? {} : { status }),
|
|
572
|
+
...(progress === undefined ? {} : { progress }),
|
|
573
|
+
};
|
|
574
|
+
}
|
|
575
|
+
/** roster 里新出现且仍在跑的 job(原新增循环)。 */
|
|
576
|
+
function collectNewJobs(tracked, roster) {
|
|
577
|
+
const seen = new Set();
|
|
578
|
+
for (const job of tracked)
|
|
579
|
+
seen.add(job.id);
|
|
580
|
+
const added = [];
|
|
479
581
|
for (const row of roster) {
|
|
480
582
|
if (seen.has(row.id))
|
|
481
583
|
continue;
|
|
482
584
|
if (row.status !== undefined && !RUNNING_JOB_STATUSES.has(row.status))
|
|
483
585
|
continue;
|
|
484
|
-
|
|
586
|
+
added.push({
|
|
485
587
|
id: row.id,
|
|
486
588
|
label: row.label,
|
|
487
589
|
...(row.status === undefined ? {} : { status: row.status }),
|
|
488
590
|
...(row.progress === undefined ? {} : { progress: row.progress }),
|
|
489
591
|
});
|
|
490
592
|
}
|
|
491
|
-
|
|
593
|
+
return added;
|
|
594
|
+
}
|
|
595
|
+
/** next 与 tracked 是否逐项同一引用(原 length + every 判定)。 */
|
|
596
|
+
function jobsUnchanged(next, tracked) {
|
|
597
|
+
if (next.length !== tracked.length)
|
|
598
|
+
return false;
|
|
599
|
+
for (let i = 0; i < next.length; i++) {
|
|
600
|
+
if (next[i] !== tracked[i])
|
|
601
|
+
return false;
|
|
602
|
+
}
|
|
603
|
+
return true;
|
|
604
|
+
}
|
|
605
|
+
export function syncBackgroundJobs(state, roster) {
|
|
606
|
+
const tracked = state.backgroundJobs ?? [];
|
|
607
|
+
const byId = new Map();
|
|
608
|
+
for (const job of roster)
|
|
609
|
+
byId.set(job.id, job);
|
|
610
|
+
const next = [];
|
|
611
|
+
for (const job of tracked) {
|
|
612
|
+
next.push(syncTrackedJob(job, byId.get(job.id)));
|
|
613
|
+
}
|
|
614
|
+
// 新出现的任务只收仍在跑的:与收尾采集同一口径(列出已结束的任务只会被
|
|
615
|
+
// 读成"还在跑")。
|
|
616
|
+
for (const added of collectNewJobs(tracked, roster)) {
|
|
617
|
+
next.push(added);
|
|
618
|
+
}
|
|
619
|
+
if (jobsUnchanged(next, tracked)) {
|
|
492
620
|
return state;
|
|
493
621
|
}
|
|
494
622
|
return { ...state, backgroundJobs: next };
|
package/dist/card/tool-render.js
CHANGED
|
@@ -98,11 +98,23 @@ export function toolBodyMd(tool, opts = {}) {
|
|
|
98
98
|
// 同上:走 truncate 保证切点不落在代理对中间(末尾 '…' 由 truncate 附加以对齐预算)。
|
|
99
99
|
return `${truncate(body, BODY_TOTAL_MAX)}\n\n_(body 已截断,完整内容查日志)_`;
|
|
100
100
|
}
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
101
|
+
function renderFieldInput(str, spec) {
|
|
102
|
+
const value = str(spec.key);
|
|
103
|
+
if (!value)
|
|
104
|
+
return '';
|
|
105
|
+
const shown = spec.truncate ? truncate(value, BODY_FIELD_MAX) : value;
|
|
106
|
+
return spec.fence
|
|
107
|
+
? `**${spec.label}**\n\`\`\`${spec.fence}\n${shown}\n\`\`\``
|
|
108
|
+
: `**${spec.label}** \`${shown}\``;
|
|
105
109
|
}
|
|
110
|
+
const BASH_FIELD = {
|
|
111
|
+
key: 'command',
|
|
112
|
+
label: 'Command',
|
|
113
|
+
truncate: true,
|
|
114
|
+
fence: 'bash',
|
|
115
|
+
};
|
|
116
|
+
const GLOB_FIELD = { key: 'pattern', label: 'Pattern', truncate: true };
|
|
117
|
+
const LS_FIELD = { key: 'path', label: 'Path', truncate: false };
|
|
106
118
|
function renderFileInput(str) {
|
|
107
119
|
const fp = str('file_path') || str('path');
|
|
108
120
|
return fp ? `**File** \`${fp}\`` : '';
|
|
@@ -115,14 +127,6 @@ function renderGrepInput(str) {
|
|
|
115
127
|
lines.push(`**Path** \`${str('path')}\``);
|
|
116
128
|
return lines.join('\n');
|
|
117
129
|
}
|
|
118
|
-
function renderGlobInput(str) {
|
|
119
|
-
const p = str('pattern');
|
|
120
|
-
return p ? `**Pattern** \`${truncate(p, BODY_FIELD_MAX)}\`` : '';
|
|
121
|
-
}
|
|
122
|
-
function renderLsInput(str) {
|
|
123
|
-
const p = str('path');
|
|
124
|
-
return p ? `**Path** \`${p}\`` : '';
|
|
125
|
-
}
|
|
126
130
|
function renderWebFetchInput(str) {
|
|
127
131
|
return str('url') ? `**URL** ${str('url')}` : '';
|
|
128
132
|
}
|
|
@@ -149,7 +153,7 @@ function renderInput(tool) {
|
|
|
149
153
|
// 完全一致。hint 分支与 name case 共用同一渲染函数,无复制粘贴。
|
|
150
154
|
switch (tool.toolHint) {
|
|
151
155
|
case 'shell':
|
|
152
|
-
return
|
|
156
|
+
return renderFieldInput(str, BASH_FIELD);
|
|
153
157
|
case 'file':
|
|
154
158
|
case 'patch':
|
|
155
159
|
return renderFileInput(str);
|
|
@@ -162,15 +166,15 @@ function renderInput(tool) {
|
|
|
162
166
|
}
|
|
163
167
|
switch (toolKindOf(tool.name)) {
|
|
164
168
|
case 'shell':
|
|
165
|
-
return
|
|
169
|
+
return renderFieldInput(str, BASH_FIELD);
|
|
166
170
|
case 'file':
|
|
167
171
|
return renderFileInput(str);
|
|
168
172
|
case 'grep':
|
|
169
173
|
return renderGrepInput(str);
|
|
170
174
|
case 'glob':
|
|
171
|
-
return
|
|
175
|
+
return renderFieldInput(str, GLOB_FIELD);
|
|
172
176
|
case 'ls':
|
|
173
|
-
return
|
|
177
|
+
return renderFieldInput(str, LS_FIELD);
|
|
174
178
|
case 'webfetch':
|
|
175
179
|
return renderWebFetchInput(str);
|
|
176
180
|
case 'websearch':
|