@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.
- package/LICENSE +21 -0
- package/README.md +44 -181
- package/dist/launcher.js +32 -0
- package/package.json +15 -27
- package/dist/best-agent-native-bridge.d.ts +0 -389
- package/dist/best-agent-native-bridge.js +0 -4645
- package/dist/cli-storage.d.ts +0 -5
- package/dist/cli-storage.js +0 -7
- package/dist/dimsdk/ask-user.d.ts +0 -21
- package/dist/dimsdk/ask-user.js +0 -131
- package/dist/dimsdk/cli/version.d.ts +0 -6
- package/dist/dimsdk/cli/version.js +0 -36
- package/dist/dimsdk/compat/archships/dim-agent/runtime-host.d.ts +0 -49
- package/dist/dimsdk/compat/archships/dim-agent/runtime-host.js +0 -215
- package/dist/dimsdk/compat/runtime-host.d.ts +0 -2
- package/dist/dimsdk/compat/runtime-host.js +0 -2
- package/dist/dimsdk/core/debug-log.d.ts +0 -8
- package/dist/dimsdk/core/debug-log.js +0 -100
- package/dist/dimsdk/core/env.d.ts +0 -26
- package/dist/dimsdk/core/env.js +0 -130
- package/dist/dimsdk/core/markdown/sanitize.d.ts +0 -2
- package/dist/dimsdk/core/markdown/sanitize.js +0 -43
- package/dist/dimsdk/core/markdown/wrap.d.ts +0 -9
- package/dist/dimsdk/core/markdown/wrap.js +0 -139
- package/dist/dimsdk/core/markdown.d.ts +0 -18
- package/dist/dimsdk/core/markdown.js +0 -62
- package/dist/dimsdk/core/model.d.ts +0 -273
- package/dist/dimsdk/core/model.js +0 -16
- package/dist/dimsdk/core/observability/perf-log.d.ts +0 -18
- package/dist/dimsdk/core/observability/perf-log.js +0 -236
- package/dist/dimsdk/core/store/agent-event-mappers.d.ts +0 -26
- package/dist/dimsdk/core/store/agent-event-mappers.js +0 -109
- package/dist/dimsdk/core/store/message-mutations.d.ts +0 -105
- package/dist/dimsdk/core/store/message-mutations.js +0 -1528
- package/dist/dimsdk/core/store/semantic-readers.d.ts +0 -2
- package/dist/dimsdk/core/store/semantic-readers.js +0 -4
- package/dist/dimsdk/core/store/transcript-projector.d.ts +0 -164
- package/dist/dimsdk/core/store/transcript-projector.js +0 -580
- package/dist/dimsdk/core/tool-metadata.d.ts +0 -12
- package/dist/dimsdk/core/tool-metadata.js +0 -70
- package/dist/dimsdk/core/transcript-part-id.d.ts +0 -6
- package/dist/dimsdk/core/transcript-part-id.js +0 -22
- package/dist/dimsdk/i18n/index.d.ts +0 -18
- package/dist/dimsdk/i18n/index.js +0 -141
- package/dist/dimsdk/i18n/locales/en.d.ts +0 -988
- package/dist/dimsdk/i18n/locales/en.js +0 -987
- package/dist/dimsdk/i18n/locales/index.d.ts +0 -1976
- package/dist/dimsdk/i18n/locales/index.js +0 -8
- package/dist/dimsdk/i18n/locales/zh-CN.d.ts +0 -988
- package/dist/dimsdk/i18n/locales/zh-CN.js +0 -987
- package/dist/dimsdk/i18n/types.d.ts +0 -13
- package/dist/dimsdk/i18n/types.js +0 -2
- package/dist/dimsdk/shared/value-normalizers.d.ts +0 -7
- package/dist/dimsdk/shared/value-normalizers.js +0 -48
- package/dist/dimsdk-tui.bundle-size.json +0 -169
- package/dist/dimsdk-tui.bundle.js +0 -330756
- package/dist/goatchain-adapter.d.ts +0 -41
- package/dist/goatchain-adapter.js +0 -499
- package/dist/index.d.ts +0 -3
- package/dist/index.js +0 -1405
- package/dist/native-runtime-bridge.d.ts +0 -387
- package/dist/native-runtime-bridge.js +0 -4624
- package/dist/provider-config-env.d.ts +0 -7
- package/dist/provider-config-env.js +0 -286
- package/dist/provider-model-limit-index.generated.d.ts +0 -3
- package/dist/provider-model-limit-index.generated.js +0 -7818
- package/dist/provider-model-limits.d.ts +0 -8
- package/dist/provider-model-limits.js +0 -43
- package/dist/runtime.d.ts +0 -47
- 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
|