@best-agent/cli 0.0.2-beta.8 → 0.0.2-beta.9

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 (70) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +44 -181
  3. package/dist/launcher.js +32 -0
  4. package/package.json +15 -27
  5. package/dist/best-agent-native-bridge.d.ts +0 -389
  6. package/dist/best-agent-native-bridge.js +0 -4645
  7. package/dist/cli-storage.d.ts +0 -5
  8. package/dist/cli-storage.js +0 -7
  9. package/dist/dimsdk/ask-user.d.ts +0 -21
  10. package/dist/dimsdk/ask-user.js +0 -131
  11. package/dist/dimsdk/cli/version.d.ts +0 -6
  12. package/dist/dimsdk/cli/version.js +0 -36
  13. package/dist/dimsdk/compat/archships/dim-agent/runtime-host.d.ts +0 -49
  14. package/dist/dimsdk/compat/archships/dim-agent/runtime-host.js +0 -215
  15. package/dist/dimsdk/compat/runtime-host.d.ts +0 -2
  16. package/dist/dimsdk/compat/runtime-host.js +0 -2
  17. package/dist/dimsdk/core/debug-log.d.ts +0 -8
  18. package/dist/dimsdk/core/debug-log.js +0 -100
  19. package/dist/dimsdk/core/env.d.ts +0 -26
  20. package/dist/dimsdk/core/env.js +0 -130
  21. package/dist/dimsdk/core/markdown/sanitize.d.ts +0 -2
  22. package/dist/dimsdk/core/markdown/sanitize.js +0 -43
  23. package/dist/dimsdk/core/markdown/wrap.d.ts +0 -9
  24. package/dist/dimsdk/core/markdown/wrap.js +0 -139
  25. package/dist/dimsdk/core/markdown.d.ts +0 -18
  26. package/dist/dimsdk/core/markdown.js +0 -62
  27. package/dist/dimsdk/core/model.d.ts +0 -273
  28. package/dist/dimsdk/core/model.js +0 -16
  29. package/dist/dimsdk/core/observability/perf-log.d.ts +0 -18
  30. package/dist/dimsdk/core/observability/perf-log.js +0 -236
  31. package/dist/dimsdk/core/store/agent-event-mappers.d.ts +0 -26
  32. package/dist/dimsdk/core/store/agent-event-mappers.js +0 -109
  33. package/dist/dimsdk/core/store/message-mutations.d.ts +0 -105
  34. package/dist/dimsdk/core/store/message-mutations.js +0 -1528
  35. package/dist/dimsdk/core/store/semantic-readers.d.ts +0 -2
  36. package/dist/dimsdk/core/store/semantic-readers.js +0 -4
  37. package/dist/dimsdk/core/store/transcript-projector.d.ts +0 -164
  38. package/dist/dimsdk/core/store/transcript-projector.js +0 -580
  39. package/dist/dimsdk/core/tool-metadata.d.ts +0 -12
  40. package/dist/dimsdk/core/tool-metadata.js +0 -70
  41. package/dist/dimsdk/core/transcript-part-id.d.ts +0 -6
  42. package/dist/dimsdk/core/transcript-part-id.js +0 -22
  43. package/dist/dimsdk/i18n/index.d.ts +0 -18
  44. package/dist/dimsdk/i18n/index.js +0 -141
  45. package/dist/dimsdk/i18n/locales/en.d.ts +0 -988
  46. package/dist/dimsdk/i18n/locales/en.js +0 -987
  47. package/dist/dimsdk/i18n/locales/index.d.ts +0 -1976
  48. package/dist/dimsdk/i18n/locales/index.js +0 -8
  49. package/dist/dimsdk/i18n/locales/zh-CN.d.ts +0 -988
  50. package/dist/dimsdk/i18n/locales/zh-CN.js +0 -987
  51. package/dist/dimsdk/i18n/types.d.ts +0 -13
  52. package/dist/dimsdk/i18n/types.js +0 -2
  53. package/dist/dimsdk/shared/value-normalizers.d.ts +0 -7
  54. package/dist/dimsdk/shared/value-normalizers.js +0 -48
  55. package/dist/dimsdk-tui.bundle-size.json +0 -169
  56. package/dist/dimsdk-tui.bundle.js +0 -330756
  57. package/dist/goatchain-adapter.d.ts +0 -41
  58. package/dist/goatchain-adapter.js +0 -499
  59. package/dist/index.d.ts +0 -3
  60. package/dist/index.js +0 -1405
  61. package/dist/native-runtime-bridge.d.ts +0 -387
  62. package/dist/native-runtime-bridge.js +0 -4624
  63. package/dist/provider-config-env.d.ts +0 -7
  64. package/dist/provider-config-env.js +0 -286
  65. package/dist/provider-model-limit-index.generated.d.ts +0 -3
  66. package/dist/provider-model-limit-index.generated.js +0 -7818
  67. package/dist/provider-model-limits.d.ts +0 -8
  68. package/dist/provider-model-limits.js +0 -43
  69. package/dist/runtime.d.ts +0 -47
  70. package/dist/runtime.js +0 -516
@@ -1,1528 +0,0 @@
1
- import { isRecord } from "@best-agent/foundation";
2
- import { createEnvReader, readProcessEnv } from "../env.js";
3
- import { splitMarkdownByFencedCodeBlocks } from "../markdown.js";
4
- import { isOneShotEnabled, isPerfEnabled, perfLog as perfLogLine, } from "../observability/perf-log.js";
5
- import { createChecklistPartId, createLocalInteractivePartId, createLocalThinkingPartId, } from "../transcript-part-id.js";
6
- import { isTerminalStatusText, t } from "../../i18n/index.js";
7
- import { computed } from "vue";
8
- function getRuntimeParts(message) {
9
- return message.parts;
10
- }
11
- function isRuntimeMarkdownPart(part) {
12
- return part?.type === "markdown";
13
- }
14
- function isApprovalForToolCall(part, toolCallId) {
15
- return part?.type === "approve" && part.request.toolCallId === toolCallId;
16
- }
17
- function isToolOutputForCall(part, toolCallId) {
18
- return part?.type === "tool_output" && part.toolCallId === toolCallId;
19
- }
20
- function isToolResultForCall(part, toolCallId) {
21
- return part?.type === "tool_result" && part.result.toolCallId === toolCallId;
22
- }
23
- function findToolCallPartIndex(parts, callId) {
24
- for (let i = parts.length - 1; i >= 0; i--) {
25
- const part = parts[i];
26
- if (part?.type === "tool_call" && part.call.id === callId)
27
- return i;
28
- }
29
- return -1;
30
- }
31
- function findToolOutputPartIndex(parts, toolCallId) {
32
- for (let i = parts.length - 1; i >= 0; i--) {
33
- const part = parts[i];
34
- if (part?.type === "tool_output" && part.toolCallId === toolCallId)
35
- return i;
36
- }
37
- return -1;
38
- }
39
- function getScopedSubagentId(part) {
40
- const value = part._subagentId ??
41
- (part.type === "tool_call" ? part.call.subagentId : undefined) ??
42
- (part.type === "tool_result" ? part.result.subagentId : undefined) ??
43
- (part.type === "tool_output" ? part.subagentId : undefined) ??
44
- (part.type === "approve" ? part.request.subagentId : undefined);
45
- return String(value ?? "").trim();
46
- }
47
- function isChecklistPartOfKind(part, kind) {
48
- return part.type === kind;
49
- }
50
- function createChecklistPart(kind, input) {
51
- return kind === "plan" ? { type: "plan", ...input } : { type: "todo", ...input };
52
- }
53
- export function createMessageMutations(state, hooks = {}) {
54
- const userHistoryIndexByMessageId = new Map();
55
- const env = createEnvReader(readProcessEnv());
56
- const perfEnabled = isPerfEnabled();
57
- const perfStreamEnabled = perfEnabled && (isOneShotEnabled() || env.prefixedStr("PROFILE_STREAM_MUTATIONS") === "1");
58
- let lastStreamSlowLogAt = 0;
59
- function perfLogStream(line) {
60
- if (!perfEnabled)
61
- return;
62
- perfLogLine(line);
63
- }
64
- const streamMarkdownTailLimit = (() => {
65
- const raw = env.prefixedStr("STREAM_MARKDOWN_TAIL_CHARS");
66
- const n = Number(raw);
67
- if (Number.isFinite(n) && n > 0)
68
- return Math.min(5_000_000, Math.max(2000, Math.floor(n)));
69
- return 30_000;
70
- })();
71
- const streamMarkdownChunkThreshold = (() => {
72
- const raw = env.prefixedStr("STREAM_MARKDOWN_CHUNK_THRESHOLD_CHARS");
73
- const n = Number(raw);
74
- if (Number.isFinite(n) && n > 0)
75
- return Math.min(5_000_000, Math.max(2000, Math.floor(n)));
76
- return 20_000;
77
- })();
78
- const streamToolOutputTailLimit = (() => {
79
- const raw = env.prefixedStr("STREAM_TOOL_OUTPUT_TAIL_CHARS");
80
- const n = Number(raw);
81
- if (Number.isFinite(n) && n > 0)
82
- return Math.min(5_000_000, Math.max(2000, Math.floor(n)));
83
- return 30_000;
84
- })();
85
- const streamToolOutputTailLinesLimit = (() => {
86
- const raw = env.prefixedStr("STREAM_TOOL_OUTPUT_TAIL_LINES");
87
- const n = Number(raw);
88
- if (Number.isFinite(n) && n > 0)
89
- return Math.min(50_000, Math.max(20, Math.floor(n)));
90
- return 2000;
91
- })();
92
- const streamToolArgsTailLimit = (() => {
93
- const raw = env.prefixedStr("STREAM_TOOL_ARGS_TAIL_CHARS");
94
- const n = Number(raw);
95
- if (Number.isFinite(n) && n > 0)
96
- return Math.min(5_000_000, Math.max(2000, Math.floor(n)));
97
- return 30_000;
98
- })();
99
- let messageIndexById = null;
100
- function rebuildMessageIndex() {
101
- const next = new Map();
102
- for (let i = 0; i < state.messages.length; i += 1) {
103
- const id = String(state.messages[i]?.id ?? "").trim();
104
- if (id)
105
- next.set(id, i);
106
- }
107
- messageIndexById = next;
108
- return next;
109
- }
110
- function messageIndex(messageId) {
111
- const id = String(messageId ?? "").trim();
112
- if (!id)
113
- return -1;
114
- let index = messageIndexById ?? rebuildMessageIndex();
115
- const found = index.get(id);
116
- if (found !== undefined &&
117
- found >= 0 &&
118
- found < state.messages.length &&
119
- String(state.messages[found]?.id ?? "").trim() === id) {
120
- return found;
121
- }
122
- index = rebuildMessageIndex();
123
- return index.get(id) ?? -1;
124
- }
125
- function messageById(messageId) {
126
- const idx = messageIndex(messageId);
127
- return idx >= 0 ? state.messages[idx] : undefined;
128
- }
129
- function getMarkdownCodeBlockHash(language, code) {
130
- const str = `${String(language ?? "").trim() || "text"}\0${String(code ?? "").replace(/\r/g, "")}`;
131
- let hash = 0;
132
- for (let i = 0; i < str.length; i++) {
133
- const ch = str.charCodeAt(i);
134
- hash = (hash << 5) - hash + ch;
135
- hash = hash & hash;
136
- }
137
- return hash.toString(36);
138
- }
139
- function pruneMarkdownCodeBlockPreviews(markdown, existing) {
140
- if (!Array.isArray(existing) || existing.length === 0)
141
- return undefined;
142
- const codeBlocks = splitMarkdownByFencedCodeBlocks(markdown).filter((block) => block.type === "code");
143
- if (codeBlocks.length === 0)
144
- return undefined;
145
- const max = Math.min(existing.length, codeBlocks.length);
146
- let keep = 0;
147
- for (; keep < max; keep++) {
148
- const preview = existing[keep];
149
- const block = codeBlocks[keep];
150
- if (!preview || !block)
151
- break;
152
- const language = String(block.language ?? "").trim() || "text";
153
- const code = String(block.code ?? "").replace(/\r/g, "");
154
- const totalLines = code.split("\n").length;
155
- if (preview.hash !== getMarkdownCodeBlockHash(language, code) ||
156
- preview.totalLines !== totalLines) {
157
- break;
158
- }
159
- }
160
- return keep > 0 ? existing.slice(0, keep) : undefined;
161
- }
162
- function countNewlines(text) {
163
- const s = String(text ?? "");
164
- if (!s)
165
- return 0;
166
- let n = 0;
167
- for (let i = 0; i < s.length; i++) {
168
- // '\n'
169
- if (s.charCodeAt(i) === 10)
170
- n++;
171
- }
172
- return n;
173
- }
174
- function computeTailLinesFromTailText(tail, maxLines) {
175
- const cap = Math.max(1, Math.floor(maxLines) + 1);
176
- const segs = String(tail ?? "")
177
- .replace(/\r/g, "")
178
- .split("\n");
179
- if (segs.length > cap)
180
- return segs.slice(-cap);
181
- return segs;
182
- }
183
- function appendTailLines(prevLines, delta, maxLines, maxChars) {
184
- const parts = String(delta ?? "")
185
- .replace(/\r/g, "")
186
- .split("\n");
187
- let lines = prevLines.length > 0 ? prevLines.slice() : [""];
188
- lines[lines.length - 1] = `${lines[lines.length - 1] ?? ""}${parts[0] ?? ""}`;
189
- for (let i = 1; i < parts.length; i++)
190
- lines.push(parts[i] ?? "");
191
- const charCap = Math.max(0, Math.floor(maxChars));
192
- let totalChars = Math.max(0, lines.length - 1);
193
- for (const line of lines)
194
- totalChars += line.length;
195
- if (totalChars > charCap) {
196
- let removeChars = totalChars - charCap;
197
- let start = 0;
198
- while (start < lines.length && removeChars > (lines[start]?.length ?? 0)) {
199
- removeChars -= lines[start]?.length ?? 0;
200
- start++;
201
- if (start < lines.length)
202
- removeChars--;
203
- }
204
- lines = lines.slice(start);
205
- if (lines.length === 0)
206
- return [""];
207
- if (removeChars > 0)
208
- lines[0] = lines[0]?.slice(removeChars) ?? "";
209
- }
210
- const cap = Math.max(1, Math.floor(maxLines) + 1);
211
- if (lines.length > cap)
212
- return lines.slice(-cap);
213
- return lines;
214
- }
215
- function normalizeUserHistoryText(v) {
216
- const s = String(v ?? "");
217
- // Match ChatPage behavior: normalize newlines + trim + ignore empty.
218
- return s.replace(/\r\n?/g, "\n").trim();
219
- }
220
- function rebuildUserHistory() {
221
- userHistoryIndexByMessageId.clear();
222
- state.userHistory.length = 0;
223
- for (const m of state.messages) {
224
- if (!m || m.role !== "user")
225
- continue;
226
- const normalized = normalizeUserHistoryText(m.content);
227
- if (!normalized)
228
- continue;
229
- const idx = state.userHistory.length;
230
- state.userHistory.push(normalized);
231
- userHistoryIndexByMessageId.set(m.id, idx);
232
- }
233
- }
234
- function upsertUserHistoryForMessage(m) {
235
- if (!m || m.role !== "user")
236
- return;
237
- const id = String(m.id ?? "").trim();
238
- if (!id)
239
- return;
240
- const normalized = normalizeUserHistoryText(m.content);
241
- const existing = userHistoryIndexByMessageId.get(id);
242
- if (!normalized) {
243
- if (existing == null)
244
- return;
245
- // Removing an entry shifts indices; rebuild to keep the map consistent.
246
- rebuildUserHistory();
247
- return;
248
- }
249
- if (existing == null) {
250
- const idx = state.userHistory.length;
251
- state.userHistory.push(normalized);
252
- userHistoryIndexByMessageId.set(id, idx);
253
- return;
254
- }
255
- if (state.userHistory[existing] !== normalized)
256
- state.userHistory[existing] = normalized;
257
- }
258
- function markLayoutDirty(messageId) {
259
- const id = String(messageId ?? "").trim();
260
- if (!id)
261
- return;
262
- state.layoutDirtyMessageId = id;
263
- state.layoutDirtySeq++;
264
- }
265
- function normalizeInteractivePartIds(message) {
266
- if (message.role !== "assistant")
267
- return message;
268
- let thinkingOrdinal = 0;
269
- let todoOrdinal = 0;
270
- let planOrdinal = 0;
271
- let changed = false;
272
- const parts = message.parts.map((part) => {
273
- if (part.type === "thinking") {
274
- const existingPartId = String(part.partId ?? "").trim();
275
- const partId = existingPartId || createLocalThinkingPartId(message.id, thinkingOrdinal);
276
- thinkingOrdinal += 1;
277
- if (existingPartId)
278
- return part;
279
- changed = true;
280
- return { ...part, partId };
281
- }
282
- if (part.type === "todo" || part.type === "plan") {
283
- const existingPartId = String(part.partId ?? "").trim();
284
- const ordinal = part.type === "todo" ? todoOrdinal++ : planOrdinal++;
285
- const partId = existingPartId ||
286
- (part.toolCallId
287
- ? createChecklistPartId(part.type, part.toolCallId)
288
- : createLocalInteractivePartId(part.type, message.id, ordinal));
289
- if (existingPartId)
290
- return part;
291
- changed = true;
292
- return { ...part, partId };
293
- }
294
- return part;
295
- });
296
- return changed ? { ...message, parts } : message;
297
- }
298
- function pushMessage(m) {
299
- const next = normalizeInteractivePartIds(m);
300
- state.messages.push(next);
301
- const id = String(next.id ?? "").trim();
302
- if (id && messageIndexById)
303
- messageIndexById.set(id, state.messages.length - 1);
304
- upsertUserHistoryForMessage(next);
305
- hooks.onMessageAdded?.(next);
306
- }
307
- function setSessionHistoryDirty(value) {
308
- if (typeof state.sessionHistoryDirty === "boolean")
309
- state.sessionHistoryDirty = value;
310
- }
311
- function replaceMessageInternal(messageId, next, opts) {
312
- const normalizedNext = normalizeInteractivePartIds(next);
313
- const idx = messageIndex(messageId);
314
- if (idx >= 0) {
315
- const prev = state.messages[idx];
316
- state.messages[idx] = normalizedNext;
317
- if (messageIndexById) {
318
- messageIndexById.delete(String(prev.id ?? "").trim());
319
- const nextId = String(normalizedNext.id ?? "").trim();
320
- if (nextId)
321
- messageIndexById.set(nextId, idx);
322
- }
323
- hooks.onMessageReplaced?.(prev, normalizedNext);
324
- }
325
- upsertUserHistoryForMessage(normalizedNext);
326
- if (opts?.layoutDirty !== false)
327
- markLayoutDirty(messageId);
328
- }
329
- function replaceMessage(messageId, next) {
330
- replaceMessageInternal(messageId, next);
331
- }
332
- function updateAssistant(messageId, fn) {
333
- const msg = messageById(messageId);
334
- if (!msg || msg.role !== "assistant")
335
- return;
336
- const next = fn(msg);
337
- if (next === msg)
338
- return;
339
- replaceMessage(messageId, next);
340
- }
341
- function updateAssistantHidden(messageId, fn) {
342
- const msg = messageById(messageId);
343
- if (!msg || msg.role !== "assistant")
344
- return;
345
- const next = fn(msg);
346
- if (next === msg)
347
- return;
348
- replaceMessageInternal(messageId, next, { layoutDirty: false });
349
- }
350
- function setAssistantStatus(messageId, text) {
351
- updateAssistant(messageId, (m) => {
352
- const parts = m.parts.map((p) => p.type === "status" ? { type: "status", text } : p);
353
- // If a large streaming markdown part used chunked storage, finalize it once the status
354
- // becomes terminal. This ensures snapshots/persistence can capture the full output.
355
- const isTerminal = isTerminalStatusText(text);
356
- if (!isTerminal)
357
- return { ...m, parts };
358
- let changed = false;
359
- const finalized = parts.map((p) => {
360
- if (!isRuntimeMarkdownPart(p))
361
- return p;
362
- const chunks = p.streamMeta?.chunks;
363
- if (!Array.isArray(chunks) || chunks.length === 0)
364
- return p;
365
- const joined = chunks.join("");
366
- changed = true;
367
- const { streamMeta: _streamMeta, ...next } = p;
368
- return { ...next, markdown: joined };
369
- });
370
- return changed ? { ...m, parts: finalized } : { ...m, parts };
371
- });
372
- }
373
- function setAssistantThinking(assistantId, content, partId) {
374
- const nextContent = String(content ?? "");
375
- const normalizedPartId = String(partId ?? "").trim();
376
- if (!normalizedPartId)
377
- return;
378
- updateAssistant(assistantId, (m) => {
379
- const parts = getRuntimeParts(m).slice();
380
- const idx = parts.findIndex((p) => p.type === "thinking" && String(p.partId ?? "").trim() === normalizedPartId);
381
- if (idx >= 0) {
382
- const prev = parts[idx];
383
- parts[idx] = {
384
- type: "thinking",
385
- partId: prev.partId,
386
- title: prev.title,
387
- collapsed: prev.collapsed,
388
- content: nextContent,
389
- };
390
- return { ...m, parts };
391
- }
392
- const insertAt = Math.max(0, parts.findIndex((p) => p.type === "status") + 1);
393
- parts.splice(insertAt, 0, {
394
- type: "thinking",
395
- partId: normalizedPartId,
396
- title: t("chatParts.thinking"),
397
- collapsed: true,
398
- content: nextContent,
399
- });
400
- return { ...m, parts };
401
- });
402
- }
403
- /**
404
- * Start a new thinking block. Creates a fresh thinking part with empty content.
405
- * Used when a new thinking_start event is received.
406
- *
407
- * IMPORTANT: Always appends to the END of the parts array.
408
- * This ensures that thinking blocks appear in the correct temporal order
409
- * relative to other parts (like tool_calls) that are added during streaming.
410
- *
411
- * During streaming, events arrive in this order:
412
- * - thinking_start -> thinking_delta -> thinking_end (first thinking block)
413
- * - tool_call (tool call appears after first thinking)
414
- * - thinking_start -> thinking_delta -> thinking_end (second thinking block)
415
- *
416
- * By appending to the end, we preserve this temporal order.
417
- */
418
- function startNewThinkingBlock(assistantId, partId) {
419
- const normalizedPartId = String(partId ?? "").trim();
420
- if (!normalizedPartId)
421
- return;
422
- updateAssistant(assistantId, (m) => {
423
- const parts = m.parts.slice();
424
- const existingIdx = parts.findIndex((part) => part.type === "thinking" && String(part.partId ?? "").trim() === normalizedPartId);
425
- if (existingIdx >= 0)
426
- return m;
427
- // Always append to the end to preserve temporal order during streaming
428
- // This ensures thinking blocks appear in the correct sequence relative
429
- // to tool_calls and other parts that are added during streaming
430
- // Default collapsed; user can toggle to expand.
431
- parts.push({
432
- type: "thinking",
433
- partId: normalizedPartId,
434
- title: t("chatParts.thinking"),
435
- collapsed: true,
436
- content: "",
437
- });
438
- return { ...m, parts };
439
- });
440
- }
441
- /**
442
- * End a thinking block by optionally updating content.
443
- * Used when a thinking_end event is received.
444
- *
445
- * @param assistantId - The ID of the assistant message
446
- * @param partId - The stable identity of the thinking block
447
- * @param content - Optional final content to set on the thinking block
448
- */
449
- function endThinkingBlock(assistantId, partId, content) {
450
- const normalizedPartId = String(partId ?? "").trim();
451
- if (!normalizedPartId)
452
- return;
453
- updateAssistant(assistantId, (m) => {
454
- const parts = m.parts.slice();
455
- const idx = parts.findIndex((p) => p.type === "thinking" && String(p.partId ?? "").trim() === normalizedPartId);
456
- if (idx >= 0) {
457
- const prev = parts[idx];
458
- parts[idx] = {
459
- type: "thinking",
460
- partId: prev.partId,
461
- title: prev.title,
462
- collapsed: prev.collapsed,
463
- content: content !== undefined ? content : prev.content,
464
- };
465
- return { ...m, parts };
466
- }
467
- return m;
468
- });
469
- }
470
- function appendAssistantMarkdown(assistantId, delta) {
471
- const d = String(delta ?? "");
472
- if (!d)
473
- return;
474
- updateAssistant(assistantId, (m) => {
475
- const last = m.parts[m.parts.length - 1];
476
- if (isRuntimeMarkdownPart(last)) {
477
- const prev = last;
478
- const buildNextPart = (markdown, streamMeta) => {
479
- const nextCodeBlocks = pruneMarkdownCodeBlockPreviews(markdown, Array.isArray(prev.codeBlocks) ? prev.codeBlocks : undefined);
480
- const nextPart = {
481
- ...prev,
482
- type: "markdown",
483
- markdown,
484
- streamMeta,
485
- };
486
- if (nextCodeBlocks) {
487
- return { ...nextPart, codeBlocks: nextCodeBlocks };
488
- }
489
- delete nextPart.codeBlocks;
490
- return nextPart;
491
- };
492
- const prevChunks = prev.streamMeta?.chunks;
493
- if (Array.isArray(prevChunks)) {
494
- // Chunked mode: keep full content as chunks, but only maintain a small tail string
495
- // to avoid O(n^2) string concatenation as the message grows.
496
- const chunks = [...prevChunks, d];
497
- const nextTail = `${String(prev.markdown ?? "")}${d}`;
498
- const markdown = nextTail.length > streamMarkdownTailLimit
499
- ? nextTail.slice(-streamMarkdownTailLimit)
500
- : nextTail;
501
- getRuntimeParts(m)[m.parts.length - 1] = buildNextPart(markdown, {
502
- chunks,
503
- fullLen: Number(prev.streamMeta?.fullLen ?? 0) + d.length,
504
- tailLimit: streamMarkdownTailLimit,
505
- appendSeq: Number(prev.streamMeta?.appendSeq ?? 0) + 1,
506
- lastAppendDelta: d,
507
- });
508
- return { ...m, parts: m.parts };
509
- }
510
- const prevText = String(prev.markdown ?? "");
511
- const nextText = `${prevText}${d}`;
512
- if (nextText.length >= streamMarkdownChunkThreshold) {
513
- const chunks = [prevText, d];
514
- const markdown = nextText.length > streamMarkdownTailLimit
515
- ? nextText.slice(-streamMarkdownTailLimit)
516
- : nextText;
517
- getRuntimeParts(m)[m.parts.length - 1] = buildNextPart(markdown, {
518
- chunks,
519
- fullLen: nextText.length,
520
- tailLimit: streamMarkdownTailLimit,
521
- appendSeq: Number(prev.streamMeta?.appendSeq ?? 0) + 1,
522
- lastAppendDelta: d,
523
- });
524
- return { ...m, parts: m.parts };
525
- }
526
- // Small markdown: keep as a single string.
527
- // Streaming optimization: mutate in-place to avoid allocating a new parts array per chunk.
528
- getRuntimeParts(m)[m.parts.length - 1] = buildNextPart(nextText, {
529
- appendSeq: Number(prev.streamMeta?.appendSeq ?? 0) + 1,
530
- lastAppendDelta: d,
531
- });
532
- return { ...m, parts: m.parts };
533
- }
534
- // Add new part (also in-place to avoid allocations during streaming).
535
- getRuntimeParts(m).push({
536
- type: "markdown",
537
- markdown: d,
538
- streamMeta: {
539
- appendSeq: 1,
540
- lastAppendDelta: d,
541
- },
542
- });
543
- return { ...m, parts: m.parts };
544
- });
545
- }
546
- function appendInfoPart(assistantId, text, meta) {
547
- const t = String(text ?? "").trim();
548
- const hasReasoningMeta = typeof meta?._reasoning === "string";
549
- if (!t && !hasReasoningMeta)
550
- return;
551
- updateAssistant(assistantId, (m) => {
552
- getRuntimeParts(m).push({ type: "info", text: t, ...meta });
553
- return { ...m, parts: m.parts };
554
- });
555
- }
556
- function applySubagentInfoToParts(parts, input) {
557
- const subagentId = String(input.subagentId ?? "").trim();
558
- const t = String(input.text ?? "").trim();
559
- if (!subagentId || !t)
560
- return false;
561
- const metaTurnId = String(input.meta?.subagentTurnId ?? "").trim();
562
- const metaStream = String(input.meta?.subagentStream ?? "").trim();
563
- const sameInfo = (part, infoKind) => Boolean(part?.type === "info" &&
564
- part.text === t &&
565
- part._subagent === infoKind &&
566
- part._subagentId === subagentId &&
567
- String(part._subagentTurnId ?? "").trim() === metaTurnId &&
568
- String(part._subagentStream ?? "").trim() === metaStream);
569
- // Find the insertion point: right after the last part belonging to
570
- // this subagentId. This keeps header + the single live activity line
571
- // grouped together even when multiple subagents run concurrently.
572
- const findInsertAfter = () => {
573
- for (let i = parts.length - 1; i >= 0; i--) {
574
- if (parts[i]?._subagentId === subagentId)
575
- return i + 1;
576
- }
577
- return parts.length;
578
- };
579
- const findHeaderIndex = () => {
580
- for (let i = parts.length - 1; i >= 0; i--) {
581
- if (parts[i]?.type === "info" &&
582
- parts[i]?._subagent === "header" &&
583
- parts[i]?._subagentId === subagentId) {
584
- return i;
585
- }
586
- }
587
- return -1;
588
- };
589
- const removeInfoParts = (infoKind) => {
590
- let removed = false;
591
- for (let i = parts.length - 1; i >= 0; i--) {
592
- if (parts[i]?.type === "info" &&
593
- parts[i]?._subagent === infoKind &&
594
- parts[i]?._subagentId === subagentId) {
595
- parts.splice(i, 1);
596
- removed = true;
597
- }
598
- }
599
- return removed;
600
- };
601
- if (input.kind === "header") {
602
- const headerIndex = findHeaderIndex();
603
- const metaFields = {
604
- ...(metaTurnId ? { _subagentTurnId: metaTurnId } : {}),
605
- ...(metaStream ? { _subagentStream: metaStream } : {}),
606
- };
607
- if (headerIndex >= 0) {
608
- if (sameInfo(parts[headerIndex], "header"))
609
- return false;
610
- parts[headerIndex] = {
611
- ...parts[headerIndex],
612
- type: "info",
613
- text: t,
614
- _subagent: "header",
615
- _subagentId: subagentId,
616
- ...metaFields,
617
- };
618
- }
619
- else {
620
- parts.push({
621
- type: "info",
622
- text: t,
623
- _subagent: "header",
624
- _subagentId: subagentId,
625
- ...metaFields,
626
- });
627
- }
628
- return true;
629
- }
630
- if (input.kind === "activity") {
631
- // Replace this subagent's own activity part (scoped by _subagentId so
632
- // multiple concurrent/sequential tasks don't overwrite each other).
633
- for (let i = parts.length - 1; i >= 0; i--) {
634
- if (parts[i].type === "info" &&
635
- parts[i]._subagent === "activity" &&
636
- parts[i]._subagentId === subagentId) {
637
- if (sameInfo(parts[i], "activity"))
638
- return false;
639
- parts[i] = {
640
- type: "info",
641
- text: t,
642
- _subagent: "activity",
643
- _subagentId: subagentId,
644
- ...(metaTurnId ? { _subagentTurnId: metaTurnId } : {}),
645
- ...(metaStream ? { _subagentStream: metaStream } : {}),
646
- };
647
- return true;
648
- }
649
- }
650
- const idx = findInsertAfter();
651
- parts.splice(idx, 0, {
652
- type: "info",
653
- text: t,
654
- _subagent: "activity",
655
- _subagentId: subagentId,
656
- ...(metaTurnId ? { _subagentTurnId: metaTurnId } : {}),
657
- ...(metaStream ? { _subagentStream: metaStream } : {}),
658
- });
659
- return true;
660
- }
661
- // Once the subagent completes, collapse the live detail back down to
662
- // the header line and keep the terminal state on that header.
663
- const removedActivity = removeInfoParts("activity");
664
- const removedFinished = removeInfoParts("finished");
665
- const headerIndex = findHeaderIndex();
666
- if (headerIndex >= 0) {
667
- const header = parts[headerIndex];
668
- if (!removedActivity && !removedFinished && header?._subagentState === t)
669
- return false;
670
- parts[headerIndex] = {
671
- ...header,
672
- _subagentState: t,
673
- };
674
- return true;
675
- }
676
- parts.push({
677
- type: "info",
678
- text: subagentId,
679
- _subagent: "header",
680
- _subagentId: subagentId,
681
- _subagentState: t,
682
- });
683
- return true;
684
- }
685
- /**
686
- * Upsert subagent activity info parts.
687
- * - `header`: appends a new header line for the task
688
- * - `activity`: replaces the last subagent activity line (ticker during run)
689
- * - `finished`: appends/replaces a finished line WITHOUT removing the last activity
690
- * so the final display shows: ⎿ last_tool → ⎿ done
691
- */
692
- function upsertSubagentInfo(assistantId, subagentId, kind, text, meta) {
693
- upsertSubagentInfoBatch(assistantId, [{ subagentId, kind, text, meta }]);
694
- }
695
- function upsertSubagentInfoBatch(assistantId, items) {
696
- if (!items.length)
697
- return;
698
- updateAssistant(assistantId, (m) => {
699
- const parts = getRuntimeParts(m);
700
- let changed = false;
701
- for (const item of items) {
702
- if (applySubagentInfoToParts(parts, item))
703
- changed = true;
704
- }
705
- return changed ? { ...m, parts: m.parts } : m;
706
- });
707
- }
708
- function compactSettledSubagentTranscript(assistantId) {
709
- updateAssistant(assistantId, (m) => {
710
- const terminalSubagentIds = new Set();
711
- for (const part of getRuntimeParts(m)) {
712
- if (part?.type === "info" &&
713
- part?._subagent === "header" &&
714
- String(part?._subagentId ?? "").trim() &&
715
- String(part?._subagentState ?? "").trim()) {
716
- terminalSubagentIds.add(String(part._subagentId).trim());
717
- }
718
- }
719
- if (terminalSubagentIds.size === 0)
720
- return m;
721
- const nextParts = getRuntimeParts(m).filter((part) => {
722
- const scopedSubagentId = getScopedSubagentId(part);
723
- if (!scopedSubagentId || !terminalSubagentIds.has(scopedSubagentId))
724
- return true;
725
- return part?.type === "info" && part?._subagent === "header";
726
- });
727
- return nextParts.length === m.parts.length ? m : { ...m, parts: nextParts };
728
- });
729
- }
730
- function findSubagentInsertAfter(parts, subagentId) {
731
- for (let i = parts.length - 1; i >= 0; i--) {
732
- const part = parts[i];
733
- if (part?._subagentId === subagentId)
734
- return i + 1;
735
- if (part?.type === "tool_call" && part.call.subagentId === subagentId)
736
- return i + 1;
737
- if (part?.type === "tool_output" && part.subagentId === subagentId)
738
- return i + 1;
739
- if (part?.type === "tool_result" && part.result.subagentId === subagentId)
740
- return i + 1;
741
- }
742
- return parts.length;
743
- }
744
- function tickSubagentFrame(assistantId) {
745
- void assistantId;
746
- const frame = Number(state.ui?.subagentSpinnerFrame ?? 0);
747
- if (state.ui)
748
- state.ui.subagentSpinnerFrame = Number.isFinite(frame) ? Math.floor(frame) + 1 : 1;
749
- }
750
- function appendAssistantLlmMeta(assistantId, info) {
751
- const durationMs = Number(info?.durationMs);
752
- if (!Number.isFinite(durationMs) || durationMs < 0)
753
- return;
754
- const modelId = String(info?.modelId ?? "").trim() || undefined;
755
- const toolCalls = Number(info?.toolCalls);
756
- const tokensUsed = Number(info?.tokensUsed);
757
- const changedFileDetails = (() => {
758
- const raw = Array.isArray(info?.changedFileDetails) ? info.changedFileDetails : [];
759
- if (raw.length === 0)
760
- return [];
761
- const out = [];
762
- const seen = new Set();
763
- for (const item of raw) {
764
- const path = String(item?.path ?? "").trim();
765
- if (!path || seen.has(path))
766
- continue;
767
- seen.add(path);
768
- const added = Number(item?.added);
769
- const removed = Number(item?.removed);
770
- out.push({
771
- path,
772
- ...(Number.isFinite(added) && added >= 0 ? { added: Math.floor(added) } : {}),
773
- ...(Number.isFinite(removed) && removed >= 0 ? { removed: Math.floor(removed) } : {}),
774
- });
775
- }
776
- return out;
777
- })();
778
- const changedFiles = (() => {
779
- const raw = Array.isArray(info?.changedFiles) ? info.changedFiles : [];
780
- const source = raw.length > 0 ? raw : changedFileDetails.map((item) => item.path);
781
- if (source.length === 0)
782
- return [];
783
- const out = [];
784
- const seen = new Set();
785
- for (const item of source) {
786
- const file = String(item ?? "").trim();
787
- if (!file)
788
- continue;
789
- if (seen.has(file))
790
- continue;
791
- seen.add(file);
792
- out.push(file);
793
- }
794
- return out;
795
- })();
796
- const hasToolCalls = Number.isFinite(toolCalls) && toolCalls >= 0;
797
- const hasTokensUsed = Number.isFinite(tokensUsed) && tokensUsed >= 0;
798
- const hasChangedFiles = changedFiles.length > 0;
799
- const hasChangedFileDetails = changedFileDetails.length > 0;
800
- updateAssistant(assistantId, (m) => {
801
- if (!info?.isFinalRound) {
802
- // Suppress meta on tool-only rounds to avoid confusing mid-run summaries.
803
- let lastToolCallIndex = -1;
804
- for (let i = 0; i < m.parts.length; i++) {
805
- if (m.parts[i]?.type === "tool_call")
806
- lastToolCallIndex = i;
807
- }
808
- if (lastToolCallIndex >= 0) {
809
- let hasUserFacingAfterToolCall = false;
810
- for (let i = lastToolCallIndex + 1; i < m.parts.length; i++) {
811
- const part = m.parts[i];
812
- if (!part)
813
- continue;
814
- if (part.type === "markdown" && String(part.markdown ?? "").trim()) {
815
- hasUserFacingAfterToolCall = true;
816
- break;
817
- }
818
- if (part.type === "todo" || part.type === "plan" || part.type === "approve") {
819
- hasUserFacingAfterToolCall = true;
820
- break;
821
- }
822
- }
823
- if (!hasUserFacingAfterToolCall)
824
- return m;
825
- }
826
- }
827
- const parts = m.parts.slice();
828
- parts.push({
829
- type: "llm_meta",
830
- durationMs,
831
- ...(modelId ? { modelId } : {}),
832
- ...(hasToolCalls ? { toolCalls: Math.floor(toolCalls) } : {}),
833
- ...(hasTokensUsed ? { tokensUsed: Math.floor(tokensUsed) } : {}),
834
- ...(hasChangedFiles ? { changedFiles } : {}),
835
- ...(hasChangedFileDetails ? { changedFileDetails } : {}),
836
- });
837
- return { ...m, parts };
838
- });
839
- }
840
- function updateAssistantLlmMeta(assistantId, info) {
841
- const durationMs = Number(info?.durationMs);
842
- const modelId = String(info?.modelId ?? "").trim() || undefined;
843
- const toolCalls = Number(info?.toolCalls);
844
- const tokensUsed = Number(info?.tokensUsed);
845
- const changedFileDetails = (() => {
846
- const raw = Array.isArray(info?.changedFileDetails) ? info.changedFileDetails : null;
847
- if (!raw)
848
- return null;
849
- const out = [];
850
- const seen = new Set();
851
- for (const item of raw) {
852
- const path = String(item?.path ?? "").trim();
853
- if (!path || seen.has(path))
854
- continue;
855
- seen.add(path);
856
- const added = Number(item?.added);
857
- const removed = Number(item?.removed);
858
- out.push({
859
- path,
860
- ...(Number.isFinite(added) && added >= 0 ? { added: Math.floor(added) } : {}),
861
- ...(Number.isFinite(removed) && removed >= 0 ? { removed: Math.floor(removed) } : {}),
862
- });
863
- }
864
- return out;
865
- })();
866
- const changedFiles = (() => {
867
- const raw = Array.isArray(info?.changedFiles) ? info.changedFiles : null;
868
- if (!raw) {
869
- if (!Array.isArray(changedFileDetails))
870
- return null;
871
- return changedFileDetails.map((item) => item.path);
872
- }
873
- if (!raw)
874
- return null;
875
- const out = [];
876
- const seen = new Set();
877
- for (const item of raw) {
878
- const file = String(item ?? "").trim();
879
- if (!file)
880
- continue;
881
- if (seen.has(file))
882
- continue;
883
- seen.add(file);
884
- out.push(file);
885
- }
886
- return out;
887
- })();
888
- updateAssistant(assistantId, (m) => {
889
- const parts = m.parts.slice();
890
- for (let i = parts.length - 1; i >= 0; i--) {
891
- const p = parts[i];
892
- if (p.type !== "llm_meta")
893
- continue;
894
- const next = { ...p };
895
- let changed = false;
896
- if (Number.isFinite(durationMs) && durationMs >= 0) {
897
- next.durationMs = durationMs;
898
- changed = true;
899
- }
900
- if (modelId) {
901
- next.modelId = modelId;
902
- changed = true;
903
- }
904
- if (Number.isFinite(toolCalls) && toolCalls >= 0) {
905
- next.toolCalls = Math.floor(toolCalls);
906
- changed = true;
907
- }
908
- if (Number.isFinite(tokensUsed) && tokensUsed >= 0) {
909
- next.tokensUsed = Math.floor(tokensUsed);
910
- changed = true;
911
- }
912
- if (Array.isArray(changedFiles)) {
913
- if (changedFiles.length > 0)
914
- next.changedFiles = changedFiles;
915
- else
916
- delete next.changedFiles;
917
- changed = true;
918
- }
919
- if (Array.isArray(changedFileDetails)) {
920
- if (changedFileDetails.length > 0)
921
- next.changedFileDetails = changedFileDetails;
922
- else
923
- delete next.changedFileDetails;
924
- changed = true;
925
- }
926
- if (!changed)
927
- return m;
928
- parts[i] = next;
929
- return { ...m, parts };
930
- }
931
- return m;
932
- });
933
- }
934
- function upsertToolCallDelta(assistantId, callId, toolName, argsTextDelta, linesWritten, meta) {
935
- const idRaw = String(callId ?? "").trim();
936
- if (!idRaw)
937
- return;
938
- const name = String(toolName ?? "").trim() || "tool";
939
- const shouldTrackStartedAt = /^shell$/i.test(name);
940
- const delta = String(argsTextDelta ?? "");
941
- const subagentId = String(meta?.subagentId ?? "").trim() || undefined;
942
- const subagentTurnId = String(meta?.subagentTurnId ?? "").trim() || undefined;
943
- const t0 = perfStreamEnabled ? Date.now() : 0;
944
- updateAssistant(assistantId, (m) => {
945
- // Streaming optimization: mutate in-place to avoid allocating a new parts array per tool chunk.
946
- const parts = getRuntimeParts(m);
947
- let idx = -1;
948
- for (let i = parts.length - 1; i >= 0; i--) {
949
- const p = parts[i];
950
- if (p?.type === "tool_call" && p?.call?.id === idRaw) {
951
- idx = i;
952
- break;
953
- }
954
- }
955
- if (idx >= 0) {
956
- const p = parts[idx];
957
- const prevTail = String(p.call.argumentsText ?? "");
958
- const prevFullLen = Number(p.call.streamMeta?.argsTextFullLen ?? prevTail.length);
959
- const nextFullLen = prevFullLen + delta.length;
960
- const nextTailRaw = `${prevTail}${delta}`;
961
- const nextTail = nextTailRaw.length > streamToolArgsTailLimit
962
- ? nextTailRaw.slice(-streamToolArgsTailLimit)
963
- : nextTailRaw;
964
- const prevStartedAtMs = Number(p.call.startedAtMs ?? 0);
965
- const metaStartedAtMs = Number(meta?.startedAtMs ?? 0);
966
- const startedAtMs = shouldTrackStartedAt
967
- ? Number.isFinite(prevStartedAtMs) && prevStartedAtMs > 0
968
- ? prevStartedAtMs
969
- : Number.isFinite(metaStartedAtMs) && metaStartedAtMs > 0
970
- ? metaStartedAtMs
971
- : Date.now()
972
- : undefined;
973
- const nextCall = {
974
- ...p.call,
975
- name,
976
- ...(subagentId ? { subagentId } : {}),
977
- ...(subagentTurnId ? { subagentTurnId } : {}),
978
- argumentsText: nextTail,
979
- streamMeta: {
980
- ...p.call.streamMeta,
981
- argsTextFullLen: nextFullLen,
982
- argsTextTailLimit: streamToolArgsTailLimit,
983
- },
984
- ...(typeof startedAtMs === "number" ? { startedAtMs } : null),
985
- ...(typeof linesWritten === "number" ? { linesWritten } : null),
986
- };
987
- parts[idx] = { type: "tool_call", call: nextCall };
988
- return { ...m, parts: m.parts };
989
- }
990
- const firstTail = delta.length > streamToolArgsTailLimit ? delta.slice(-streamToolArgsTailLimit) : delta;
991
- const call = {
992
- id: idRaw,
993
- name,
994
- ...(subagentId ? { subagentId } : {}),
995
- ...(subagentTurnId ? { subagentTurnId } : {}),
996
- arguments: {},
997
- argumentsText: firstTail,
998
- streamMeta: {
999
- argsTextFullLen: delta.length,
1000
- argsTextTailLimit: streamToolArgsTailLimit,
1001
- },
1002
- ...(shouldTrackStartedAt
1003
- ? {
1004
- startedAtMs: typeof meta?.startedAtMs === "number" && Number.isFinite(meta.startedAtMs)
1005
- ? meta.startedAtMs
1006
- : Date.now(),
1007
- }
1008
- : null),
1009
- ...(typeof linesWritten === "number" ? { linesWritten } : null),
1010
- };
1011
- const next = {
1012
- type: "tool_call",
1013
- call,
1014
- ...(subagentId ? { _subagentId: subagentId } : {}),
1015
- };
1016
- if (subagentId) {
1017
- const insertAt = findSubagentInsertAfter(parts, subagentId);
1018
- parts.splice(insertAt, 0, next);
1019
- }
1020
- else {
1021
- parts.push(next);
1022
- }
1023
- return { ...m, parts: m.parts };
1024
- });
1025
- if (perfStreamEnabled) {
1026
- const ms = Date.now() - t0;
1027
- const threshold = isOneShotEnabled() ? 5 : 40;
1028
- if (ms >= threshold) {
1029
- const now = Date.now();
1030
- if (isOneShotEnabled() || now - lastStreamSlowLogAt >= 800) {
1031
- lastStreamSlowLogAt = now;
1032
- perfLogStream(`[STREAM_TC] ${ms}ms callId=${idRaw} tool=${name} deltaChars=${delta.length}`);
1033
- }
1034
- }
1035
- }
1036
- }
1037
- function upsertToolCallEnd(assistantId, callId, toolName, args, thoughtSignature, meta) {
1038
- const idRaw = String(callId ?? "").trim();
1039
- if (!idRaw)
1040
- return;
1041
- const subagentId = String(meta?.subagentId ?? "").trim() || undefined;
1042
- const subagentTurnId = String(meta?.subagentTurnId ?? "").trim() || undefined;
1043
- const shouldTrackStartedAt = /^shell$/i.test(toolName);
1044
- updateAssistant(assistantId, (m) => {
1045
- const parts = m.parts.slice();
1046
- const idx = findToolCallPartIndex(parts, idRaw);
1047
- const prevCall = idx >= 0 && parts[idx]?.type === "tool_call" ? parts[idx].call : undefined;
1048
- const prevArgsText = prevCall?.argumentsText;
1049
- const nextThoughtSignature = thoughtSignature || prevCall?.thoughtSignature;
1050
- const prevStartedAtMs = typeof prevCall?.startedAtMs === "number" && Number.isFinite(prevCall.startedAtMs)
1051
- ? prevCall.startedAtMs
1052
- : undefined;
1053
- const metaStartedAtMs = typeof meta?.startedAtMs === "number" && Number.isFinite(meta.startedAtMs)
1054
- ? meta.startedAtMs
1055
- : undefined;
1056
- const prevLinesWritten = typeof prevCall?.linesWritten === "number" ? prevCall.linesWritten : undefined;
1057
- const call = {
1058
- id: idRaw,
1059
- name: toolName,
1060
- ...(subagentId ? { subagentId } : {}),
1061
- ...(subagentTurnId ? { subagentTurnId } : {}),
1062
- arguments: args,
1063
- argumentsText: prevArgsText,
1064
- ...(shouldTrackStartedAt
1065
- ? typeof prevStartedAtMs === "number"
1066
- ? { startedAtMs: prevStartedAtMs }
1067
- : typeof metaStartedAtMs === "number"
1068
- ? { startedAtMs: metaStartedAtMs }
1069
- : null
1070
- : null),
1071
- ...(nextThoughtSignature ? { thoughtSignature: nextThoughtSignature } : {}),
1072
- ...(typeof prevLinesWritten === "number" ? { linesWritten: prevLinesWritten } : null),
1073
- };
1074
- const nextPart = {
1075
- type: "tool_call",
1076
- call,
1077
- ...(subagentId ? { _subagentId: subagentId } : {}),
1078
- };
1079
- if (idx >= 0)
1080
- parts[idx] = nextPart;
1081
- else if (subagentId) {
1082
- const insertAt = findSubagentInsertAfter(parts, subagentId);
1083
- parts.splice(insertAt, 0, nextPart);
1084
- }
1085
- else
1086
- parts.push(nextPart);
1087
- return { ...m, parts };
1088
- });
1089
- }
1090
- function upsertToolOutputStart(assistantId, toolCallId, toolName, meta) {
1091
- const idRaw = String(toolCallId ?? "").trim();
1092
- if (!idRaw)
1093
- return;
1094
- const name = String(toolName ?? "").trim() || "tool";
1095
- const subagentId = String(meta?.subagentId ?? "").trim() || undefined;
1096
- const isExplore = name.toLowerCase() === "task";
1097
- const tailLimit = isExplore
1098
- ? Math.max(streamToolOutputTailLimit, 200_000)
1099
- : streamToolOutputTailLimit;
1100
- const tailLinesLimit = isExplore
1101
- ? Math.max(streamToolOutputTailLinesLimit, 10_000)
1102
- : streamToolOutputTailLinesLimit;
1103
- updateAssistant(assistantId, (m) => {
1104
- const parts = getRuntimeParts(m);
1105
- const idx = findToolOutputPartIndex(parts, idRaw);
1106
- if (idx >= 0) {
1107
- const prev = parts[idx];
1108
- if (prev?.type !== "tool_output")
1109
- return { ...m, parts: m.parts };
1110
- parts[idx] = {
1111
- ...prev,
1112
- type: "tool_output",
1113
- toolCallId: idRaw,
1114
- toolName: prev.toolName || name,
1115
- ...(subagentId ? { subagentId } : {}),
1116
- streamMeta: {
1117
- ...prev.streamMeta,
1118
- tailLimit,
1119
- tailLinesLimit,
1120
- },
1121
- };
1122
- return { ...m, parts: m.parts };
1123
- }
1124
- const next = {
1125
- type: "tool_output",
1126
- toolCallId: idRaw,
1127
- toolName: name,
1128
- output: "",
1129
- ...(subagentId ? { subagentId, _subagentId: subagentId } : {}),
1130
- streamMeta: {
1131
- fullLen: 0,
1132
- lineBreaks: 0,
1133
- tailLines: [""],
1134
- tailLimit,
1135
- tailLinesLimit,
1136
- },
1137
- };
1138
- const callIdx = findToolCallPartIndex(parts, idRaw);
1139
- if (callIdx >= 0) {
1140
- let insertAt = callIdx + 1;
1141
- while (insertAt < parts.length && isApprovalForToolCall(parts[insertAt], idRaw)) {
1142
- insertAt++;
1143
- }
1144
- parts.splice(insertAt, 0, next);
1145
- }
1146
- else if (subagentId) {
1147
- const insertAt = findSubagentInsertAfter(parts, subagentId);
1148
- parts.splice(insertAt, 0, next);
1149
- }
1150
- else {
1151
- parts.push(next);
1152
- }
1153
- return { ...m, parts: m.parts };
1154
- });
1155
- }
1156
- function appendToolOutputDelta(assistantId, toolCallId, toolName, delta, meta) {
1157
- const idRaw = String(toolCallId ?? "").trim();
1158
- if (!idRaw)
1159
- return;
1160
- const d = String(delta ?? "").replace(/\r/g, "");
1161
- if (!d)
1162
- return;
1163
- const name = String(toolName ?? "").trim() || "tool";
1164
- const subagentId = String(meta?.subagentId ?? "").trim() || undefined;
1165
- const isExplore = name.toLowerCase() === "task";
1166
- const t0 = perfStreamEnabled ? Date.now() : 0;
1167
- updateAssistant(assistantId, (m) => {
1168
- // Streaming optimization: mutate in-place to avoid allocating a new parts array per chunk.
1169
- const parts = getRuntimeParts(m);
1170
- const idx = findToolOutputPartIndex(parts, idRaw);
1171
- if (idx >= 0) {
1172
- const prev = parts[idx];
1173
- if (prev?.type !== "tool_output")
1174
- return { ...m, parts: m.parts };
1175
- const prevTail = String(prev.output ?? "");
1176
- const tailLimit = (() => {
1177
- const v = Number(prev.streamMeta?.tailLimit);
1178
- if (Number.isFinite(v) && v > 0)
1179
- return Math.floor(v);
1180
- return isExplore
1181
- ? Math.max(streamToolOutputTailLimit, 200_000)
1182
- : streamToolOutputTailLimit;
1183
- })();
1184
- const tailLinesLimit = (() => {
1185
- const v = Number(prev.streamMeta?.tailLinesLimit);
1186
- if (Number.isFinite(v) && v > 0)
1187
- return Math.floor(v);
1188
- return isExplore
1189
- ? Math.max(streamToolOutputTailLinesLimit, 10_000)
1190
- : streamToolOutputTailLinesLimit;
1191
- })();
1192
- const nextTailRaw = `${prevTail}${d}`;
1193
- const nextTail = nextTailRaw.length > tailLimit ? nextTailRaw.slice(-tailLimit) : nextTailRaw;
1194
- const prevFullLen = Number(prev.streamMeta?.fullLen ?? prevTail.length);
1195
- const prevLineBreaks = Number(prev.streamMeta?.lineBreaks ?? countNewlines(prevTail));
1196
- const nextFullLen = prevFullLen + d.length;
1197
- const nextLineBreaks = prevLineBreaks + countNewlines(d);
1198
- const prevTailLines = Array.isArray(prev.streamMeta?.tailLines)
1199
- ? prev.streamMeta.tailLines
1200
- : computeTailLinesFromTailText(prevTail, tailLinesLimit);
1201
- const nextTailLines = appendTailLines(prevTailLines, d, tailLinesLimit, nextTail.length);
1202
- parts[idx] = {
1203
- ...prev,
1204
- type: "tool_output",
1205
- toolCallId: idRaw,
1206
- toolName: prev.toolName || name,
1207
- ...(subagentId ? { subagentId } : {}),
1208
- output: nextTail,
1209
- streamMeta: {
1210
- fullLen: nextFullLen,
1211
- lineBreaks: nextLineBreaks,
1212
- tailLines: nextTailLines,
1213
- tailLimit,
1214
- tailLinesLimit,
1215
- },
1216
- };
1217
- return { ...m, parts: m.parts };
1218
- }
1219
- const tailLimit = isExplore
1220
- ? Math.max(streamToolOutputTailLimit, 200_000)
1221
- : streamToolOutputTailLimit;
1222
- const tailLinesLimit = isExplore
1223
- ? Math.max(streamToolOutputTailLinesLimit, 10_000)
1224
- : streamToolOutputTailLinesLimit;
1225
- const firstTail = d.length > tailLimit ? d.slice(-tailLimit) : d;
1226
- const firstTailLines = computeTailLinesFromTailText(firstTail, tailLinesLimit);
1227
- const next = {
1228
- type: "tool_output",
1229
- toolCallId: idRaw,
1230
- toolName: name,
1231
- output: firstTail,
1232
- ...(subagentId ? { subagentId, _subagentId: subagentId } : {}),
1233
- streamMeta: {
1234
- fullLen: d.length,
1235
- lineBreaks: countNewlines(d),
1236
- tailLines: firstTailLines,
1237
- tailLimit,
1238
- tailLinesLimit,
1239
- },
1240
- };
1241
- const callIdx = findToolCallPartIndex(parts, idRaw);
1242
- if (callIdx >= 0) {
1243
- let insertAt = callIdx + 1;
1244
- while (insertAt < parts.length && isApprovalForToolCall(parts[insertAt], idRaw)) {
1245
- insertAt++;
1246
- }
1247
- parts.splice(insertAt, 0, next);
1248
- }
1249
- else if (subagentId) {
1250
- const insertAt = findSubagentInsertAfter(parts, subagentId);
1251
- parts.splice(insertAt, 0, next);
1252
- }
1253
- else {
1254
- parts.push(next);
1255
- }
1256
- return { ...m, parts: m.parts };
1257
- });
1258
- if (perfStreamEnabled) {
1259
- const ms = Date.now() - t0;
1260
- const threshold = isOneShotEnabled() ? 5 : 40;
1261
- if (ms >= threshold) {
1262
- const now = Date.now();
1263
- if (isOneShotEnabled() || now - lastStreamSlowLogAt >= 800) {
1264
- lastStreamSlowLogAt = now;
1265
- perfLogStream(`[STREAM_TO] ${ms}ms callId=${idRaw} tool=${name} deltaChars=${d.length}`);
1266
- }
1267
- }
1268
- }
1269
- }
1270
- function attachToolResultPart(assistantId, result, meta) {
1271
- const callId = String(result.toolCallId ?? "").trim();
1272
- if (!callId)
1273
- return;
1274
- const subagentId = String(meta?.subagentId ?? result.subagentId ?? "").trim() || undefined;
1275
- const subagentTurnId = String(meta?.subagentTurnId ??
1276
- result.subagentTurnId ??
1277
- "").trim() || undefined;
1278
- const nextResult = subagentId || subagentTurnId
1279
- ? {
1280
- ...result,
1281
- ...(subagentId ? { subagentId } : {}),
1282
- ...(subagentTurnId ? { subagentTurnId } : {}),
1283
- }
1284
- : result;
1285
- updateAssistant(assistantId, (m) => {
1286
- const parts = m.parts.slice();
1287
- const callIdx = findToolCallPartIndex(parts, callId);
1288
- const withoutPrev = parts.filter((p) => !(p.type === "tool_result" && p.result.toolCallId === callId));
1289
- const rebuilt = withoutPrev.slice();
1290
- if (callIdx >= 0) {
1291
- const rebuiltCallIdx = findToolCallPartIndex(rebuilt, callId);
1292
- if (rebuiltCallIdx >= 0) {
1293
- let insertAt = rebuiltCallIdx + 1;
1294
- while (insertAt < rebuilt.length && isApprovalForToolCall(rebuilt[insertAt], callId)) {
1295
- insertAt++;
1296
- }
1297
- while (insertAt < rebuilt.length && isToolOutputForCall(rebuilt[insertAt], callId))
1298
- insertAt++;
1299
- rebuilt.splice(insertAt, 0, {
1300
- type: "tool_result",
1301
- result: nextResult,
1302
- ...(subagentId ? { _subagentId: subagentId } : {}),
1303
- });
1304
- }
1305
- else {
1306
- rebuilt.push({
1307
- type: "tool_result",
1308
- result: nextResult,
1309
- ...(subagentId ? { _subagentId: subagentId } : {}),
1310
- });
1311
- }
1312
- }
1313
- else if (subagentId) {
1314
- const insertAt = findSubagentInsertAfter(rebuilt, subagentId);
1315
- rebuilt.splice(insertAt, 0, {
1316
- type: "tool_result",
1317
- result: nextResult,
1318
- _subagentId: subagentId,
1319
- });
1320
- }
1321
- else {
1322
- rebuilt.push({ type: "tool_result", result });
1323
- }
1324
- return { ...m, parts: rebuilt };
1325
- });
1326
- }
1327
- function insertApprovalPart(assistantId, request) {
1328
- updateAssistant(assistantId, (m) => {
1329
- const has = m.parts.some((p) => p.type === "approve" &&
1330
- p.request.toolCallId === request.toolCallId &&
1331
- p.request.status === "pending");
1332
- if (has)
1333
- return m;
1334
- const parts = m.parts.slice();
1335
- const callIdx = parts.findIndex((p) => p.type === "tool_call" && p.call.id === request.toolCallId);
1336
- if (callIdx >= 0)
1337
- parts.splice(callIdx + 1, 0, { type: "approve", request });
1338
- else if (request.subagentId) {
1339
- const insertAt = findSubagentInsertAfter(parts, request.subagentId);
1340
- parts.splice(insertAt, 0, { type: "approve", request });
1341
- }
1342
- else
1343
- parts.push({ type: "approve", request });
1344
- return { ...m, parts };
1345
- });
1346
- }
1347
- function toggleChecklist(messageId, partType, itemId) {
1348
- const msg = messageById(messageId);
1349
- if (!msg || msg.role !== "assistant")
1350
- return;
1351
- // Tool-derived TodoWrite checklists are read-only in the transcript UI.
1352
- // (They reflect the tool result and should not be mutated by user clicks.)
1353
- if (partType === "todo") {
1354
- const block = msg.parts.find((p) => p.type === "todo" && p.items.some((it) => it.id === itemId));
1355
- const callId = String(block?.toolCallId ?? "").trim();
1356
- if (callId) {
1357
- const toolCall = msg.parts.find((p) => p.type === "tool_call" && p.call.id === callId);
1358
- const name = String(toolCall?.call?.name ?? "")
1359
- .trim()
1360
- .toLowerCase();
1361
- if (!name || name === "todowrite")
1362
- return;
1363
- }
1364
- }
1365
- const parts = msg.parts.map((p) => {
1366
- if (p.type !== partType)
1367
- return p;
1368
- return {
1369
- ...p,
1370
- items: p.items.map((it) => {
1371
- if (it.id !== itemId)
1372
- return it;
1373
- const done = !it.done;
1374
- return { ...it, done, status: done ? "completed" : "pending" };
1375
- }),
1376
- };
1377
- });
1378
- replaceMessage(messageId, { ...msg, parts });
1379
- }
1380
- function toggleBlockCollapsed(messageId, partType, partId) {
1381
- const normalizedPartId = String(partId ?? "").trim();
1382
- if (!normalizedPartId)
1383
- return;
1384
- const msg = messageById(messageId);
1385
- if (!msg || msg.role !== "assistant")
1386
- return;
1387
- const parts = msg.parts.map((p) => {
1388
- if (p.type !== partType)
1389
- return p;
1390
- if (String(p.partId ?? "").trim() !== normalizedPartId)
1391
- return p;
1392
- return { ...p, collapsed: !p.collapsed };
1393
- });
1394
- replaceMessage(messageId, { ...msg, parts });
1395
- }
1396
- function revertFromMessage(messageId) {
1397
- const idx = messageIndex(messageId);
1398
- if (idx < 0)
1399
- return 0;
1400
- const removed = state.messages.splice(idx);
1401
- messageIndexById = null;
1402
- state.revertedMessages = removed;
1403
- setSessionHistoryDirty(true);
1404
- rebuildUserHistory();
1405
- hooks.onMessagesRemoved?.(removed);
1406
- return removed.length;
1407
- }
1408
- function restoreRevertedMessages() {
1409
- if (state.revertedMessages.length === 0)
1410
- return 0;
1411
- const count = state.revertedMessages.length;
1412
- const restored = state.revertedMessages.slice();
1413
- state.messages.push(...restored);
1414
- messageIndexById = null;
1415
- state.revertedMessages = [];
1416
- // A full restore brings the transcript back to its pre-revert shape.
1417
- setSessionHistoryDirty(false);
1418
- rebuildUserHistory();
1419
- hooks.onMessagesRestored?.(restored);
1420
- return count;
1421
- }
1422
- function clearRevertedMessages() {
1423
- state.revertedMessages = [];
1424
- }
1425
- const hasRevertedMessages = computed(() => state.revertedMessages.length > 0);
1426
- const revertedMessageCount = computed(() => state.revertedMessages.length);
1427
- function upsertChecklistPartFromDimSdk(assistantId, kind, toolCallId, structured) {
1428
- const todos = isRecord(structured) ? structured.todos : undefined;
1429
- if (!Array.isArray(todos))
1430
- return;
1431
- const items = todos
1432
- .filter(isRecord)
1433
- .map((todo, i) => {
1434
- const rawContent = todo.content;
1435
- const rawStatus = String(todo.status ?? "").trim() || "pending";
1436
- const text = String(rawContent ?? "").trim();
1437
- const status = rawStatus === "completed" || rawStatus === "in_progress" || rawStatus === "pending"
1438
- ? rawStatus
1439
- : rawStatus;
1440
- return {
1441
- id: `${toolCallId}:${i}`,
1442
- text,
1443
- done: status === "completed",
1444
- status,
1445
- };
1446
- })
1447
- .filter((item) => Boolean(item.text));
1448
- if (items.length === 0)
1449
- return;
1450
- const title = kind === "plan" ? t("chatParts.plan") : t("chatParts.todo");
1451
- const partId = createChecklistPartId(kind, toolCallId);
1452
- updateAssistant(assistantId, (m) => {
1453
- const parts = getRuntimeParts(m).slice();
1454
- const idx = parts.findIndex((p) => isChecklistPartOfKind(p, kind) && String(p.partId ?? "").trim() === partId);
1455
- if (idx >= 0) {
1456
- const prev = parts[idx];
1457
- const collapsed = isChecklistPartOfKind(prev, kind) ? Boolean(prev.collapsed) : false;
1458
- parts[idx] = createChecklistPart(kind, {
1459
- collapsed,
1460
- items,
1461
- partId,
1462
- title,
1463
- toolCallId,
1464
- });
1465
- return { ...m, parts };
1466
- }
1467
- const nextPart = createChecklistPart(kind, {
1468
- collapsed: false,
1469
- items,
1470
- partId,
1471
- title,
1472
- toolCallId,
1473
- });
1474
- const callIdx = parts.findIndex((p) => p?.type === "tool_call" && p.call.id === toolCallId);
1475
- if (callIdx >= 0) {
1476
- let insertAt = callIdx + 1;
1477
- while (insertAt < parts.length && isApprovalForToolCall(parts[insertAt], toolCallId)) {
1478
- insertAt++;
1479
- }
1480
- while (insertAt < parts.length && isToolOutputForCall(parts[insertAt], toolCallId))
1481
- insertAt++;
1482
- while (insertAt < parts.length && isToolResultForCall(parts[insertAt], toolCallId))
1483
- insertAt++;
1484
- parts.splice(insertAt, 0, nextPart);
1485
- return { ...m, parts };
1486
- }
1487
- parts.push(nextPart);
1488
- return { ...m, parts };
1489
- });
1490
- }
1491
- // Ensure history is correct if messages were preloaded (e.g. resumed session).
1492
- rebuildUserHistory();
1493
- return {
1494
- pushMessage,
1495
- setSessionHistoryDirty,
1496
- replaceMessage,
1497
- updateAssistant,
1498
- updateAssistantHidden,
1499
- setAssistantStatus,
1500
- setAssistantThinking,
1501
- startNewThinkingBlock,
1502
- endThinkingBlock,
1503
- appendAssistantMarkdown,
1504
- appendInfoPart,
1505
- upsertSubagentInfo,
1506
- upsertSubagentInfoBatch,
1507
- compactSettledSubagentTranscript,
1508
- tickSubagentFrame,
1509
- appendAssistantLlmMeta,
1510
- updateAssistantLlmMeta,
1511
- upsertToolCallDelta,
1512
- upsertToolCallEnd,
1513
- upsertToolOutputStart,
1514
- appendToolOutputDelta,
1515
- attachToolResultPart,
1516
- insertApprovalPart,
1517
- toggleChecklist,
1518
- toggleBlockCollapsed,
1519
- revertFromMessage,
1520
- restoreRevertedMessages,
1521
- clearRevertedMessages,
1522
- rebuildUserHistory,
1523
- hasRevertedMessages,
1524
- revertedMessageCount,
1525
- upsertChecklistPartFromDimSdk,
1526
- };
1527
- }
1528
- //# sourceMappingURL=message-mutations.js.map