@tangle-network/sandbox-ui 2.0.6 → 2.0.7
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/README.md +4 -0
- package/dist/{chat-BrlLa4rg.js → chat-BU39pb2j.js} +2 -2
- package/dist/chat.d.ts +1 -1
- package/dist/chat.js +1 -1
- package/dist/globals.css +169 -0
- package/dist/index-D70BfRTV.d.ts +308 -0
- package/dist/{index-5zOVKgDT.d.ts → index-Due7tY0w.d.ts} +2 -2
- package/dist/index.d.ts +3 -3
- package/dist/index.js +3 -3
- package/dist/rolldown-runtime-D8jQpDG7.js +12 -0
- package/dist/run-BgllNBrW.js +2036 -0
- package/dist/run.d.ts +2 -2
- package/dist/run.js +2 -2
- package/dist/styles.css +169 -0
- package/dist/tailwind.css +30 -0
- package/package.json +3 -2
- package/dist/index-DFNf9mJ2.d.ts +0 -121
- package/dist/run-D996hHtq.js +0 -612
|
@@ -0,0 +1,2036 @@
|
|
|
1
|
+
import { t as __exportAll } from "./rolldown-runtime-D8jQpDG7.js";
|
|
2
|
+
import { t as cn$1 } from "./utils-BER_bdgE.js";
|
|
3
|
+
import { i as modelBrandFor, t as ModelBrandStack } from "./model-brand-bTcMjQtB.js";
|
|
4
|
+
import { t as ProviderIcon } from "./provider-logo-CPKRPHcS.js";
|
|
5
|
+
import { f as syncAmbient, l as activityTiming, t as AgentGraph, u as ambientStyle } from "./AgentGraph-CFlI9qta.js";
|
|
6
|
+
import { t as LatestEvents } from "./latest-events-v_vXRyd7.js";
|
|
7
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
8
|
+
import { useCallback, useEffect, useLayoutEffect, useMemo, useReducer, useRef, useState } from "react";
|
|
9
|
+
import { X } from "lucide-react";
|
|
10
|
+
import { getToolDisplayMetadata } from "@tangle-network/ui/utils";
|
|
11
|
+
import { AgentTimeline, AgentTimelineColumn, AgentTimelineStep, SpeakerHeader, agentTimelineStepKind } from "@tangle-network/ui/chat";
|
|
12
|
+
import { ApprovalCard, ApprovalDiffSummary, ExpandedToolDetail, InlineThinkingItem, InlineThinkingItem as InlineThinkingItem$1, InlineToolItem, LiveDuration, RunGroup, RunPhaseList, RunRowShell, RunRowShell as RunRowShell$1, RunRowStatusDot, RunStatusIcon as RunStatusIcon$1, TaskList, ToolCallFeed, parseToolEvent, runStatus, runStatus as runStatus$1, showsTaskPriority, statusMeta as statusMeta$1, taskProgress, taskStatus } from "@tangle-network/ui/run";
|
|
13
|
+
import { FlowWaterfall, toolGlyph } from "@tangle-network/charts/react";
|
|
14
|
+
// src/run/trace-waterfall.tsx
|
|
15
|
+
/** Agent and model spans show their lab/host mark; tool spans show their provider's mark. Unknown keeps the kind glyph. */
|
|
16
|
+
function traceSpanMark(span) {
|
|
17
|
+
if (span.kind === "pipeline" || span.kind === "model") {
|
|
18
|
+
const identity = span.detail ? modelBrandFor(span.detail) : null;
|
|
19
|
+
return identity ? /* @__PURE__ */ jsx(ModelBrandStack, {
|
|
20
|
+
identity,
|
|
21
|
+
size: "sm"
|
|
22
|
+
}) : void 0;
|
|
23
|
+
}
|
|
24
|
+
const provider = span.icon ?? (span.kind === "tool" ? span.detail : void 0);
|
|
25
|
+
return provider ? /* @__PURE__ */ jsx(ProviderIcon, {
|
|
26
|
+
id: provider,
|
|
27
|
+
displayName: provider,
|
|
28
|
+
size: 14,
|
|
29
|
+
className: "rounded-[3px]"
|
|
30
|
+
}) : void 0;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* The run inspector: the agent timeline (`defaultView="agents"`) and the span waterfall over one clock, from
|
|
34
|
+
* `@tangle-network/charts`, with Sandbox UI's brand marks. Feed it `otlpFlowTrace(spans)` from the
|
|
35
|
+
* `traces convert` contract. Import `@tangle-network/charts/react/flow-waterfall.css` once.
|
|
36
|
+
*/
|
|
37
|
+
function TraceWaterfall({ renderIcon = traceSpanMark, ...props }) {
|
|
38
|
+
return /* @__PURE__ */ jsx(FlowWaterfall, {
|
|
39
|
+
renderIcon,
|
|
40
|
+
...props
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
// src/run/agent-stream.ts
|
|
44
|
+
const TOOL_TYPES = /* @__PURE__ */ new Set([
|
|
45
|
+
"bash",
|
|
46
|
+
"read",
|
|
47
|
+
"write",
|
|
48
|
+
"edit",
|
|
49
|
+
"glob",
|
|
50
|
+
"grep",
|
|
51
|
+
"list",
|
|
52
|
+
"download",
|
|
53
|
+
"inspect",
|
|
54
|
+
"audit"
|
|
55
|
+
]);
|
|
56
|
+
const toolType = (name) => {
|
|
57
|
+
const key = (name ?? "").toLowerCase();
|
|
58
|
+
return TOOL_TYPES.has(key) ? key : "unknown";
|
|
59
|
+
};
|
|
60
|
+
const brief = (value, max = 240) => {
|
|
61
|
+
if (value === void 0 || value === null) return void 0;
|
|
62
|
+
const text = typeof value === "string" ? value : JSON.stringify(value);
|
|
63
|
+
return text.length > max ? `${text.slice(0, max - 1)}…` : text;
|
|
64
|
+
};
|
|
65
|
+
const failed = (result) => typeof result === "object" && result !== null && ("error" in result || result.is_error === true);
|
|
66
|
+
const recorded = (text) => {
|
|
67
|
+
try {
|
|
68
|
+
const value = JSON.parse(text);
|
|
69
|
+
return typeof value === "object" && value !== null ? `\`\`\`json\n${text}\n\`\`\`` : text;
|
|
70
|
+
} catch {
|
|
71
|
+
return text;
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
/** Fold stream rows into transcript entries: text of one turn merges, a tool call becomes a row its result completes. */
|
|
75
|
+
function foldAgentRows(entries, rows) {
|
|
76
|
+
const next = [...entries];
|
|
77
|
+
for (const row of rows) {
|
|
78
|
+
const at = Date.parse(row.at);
|
|
79
|
+
const { event } = row;
|
|
80
|
+
const last = next[next.length - 1];
|
|
81
|
+
if (event.kind === "text" && event.text) {
|
|
82
|
+
if (last?.kind === "message" && last.role === "assistant" && !last.sealed) next[next.length - 1] = {
|
|
83
|
+
...last,
|
|
84
|
+
content: last.content + event.text
|
|
85
|
+
};
|
|
86
|
+
else next.push({
|
|
87
|
+
id: `m-${row.seq}`,
|
|
88
|
+
at,
|
|
89
|
+
kind: "message",
|
|
90
|
+
role: "assistant",
|
|
91
|
+
content: event.text
|
|
92
|
+
});
|
|
93
|
+
} else if (event.kind === "user" && event.text) next.push({
|
|
94
|
+
id: `u-${row.seq}`,
|
|
95
|
+
at,
|
|
96
|
+
kind: "message",
|
|
97
|
+
role: "user",
|
|
98
|
+
content: event.text
|
|
99
|
+
});
|
|
100
|
+
else if (event.kind === "notice" && event.text) {
|
|
101
|
+
if (event.provenance === "delegated-input") next.push({
|
|
102
|
+
id: `u-${row.seq}`,
|
|
103
|
+
at,
|
|
104
|
+
kind: "message",
|
|
105
|
+
role: "user",
|
|
106
|
+
content: recorded(event.text)
|
|
107
|
+
});
|
|
108
|
+
else if (event.provenance === "recorded-result") next.push({
|
|
109
|
+
id: `m-${row.seq}`,
|
|
110
|
+
at,
|
|
111
|
+
kind: "message",
|
|
112
|
+
role: "assistant",
|
|
113
|
+
content: recorded(event.text),
|
|
114
|
+
sealed: true
|
|
115
|
+
});
|
|
116
|
+
else next.push({
|
|
117
|
+
id: `n-${row.seq}`,
|
|
118
|
+
at,
|
|
119
|
+
kind: "notice",
|
|
120
|
+
text: event.text
|
|
121
|
+
});
|
|
122
|
+
} else if (event.kind === "reasoning" && event.text) {
|
|
123
|
+
if (last?.kind === "reasoning") next[next.length - 1] = {
|
|
124
|
+
...last,
|
|
125
|
+
text: last.text + event.text
|
|
126
|
+
};
|
|
127
|
+
else next.push({
|
|
128
|
+
id: `r-${row.seq}`,
|
|
129
|
+
at,
|
|
130
|
+
kind: "reasoning",
|
|
131
|
+
text: event.text
|
|
132
|
+
});
|
|
133
|
+
} else if (event.kind === "tool-call") {
|
|
134
|
+
const id = event.toolCallId ?? `t-${row.seq}`;
|
|
135
|
+
next.push({
|
|
136
|
+
id,
|
|
137
|
+
at,
|
|
138
|
+
kind: "tool",
|
|
139
|
+
call: {
|
|
140
|
+
id,
|
|
141
|
+
type: toolType(event.toolName),
|
|
142
|
+
label: event.toolName ?? "tool",
|
|
143
|
+
status: "running",
|
|
144
|
+
detail: brief(event.args, 120)
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
} else if (event.kind === "tool-result") {
|
|
148
|
+
const index = next.findIndex((entry) => entry.kind === "tool" && entry.call.id === event.toolCallId);
|
|
149
|
+
const entry = next[index];
|
|
150
|
+
if (entry?.kind === "tool") next[index] = {
|
|
151
|
+
...entry,
|
|
152
|
+
call: {
|
|
153
|
+
...entry.call,
|
|
154
|
+
status: failed(event.result) ? "error" : "success",
|
|
155
|
+
output: brief(event.result),
|
|
156
|
+
duration: Math.max(0, at - entry.at)
|
|
157
|
+
}
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return next;
|
|
162
|
+
}
|
|
163
|
+
function reduce(state, action) {
|
|
164
|
+
switch (action.type) {
|
|
165
|
+
case "reset": return {
|
|
166
|
+
generation: action.generation,
|
|
167
|
+
entries: [],
|
|
168
|
+
lastSeq: 0,
|
|
169
|
+
alive: true
|
|
170
|
+
};
|
|
171
|
+
case "rows": {
|
|
172
|
+
const base = action.generation !== void 0 && action.generation !== state.generation ? {
|
|
173
|
+
entries: [],
|
|
174
|
+
lastSeq: 0
|
|
175
|
+
} : state;
|
|
176
|
+
const fresh = action.rows.filter((row) => row.seq > base.lastSeq);
|
|
177
|
+
return {
|
|
178
|
+
generation: action.generation ?? state.generation,
|
|
179
|
+
entries: foldAgentRows(base.entries, fresh),
|
|
180
|
+
lastSeq: fresh.reduce((max, row) => Math.max(max, row.seq), base.lastSeq),
|
|
181
|
+
alive: action.alive ?? state.alive,
|
|
182
|
+
error: void 0
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
case "end": return {
|
|
186
|
+
...state,
|
|
187
|
+
alive: false,
|
|
188
|
+
error: void 0
|
|
189
|
+
};
|
|
190
|
+
case "error": return {
|
|
191
|
+
...state,
|
|
192
|
+
error: action.message
|
|
193
|
+
};
|
|
194
|
+
case "unreachable": return {
|
|
195
|
+
...state,
|
|
196
|
+
error: "The agent stream is unreachable; the viewer may be closed."
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Follow one agent's message stream over Server-Sent Events: `rows`, `reset` and `end` events, reconnecting with
|
|
202
|
+
* `?after=<lastSeq>` when the server closes the connection, so a long turn streams without gaps or repeats.
|
|
203
|
+
*/
|
|
204
|
+
function useAgentStream(url) {
|
|
205
|
+
const [state, dispatch] = useReducer(reduce, {
|
|
206
|
+
entries: [],
|
|
207
|
+
lastSeq: 0,
|
|
208
|
+
alive: false
|
|
209
|
+
});
|
|
210
|
+
useEffect(() => {
|
|
211
|
+
if (!url || typeof EventSource === "undefined") return;
|
|
212
|
+
dispatch({ type: "reset" });
|
|
213
|
+
let lastSeq = 0;
|
|
214
|
+
let closed = false;
|
|
215
|
+
let source;
|
|
216
|
+
let retry;
|
|
217
|
+
const open = () => {
|
|
218
|
+
const target = new URL(url, globalThis.location?.href);
|
|
219
|
+
if (lastSeq > 0) target.searchParams.set("after", String(lastSeq));
|
|
220
|
+
source = new EventSource(target.toString());
|
|
221
|
+
let opened = false;
|
|
222
|
+
source.onopen = () => {
|
|
223
|
+
opened = true;
|
|
224
|
+
};
|
|
225
|
+
source.addEventListener("rows", (message) => {
|
|
226
|
+
const data = JSON.parse(message.data);
|
|
227
|
+
lastSeq = Math.max(lastSeq, data.lastSeq ?? 0, ...data.events.map((row) => row.seq));
|
|
228
|
+
dispatch({
|
|
229
|
+
type: "rows",
|
|
230
|
+
generation: data.generation,
|
|
231
|
+
rows: data.events,
|
|
232
|
+
alive: data.alive
|
|
233
|
+
});
|
|
234
|
+
});
|
|
235
|
+
source.addEventListener("reset", (message) => {
|
|
236
|
+
lastSeq = 0;
|
|
237
|
+
dispatch({
|
|
238
|
+
type: "reset",
|
|
239
|
+
generation: JSON.parse(message.data).generation
|
|
240
|
+
});
|
|
241
|
+
});
|
|
242
|
+
source.addEventListener("end", () => {
|
|
243
|
+
closed = true;
|
|
244
|
+
source?.close();
|
|
245
|
+
dispatch({ type: "end" });
|
|
246
|
+
});
|
|
247
|
+
source.onerror = () => {
|
|
248
|
+
source?.close();
|
|
249
|
+
if (closed) return;
|
|
250
|
+
if (!opened) dispatch({ type: "unreachable" });
|
|
251
|
+
retry = setTimeout(open, 1e3);
|
|
252
|
+
};
|
|
253
|
+
};
|
|
254
|
+
open();
|
|
255
|
+
return () => {
|
|
256
|
+
closed = true;
|
|
257
|
+
clearTimeout(retry);
|
|
258
|
+
source?.close();
|
|
259
|
+
};
|
|
260
|
+
}, [url]);
|
|
261
|
+
return state;
|
|
262
|
+
}
|
|
263
|
+
// src/run/trace-workspace.tsx
|
|
264
|
+
/** The agent that owns a span: the span itself when it is an agent, else its nearest agent ancestor. */
|
|
265
|
+
function agentOf(span, byId) {
|
|
266
|
+
for (let cursor = span; cursor; cursor = cursor.parentId ? byId.get(cursor.parentId) : void 0) if (cursor.kind === "pipeline") return cursor;
|
|
267
|
+
}
|
|
268
|
+
/** The trace's agents as a spawn tree at the cursor: an agent not started by then is left out, one that ends after it is
|
|
269
|
+
* still running, so the Graph and the Timeline always show the same moment. */
|
|
270
|
+
function agentGraphAt(spans, byId, at, epoch, cards, extraEdges = []) {
|
|
271
|
+
const nodes = [], edges = [], seen = /* @__PURE__ */ new Set();
|
|
272
|
+
const timed = new Set(spans.flatMap((span) => span.kind === "pipeline" && span.id ? [span.id] : []));
|
|
273
|
+
const recorded = /* @__PURE__ */ new Map();
|
|
274
|
+
for (const [index, span] of spans.entries()) {
|
|
275
|
+
if (span.kind === "pipeline" || at !== null && span.startMs > at) continue;
|
|
276
|
+
const owner = agentOf(span, byId);
|
|
277
|
+
if (!owner?.id) continue;
|
|
278
|
+
const events = recorded.get(owner.id) ?? [];
|
|
279
|
+
const endpoints = [{
|
|
280
|
+
suffix: "start",
|
|
281
|
+
label: "Started ",
|
|
282
|
+
time: span.startMs
|
|
283
|
+
}];
|
|
284
|
+
if (span.status !== "running" && span.endMs !== span.startMs && (at === null || span.endMs <= at)) endpoints.push({
|
|
285
|
+
suffix: "end",
|
|
286
|
+
label: "Recorded ",
|
|
287
|
+
time: span.endMs
|
|
288
|
+
});
|
|
289
|
+
for (const endpoint of endpoints) events.push({
|
|
290
|
+
id: (span.id ?? String(index)) + ":" + endpoint.suffix,
|
|
291
|
+
label: endpoint.label + span.name,
|
|
292
|
+
at: epoch == null || !Number.isFinite(epoch) ? void 0 : epoch + endpoint.time
|
|
293
|
+
});
|
|
294
|
+
recorded.set(owner.id, events);
|
|
295
|
+
}
|
|
296
|
+
for (const span of spans) {
|
|
297
|
+
if (span.kind !== "pipeline" || !span.id || seen.has(span.id) || at !== null && span.startMs > at) continue;
|
|
298
|
+
seen.add(span.id);
|
|
299
|
+
const card = cards?.[span.id];
|
|
300
|
+
const state = at !== null && (span.endMs > at || span.status === "running") ? "running" : runStatus(span.status ?? "unknown");
|
|
301
|
+
const activity = (span.activityAt ?? []).reduce((last, time) => Number.isFinite(time) && (at === null || time <= at) ? Math.max(last, time) : last, -Infinity);
|
|
302
|
+
const events = (card?.events ?? recorded.get(span.id) ?? []).filter((event) => at === null || epoch != null && event.at != null && event.at <= epoch + at).sort((a, b) => (a.at ?? 0) - (b.at ?? 0));
|
|
303
|
+
const timing = activityTiming({
|
|
304
|
+
events,
|
|
305
|
+
lastActivityAt: epoch != null && Number.isFinite(epoch) && Number.isFinite(activity) ? epoch + activity : at === null ? card?.lastActivityAt : void 0,
|
|
306
|
+
medianEventGapMs: span.medianEventGapMs ?? (at === null ? card?.medianEventGapMs : void 0)
|
|
307
|
+
});
|
|
308
|
+
nodes.push({
|
|
309
|
+
...card,
|
|
310
|
+
id: span.id,
|
|
311
|
+
label: card?.label ?? span.name,
|
|
312
|
+
model: card?.model ?? span.detail,
|
|
313
|
+
status: at === null && card?.status ? card.status : state === "done" ? "succeeded" : state,
|
|
314
|
+
costUsd: at === null ? card?.costUsd ?? span.costUsd : span.endMs <= at ? span.costUsd : void 0,
|
|
315
|
+
startedAt: epoch == null || !Number.isFinite(epoch) ? at === null ? card?.startedAt : void 0 : epoch + span.startMs,
|
|
316
|
+
...timing,
|
|
317
|
+
events: events.slice(-4),
|
|
318
|
+
turn: at === null ? card?.turn : void 0,
|
|
319
|
+
result: at === null ? card?.result ?? span.output : span.endMs <= at ? span.output : void 0,
|
|
320
|
+
commits: at === null ? card?.commits : void 0,
|
|
321
|
+
tokens: at === null ? card?.tokens : void 0
|
|
322
|
+
});
|
|
323
|
+
const parent = span.parentId ? byId.get(span.parentId) : void 0, spawner = parent && agentOf(parent, byId);
|
|
324
|
+
if (spawner?.id) edges.push({
|
|
325
|
+
from: spawner.id,
|
|
326
|
+
to: span.id,
|
|
327
|
+
kind: "spawn"
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
for (const [id, card] of Object.entries(cards ?? {})) if (!timed.has(id)) nodes.push(at === null ? {
|
|
331
|
+
...card,
|
|
332
|
+
id,
|
|
333
|
+
label: card.label ?? id,
|
|
334
|
+
status: card.status ?? "unknown"
|
|
335
|
+
} : {
|
|
336
|
+
id,
|
|
337
|
+
label: card.label ?? id,
|
|
338
|
+
model: card.model,
|
|
339
|
+
harness: card.harness,
|
|
340
|
+
role: card.role,
|
|
341
|
+
status: "unknown",
|
|
342
|
+
turn: "Timing unavailable at this cursor"
|
|
343
|
+
});
|
|
344
|
+
const ids = new Set(nodes.map((node) => node.id));
|
|
345
|
+
for (const edge of extraEdges) if (ids.has(edge.from) && ids.has(edge.to) && (edge.kind !== "spawn" || !edges.some((prior) => prior.kind === "spawn" && prior.from === edge.from && prior.to === edge.to))) edges.push(edge);
|
|
346
|
+
return {
|
|
347
|
+
nodes,
|
|
348
|
+
edges
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
function timelineItems(entries, streaming) {
|
|
352
|
+
return entries.map((entry, index) => {
|
|
353
|
+
if (entry.kind === "message") return {
|
|
354
|
+
id: entry.id,
|
|
355
|
+
kind: "message",
|
|
356
|
+
role: entry.role,
|
|
357
|
+
content: entry.content,
|
|
358
|
+
isStreaming: streaming && index === entries.length - 1
|
|
359
|
+
};
|
|
360
|
+
if (entry.kind === "tool") return {
|
|
361
|
+
id: entry.id,
|
|
362
|
+
kind: "tool",
|
|
363
|
+
call: entry.call
|
|
364
|
+
};
|
|
365
|
+
if (entry.kind === "notice") return {
|
|
366
|
+
id: entry.id,
|
|
367
|
+
kind: "custom",
|
|
368
|
+
content: /* @__PURE__ */ jsx("p", {
|
|
369
|
+
className: "text-xs text-muted-foreground",
|
|
370
|
+
children: entry.text
|
|
371
|
+
})
|
|
372
|
+
};
|
|
373
|
+
return {
|
|
374
|
+
id: entry.id,
|
|
375
|
+
kind: "custom",
|
|
376
|
+
content: /* @__PURE__ */ jsx(InlineThinkingItem, {
|
|
377
|
+
part: {
|
|
378
|
+
type: "reasoning",
|
|
379
|
+
text: entry.text
|
|
380
|
+
},
|
|
381
|
+
defaultOpen: false,
|
|
382
|
+
openWhileActive: false
|
|
383
|
+
})
|
|
384
|
+
};
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
function NotTimeIndexed({ label }) {
|
|
388
|
+
return /* @__PURE__ */ jsxs("p", {
|
|
389
|
+
className: "p-4 text-sm text-muted-foreground",
|
|
390
|
+
children: [label, ": not time-indexed"]
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
/**
|
|
394
|
+
* The run workspace: the agent timeline and span waterfall over one clock, the selected agent's conversation
|
|
395
|
+
* streaming in a right-hand drawer, and the page's code workspace and scoreboard following the time cursor. Scrubbing is time travel:
|
|
396
|
+
* every panel shows what was true at the cursor, and the conversation shows what had been said by then.
|
|
397
|
+
*/
|
|
398
|
+
function TraceWorkspace({ trace, streamUrl, events, onTimeChange, files, scoreboard, selectedAgentId, onSelectAgent, agentCards, agentGraph, className, ...waterfall }) {
|
|
399
|
+
const byId = useMemo(() => new Map(trace.spans.flatMap((span) => span.id ? [[span.id, span]] : [])), [trace]);
|
|
400
|
+
const [ownSelection, setOwnSelection] = useState();
|
|
401
|
+
const selected = selectedAgentId ?? ownSelection;
|
|
402
|
+
const select = (agentId) => {
|
|
403
|
+
setOwnSelection(agentId);
|
|
404
|
+
onSelectAgent?.(agentId);
|
|
405
|
+
};
|
|
406
|
+
const agent = selected ? byId.get(selected) : void 0, card = selected ? agentCards?.[selected] : void 0;
|
|
407
|
+
const agentId = agent?.id ?? (card ? selected : void 0), agentName = agent?.name ?? card?.label ?? agentId, agentModel = agent?.detail ?? card?.model;
|
|
408
|
+
const { defaultLayout = "tree", ...graphOptions } = agentGraph ?? {};
|
|
409
|
+
const [graphLayout, setGraphLayout] = useState(defaultLayout);
|
|
410
|
+
const [cursor, setCursor] = useState(null);
|
|
411
|
+
const stream = useAgentStream(selected && streamUrl ? streamUrl(selected) : void 0);
|
|
412
|
+
const origin = useMemo(() => {
|
|
413
|
+
if (waterfall.startedAt !== void 0) return waterfall.startedAt;
|
|
414
|
+
const first = stream.entries[0]?.at;
|
|
415
|
+
return agent && first !== void 0 ? first - agent.startMs : void 0;
|
|
416
|
+
}, [
|
|
417
|
+
waterfall.startedAt,
|
|
418
|
+
agent,
|
|
419
|
+
stream.entries
|
|
420
|
+
]);
|
|
421
|
+
const graph = useMemo(() => agentGraphAt(trace.spans, byId, cursor, waterfall.startedAt, agentCards, agentGraph?.edges), [
|
|
422
|
+
trace,
|
|
423
|
+
byId,
|
|
424
|
+
cursor,
|
|
425
|
+
waterfall.startedAt,
|
|
426
|
+
agentCards,
|
|
427
|
+
agentGraph?.edges
|
|
428
|
+
]);
|
|
429
|
+
const selectedNode = graph.nodes.find((node) => node.id === agentId);
|
|
430
|
+
const facts = cursor === null || agent && agent.endMs <= cursor ? [...agent?.facts ?? []] : [];
|
|
431
|
+
for (const [label, value] of [["Recorded result", selectedNode?.result], ["Activity", selectedNode?.turn]]) if (value && !facts.some((fact) => fact.value === value)) facts.push({
|
|
432
|
+
label,
|
|
433
|
+
value
|
|
434
|
+
});
|
|
435
|
+
const graphNow = cursor !== null && waterfall.startedAt !== void 0 ? waterfall.startedAt + cursor : void 0;
|
|
436
|
+
const shown = cursor === null || origin === void 0 ? stream.entries : stream.entries.filter((entry) => entry.at - origin <= cursor);
|
|
437
|
+
const live = stream.alive && !stream.error && cursor === null;
|
|
438
|
+
const items = useMemo(() => timelineItems(shown, live), [shown, live]);
|
|
439
|
+
const scroller = useRef(null);
|
|
440
|
+
const pinned = useRef(true);
|
|
441
|
+
const [unseen, setUnseen] = useState(false);
|
|
442
|
+
useLayoutEffect(() => {
|
|
443
|
+
const element = scroller.current;
|
|
444
|
+
if (!element) return;
|
|
445
|
+
if (pinned.current) element.scrollTop = element.scrollHeight;
|
|
446
|
+
else setUnseen(true);
|
|
447
|
+
}, [items.length, items[items.length - 1]]);
|
|
448
|
+
useEffect(() => {
|
|
449
|
+
pinned.current = true;
|
|
450
|
+
setUnseen(false);
|
|
451
|
+
}, [selected]);
|
|
452
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
453
|
+
className: cn$1("sandbox-trace-workspace flex min-h-0 flex-col gap-3", className),
|
|
454
|
+
children: [
|
|
455
|
+
/* @__PURE__ */ jsx("div", {
|
|
456
|
+
className: agentId ? "min-w-0 lg:mr-[420px]" : "min-w-0",
|
|
457
|
+
children: /* @__PURE__ */ jsx(TraceWaterfall, {
|
|
458
|
+
...waterfall,
|
|
459
|
+
trace,
|
|
460
|
+
markers: events,
|
|
461
|
+
selectedId: agentId,
|
|
462
|
+
onDeselect: () => select(void 0),
|
|
463
|
+
onSelect: (span) => {
|
|
464
|
+
const owner = agentOf(span, byId);
|
|
465
|
+
if (owner?.id) select(owner.id);
|
|
466
|
+
},
|
|
467
|
+
onTimeChange: (time) => {
|
|
468
|
+
setCursor(time);
|
|
469
|
+
onTimeChange?.(time);
|
|
470
|
+
},
|
|
471
|
+
latestEvents: /* @__PURE__ */ jsx(LatestEvents, {
|
|
472
|
+
agents: graph.nodes,
|
|
473
|
+
now: graphNow,
|
|
474
|
+
live: cursor === null,
|
|
475
|
+
selectedId: agentId,
|
|
476
|
+
onSelect: select
|
|
477
|
+
}),
|
|
478
|
+
graph: /* @__PURE__ */ jsxs("section", {
|
|
479
|
+
className: "flex h-[60vh] min-h-[480px] flex-col",
|
|
480
|
+
"aria-label": "Agent graph",
|
|
481
|
+
children: [/* @__PURE__ */ jsxs("label", {
|
|
482
|
+
className: "flex h-8 shrink-0 items-center justify-end gap-2 px-3 text-xs text-muted-foreground",
|
|
483
|
+
children: ["Layout", /* @__PURE__ */ jsxs("select", {
|
|
484
|
+
"aria-label": "Graph layout",
|
|
485
|
+
value: graphLayout,
|
|
486
|
+
onChange: (event) => setGraphLayout(event.currentTarget.value),
|
|
487
|
+
className: "rounded-[var(--radius-control)] border border-border bg-card px-2 py-1 text-foreground",
|
|
488
|
+
children: [/* @__PURE__ */ jsx("option", {
|
|
489
|
+
value: "tree",
|
|
490
|
+
children: "Tree"
|
|
491
|
+
}), /* @__PURE__ */ jsx("option", {
|
|
492
|
+
value: "hub",
|
|
493
|
+
children: "Hub"
|
|
494
|
+
})]
|
|
495
|
+
})]
|
|
496
|
+
}), /* @__PURE__ */ jsx(AgentGraph, {
|
|
497
|
+
...graphOptions,
|
|
498
|
+
layout: graphLayout,
|
|
499
|
+
renderEventIcon: (event) => toolGlyph(event.label),
|
|
500
|
+
nodes: graph.nodes,
|
|
501
|
+
edges: graph.edges,
|
|
502
|
+
selectedId: agentId ?? null,
|
|
503
|
+
onSelect: select,
|
|
504
|
+
showLatestEvents: false,
|
|
505
|
+
now: graphNow,
|
|
506
|
+
live: cursor === null && graph.nodes.some((node) => [
|
|
507
|
+
"running",
|
|
508
|
+
"waiting",
|
|
509
|
+
"needs-you",
|
|
510
|
+
"failed"
|
|
511
|
+
].includes(node.status)),
|
|
512
|
+
className: cn$1("min-h-0 flex-1", graphOptions.className)
|
|
513
|
+
})]
|
|
514
|
+
})
|
|
515
|
+
})
|
|
516
|
+
}),
|
|
517
|
+
files && /* @__PURE__ */ jsx("section", {
|
|
518
|
+
"aria-label": "Code at the cursor",
|
|
519
|
+
className: "flex min-h-[70vh] flex-col overflow-hidden rounded-xl border border-border bg-card",
|
|
520
|
+
children: files
|
|
521
|
+
}),
|
|
522
|
+
scoreboard && /* @__PURE__ */ jsx("section", {
|
|
523
|
+
"aria-label": "Scoreboard at the cursor",
|
|
524
|
+
className: "overflow-hidden rounded-xl border border-border bg-card",
|
|
525
|
+
children: scoreboard
|
|
526
|
+
}),
|
|
527
|
+
agentId && /* @__PURE__ */ jsxs("aside", {
|
|
528
|
+
"aria-label": `${agentName} conversation`,
|
|
529
|
+
onKeyDown: (event) => {
|
|
530
|
+
if (event.key === "Escape") select(void 0);
|
|
531
|
+
},
|
|
532
|
+
className: "fixed inset-y-0 right-0 z-40 flex w-full max-w-[420px] flex-col border-l border-border bg-card shadow-2xl",
|
|
533
|
+
children: [
|
|
534
|
+
/* @__PURE__ */ jsxs("header", {
|
|
535
|
+
className: "flex shrink-0 items-center gap-2 border-b border-border px-3 py-2",
|
|
536
|
+
children: [
|
|
537
|
+
/* @__PURE__ */ jsx("span", {
|
|
538
|
+
className: cn$1("h-1.5 w-1.5 shrink-0 rounded-full", live ? "bg-[var(--surface-info-text)]" : "bg-muted-foreground"),
|
|
539
|
+
"aria-hidden": true,
|
|
540
|
+
"data-motion": live ? "essential" : void 0,
|
|
541
|
+
style: live ? ambientStyle : void 0,
|
|
542
|
+
onAnimationStart: syncAmbient
|
|
543
|
+
}),
|
|
544
|
+
/* @__PURE__ */ jsx("h3", {
|
|
545
|
+
className: "min-w-0 flex-1 truncate text-sm font-semibold",
|
|
546
|
+
children: agentName
|
|
547
|
+
}),
|
|
548
|
+
agentModel && /* @__PURE__ */ jsx("span", {
|
|
549
|
+
className: "truncate text-xs text-muted-foreground",
|
|
550
|
+
children: agentModel
|
|
551
|
+
}),
|
|
552
|
+
/* @__PURE__ */ jsx("button", {
|
|
553
|
+
type: "button",
|
|
554
|
+
"aria-label": "Close conversation",
|
|
555
|
+
title: "Close (Esc)",
|
|
556
|
+
onClick: () => select(void 0),
|
|
557
|
+
className: "shrink-0 rounded-md p-1 text-muted-foreground hover:bg-muted hover:text-foreground",
|
|
558
|
+
children: /* @__PURE__ */ jsx(X, {
|
|
559
|
+
"aria-hidden": true,
|
|
560
|
+
className: "h-4 w-4"
|
|
561
|
+
})
|
|
562
|
+
})
|
|
563
|
+
]
|
|
564
|
+
}),
|
|
565
|
+
facts.length > 0 && /* @__PURE__ */ jsx("section", {
|
|
566
|
+
"aria-label": "Recorded agent details",
|
|
567
|
+
tabIndex: 0,
|
|
568
|
+
className: "max-h-[35vh] shrink-0 overflow-y-auto border-b border-border px-3 py-2 text-sm",
|
|
569
|
+
children: /* @__PURE__ */ jsx("dl", {
|
|
570
|
+
className: "space-y-2",
|
|
571
|
+
children: facts.map((fact, index) => /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("dt", {
|
|
572
|
+
className: "text-xs text-muted-foreground",
|
|
573
|
+
children: fact.label
|
|
574
|
+
}), /* @__PURE__ */ jsx("dd", {
|
|
575
|
+
className: "m-0 whitespace-pre-wrap break-words",
|
|
576
|
+
children: fact.value
|
|
577
|
+
})] }, index))
|
|
578
|
+
})
|
|
579
|
+
}),
|
|
580
|
+
/* @__PURE__ */ jsx("div", {
|
|
581
|
+
ref: scroller,
|
|
582
|
+
onScroll: (event) => {
|
|
583
|
+
const element = event.currentTarget;
|
|
584
|
+
pinned.current = element.scrollHeight - element.scrollTop - element.clientHeight < 48;
|
|
585
|
+
if (pinned.current) setUnseen(false);
|
|
586
|
+
},
|
|
587
|
+
"data-primary-content": "",
|
|
588
|
+
className: "min-h-0 flex-1 overflow-y-auto px-3 py-2",
|
|
589
|
+
children: streamUrl && (cursor === null || origin !== void 0) ? /* @__PURE__ */ jsx(AgentTimeline, {
|
|
590
|
+
items,
|
|
591
|
+
isThinking: live && items.length === 0,
|
|
592
|
+
emptyState: /* @__PURE__ */ jsx("p", {
|
|
593
|
+
className: "p-4 text-sm text-muted-foreground",
|
|
594
|
+
children: stream.error ?? "No messages yet."
|
|
595
|
+
})
|
|
596
|
+
}) : /* @__PURE__ */ jsx(NotTimeIndexed, { label: "Conversation" })
|
|
597
|
+
}),
|
|
598
|
+
unseen && /* @__PURE__ */ jsx("button", {
|
|
599
|
+
type: "button",
|
|
600
|
+
onClick: () => {
|
|
601
|
+
const element = scroller.current;
|
|
602
|
+
if (element) element.scrollTop = element.scrollHeight;
|
|
603
|
+
pinned.current = true;
|
|
604
|
+
setUnseen(false);
|
|
605
|
+
},
|
|
606
|
+
className: "absolute bottom-3 left-1/2 -translate-x-1/2 rounded-full border border-border bg-background px-3 py-1 text-xs shadow-sm",
|
|
607
|
+
children: "New output"
|
|
608
|
+
})
|
|
609
|
+
]
|
|
610
|
+
})
|
|
611
|
+
]
|
|
612
|
+
});
|
|
613
|
+
}
|
|
614
|
+
// src/run/record/VirtualList.tsx
|
|
615
|
+
/**
|
|
616
|
+
* Renders only the rows near the viewport of `scroller`. Row heights are measured as they render; unmeasured
|
|
617
|
+
* rows use `estimate`. Spacers keep the scroll height, so the browser's scroll anchoring holds the reading place.
|
|
618
|
+
*/
|
|
619
|
+
function VirtualList({ items, keyOf, render, scroller, estimate = 96, overscan = 900, target }) {
|
|
620
|
+
const heights = useRef(/* @__PURE__ */ new Map());
|
|
621
|
+
const [version, setVersion] = useState(0);
|
|
622
|
+
const [viewport, setViewport] = useState({
|
|
623
|
+
top: 0,
|
|
624
|
+
height: 900
|
|
625
|
+
});
|
|
626
|
+
const observer = useRef(null);
|
|
627
|
+
const nodes = useRef(/* @__PURE__ */ new Map());
|
|
628
|
+
const frame = useRef(0);
|
|
629
|
+
const observe = (element) => {
|
|
630
|
+
if (!observer.current && typeof ResizeObserver !== "undefined") observer.current = new ResizeObserver((entries) => {
|
|
631
|
+
let changed = false;
|
|
632
|
+
for (const entry of entries) {
|
|
633
|
+
const key = nodes.current.get(entry.target);
|
|
634
|
+
if (!key) continue;
|
|
635
|
+
const height = Math.ceil(entry.borderBoxSize?.[0]?.blockSize ?? entry.target.offsetHeight);
|
|
636
|
+
if (height > 0 && heights.current.get(key) !== height) {
|
|
637
|
+
heights.current.set(key, height);
|
|
638
|
+
changed = true;
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
if (changed && !frame.current) frame.current = requestAnimationFrame(() => {
|
|
642
|
+
frame.current = 0;
|
|
643
|
+
setVersion((value) => value + 1);
|
|
644
|
+
});
|
|
645
|
+
});
|
|
646
|
+
observer.current?.observe(element);
|
|
647
|
+
};
|
|
648
|
+
useEffect(() => () => {
|
|
649
|
+
cancelAnimationFrame(frame.current);
|
|
650
|
+
observer.current?.disconnect();
|
|
651
|
+
observer.current = null;
|
|
652
|
+
nodes.current.clear();
|
|
653
|
+
}, []);
|
|
654
|
+
useEffect(() => {
|
|
655
|
+
const element = scroller.current;
|
|
656
|
+
if (!element) return;
|
|
657
|
+
let frame = 0;
|
|
658
|
+
const update = () => {
|
|
659
|
+
frame = 0;
|
|
660
|
+
setViewport((current) => current.top === element.scrollTop && current.height === element.clientHeight ? current : {
|
|
661
|
+
top: element.scrollTop,
|
|
662
|
+
height: element.clientHeight
|
|
663
|
+
});
|
|
664
|
+
};
|
|
665
|
+
const schedule = () => {
|
|
666
|
+
if (!frame) frame = requestAnimationFrame(update);
|
|
667
|
+
};
|
|
668
|
+
update();
|
|
669
|
+
element.addEventListener("scroll", schedule, { passive: true });
|
|
670
|
+
const resize = new ResizeObserver(schedule);
|
|
671
|
+
resize.observe(element);
|
|
672
|
+
return () => {
|
|
673
|
+
cancelAnimationFrame(frame);
|
|
674
|
+
element.removeEventListener("scroll", schedule);
|
|
675
|
+
resize.disconnect();
|
|
676
|
+
};
|
|
677
|
+
}, [scroller]);
|
|
678
|
+
const offsets = useMemo(() => {
|
|
679
|
+
const result = new Float64Array(items.length + 1);
|
|
680
|
+
const guess = typeof estimate === "function" ? estimate : () => estimate;
|
|
681
|
+
for (let i = 0; i < items.length; i++) result[i + 1] = result[i] + (heights.current.get(keyOf(items[i])) ?? guess(items[i]));
|
|
682
|
+
return result;
|
|
683
|
+
}, [
|
|
684
|
+
items,
|
|
685
|
+
keyOf,
|
|
686
|
+
estimate,
|
|
687
|
+
version
|
|
688
|
+
]);
|
|
689
|
+
const find = (y) => {
|
|
690
|
+
let low = 0;
|
|
691
|
+
let high = items.length;
|
|
692
|
+
while (low < high) {
|
|
693
|
+
const mid = low + high >> 1;
|
|
694
|
+
if (offsets[mid + 1] <= y) low = mid + 1;
|
|
695
|
+
else high = mid;
|
|
696
|
+
}
|
|
697
|
+
return low;
|
|
698
|
+
};
|
|
699
|
+
const start = Math.max(0, find(viewport.top - overscan));
|
|
700
|
+
const end = Math.min(items.length, find(viewport.top + viewport.height + overscan) + 1);
|
|
701
|
+
const scrollTo = useCallback((key, align) => {
|
|
702
|
+
const element = scroller.current;
|
|
703
|
+
const index = items.findIndex((item) => keyOf(item) === key);
|
|
704
|
+
if (!element || index < 0) return false;
|
|
705
|
+
const top = offsets[index];
|
|
706
|
+
const bottom = offsets[index + 1];
|
|
707
|
+
if (align || top < element.scrollTop || bottom > element.scrollTop + element.clientHeight) element.scrollTop = Math.max(0, top - 8);
|
|
708
|
+
return true;
|
|
709
|
+
}, [
|
|
710
|
+
items,
|
|
711
|
+
keyOf,
|
|
712
|
+
offsets,
|
|
713
|
+
scroller
|
|
714
|
+
]);
|
|
715
|
+
const pending = useRef(null);
|
|
716
|
+
const rows = useRef(/* @__PURE__ */ new Map());
|
|
717
|
+
const settle = () => {
|
|
718
|
+
const goal = pending.current;
|
|
719
|
+
if (!goal) return;
|
|
720
|
+
if (performance.now() > goal.until) {
|
|
721
|
+
pending.current = null;
|
|
722
|
+
return;
|
|
723
|
+
}
|
|
724
|
+
if (!scrollTo(goal.key, true)) return;
|
|
725
|
+
const row = rows.current.get(goal.key);
|
|
726
|
+
if (row?.isConnected) {
|
|
727
|
+
const box = row.getBoundingClientRect();
|
|
728
|
+
if (box.top < 72 || box.top > window.innerHeight - 96) window.scrollBy({ top: box.top - 120 });
|
|
729
|
+
}
|
|
730
|
+
};
|
|
731
|
+
useLayoutEffect(() => {
|
|
732
|
+
pending.current = target ? {
|
|
733
|
+
key: target,
|
|
734
|
+
until: performance.now() + 2e3
|
|
735
|
+
} : null;
|
|
736
|
+
settle();
|
|
737
|
+
}, [target]);
|
|
738
|
+
useLayoutEffect(settle, [
|
|
739
|
+
version,
|
|
740
|
+
viewport,
|
|
741
|
+
items
|
|
742
|
+
]);
|
|
743
|
+
useEffect(() => {
|
|
744
|
+
const element = scroller.current;
|
|
745
|
+
if (!element) return;
|
|
746
|
+
const release = () => {
|
|
747
|
+
pending.current = null;
|
|
748
|
+
};
|
|
749
|
+
element.addEventListener("wheel", release, { passive: true });
|
|
750
|
+
element.addEventListener("touchstart", release, { passive: true });
|
|
751
|
+
element.addEventListener("pointerdown", release);
|
|
752
|
+
element.addEventListener("keydown", release);
|
|
753
|
+
return () => {
|
|
754
|
+
element.removeEventListener("wheel", release);
|
|
755
|
+
element.removeEventListener("touchstart", release);
|
|
756
|
+
element.removeEventListener("pointerdown", release);
|
|
757
|
+
element.removeEventListener("keydown", release);
|
|
758
|
+
};
|
|
759
|
+
}, [scroller]);
|
|
760
|
+
const bind = (key) => (element) => {
|
|
761
|
+
if (!element) {
|
|
762
|
+
rows.current.delete(key);
|
|
763
|
+
return;
|
|
764
|
+
}
|
|
765
|
+
rows.current.set(key, element);
|
|
766
|
+
for (const [node, value] of nodes.current) if (value === key && node !== element) {
|
|
767
|
+
observer.current?.unobserve(node);
|
|
768
|
+
nodes.current.delete(node);
|
|
769
|
+
}
|
|
770
|
+
if (!nodes.current.has(element)) {
|
|
771
|
+
nodes.current.set(element, key);
|
|
772
|
+
observe(element);
|
|
773
|
+
}
|
|
774
|
+
};
|
|
775
|
+
useEffect(() => {
|
|
776
|
+
for (const node of [...nodes.current.keys()]) if (!node.isConnected) {
|
|
777
|
+
observer.current?.unobserve(node);
|
|
778
|
+
nodes.current.delete(node);
|
|
779
|
+
}
|
|
780
|
+
});
|
|
781
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
782
|
+
/* @__PURE__ */ jsx("div", {
|
|
783
|
+
style: { height: offsets[start] },
|
|
784
|
+
"aria-hidden": "true"
|
|
785
|
+
}),
|
|
786
|
+
items.slice(start, end).map((item, i) => {
|
|
787
|
+
const key = keyOf(item);
|
|
788
|
+
return /* @__PURE__ */ jsx("div", {
|
|
789
|
+
ref: bind(key),
|
|
790
|
+
className: "virtual-row",
|
|
791
|
+
children: render(item, start + i)
|
|
792
|
+
}, key);
|
|
793
|
+
}),
|
|
794
|
+
/* @__PURE__ */ jsx("div", {
|
|
795
|
+
style: { height: offsets[items.length] - offsets[end] },
|
|
796
|
+
"aria-hidden": "true"
|
|
797
|
+
})
|
|
798
|
+
] });
|
|
799
|
+
}
|
|
800
|
+
// src/run/record/model.ts
|
|
801
|
+
var model_exports = /* @__PURE__ */ __exportAll({
|
|
802
|
+
agentState: () => agentState,
|
|
803
|
+
callsOf: () => callsOf,
|
|
804
|
+
compact: () => compact,
|
|
805
|
+
eventMatches: () => eventMatches,
|
|
806
|
+
eventSource: () => eventSource,
|
|
807
|
+
fmt: () => fmt,
|
|
808
|
+
hostless: () => hostless,
|
|
809
|
+
hoverLines: () => hoverLines,
|
|
810
|
+
indexRecord: () => indexRecord,
|
|
811
|
+
interval: () => interval,
|
|
812
|
+
measuredUsage: () => measuredUsage,
|
|
813
|
+
modelIdentity: () => modelIdentity,
|
|
814
|
+
ms: () => ms,
|
|
815
|
+
preview: () => preview,
|
|
816
|
+
roleOf: () => roleOf,
|
|
817
|
+
textOf: () => textOf,
|
|
818
|
+
toolKey: () => toolKey,
|
|
819
|
+
tooltipSections: () => tooltipSections,
|
|
820
|
+
utcTime: () => utcTime
|
|
821
|
+
});
|
|
822
|
+
const ms = (value) => Date.parse(value);
|
|
823
|
+
/** Normalize labels to UTC without rewriting the source timestamp. */
|
|
824
|
+
function utcTime(value, includeDate = false) {
|
|
825
|
+
const date = new Date(value);
|
|
826
|
+
if (!Number.isFinite(date.valueOf())) return "Unknown time";
|
|
827
|
+
const iso = date.toISOString();
|
|
828
|
+
return `${includeDate ? iso.slice(0, 19).replace("T", " ") : iso.slice(11, 19)} UTC`;
|
|
829
|
+
}
|
|
830
|
+
const compact = (value) => value.replace(/\s+/g, " ").trim();
|
|
831
|
+
/**
|
|
832
|
+
* Recorded text names the host folders an agent ran in (`/home/<user>/.cache/disco/host/<run>/sessions/<session>/inputs/x.md`).
|
|
833
|
+
* The reader needs the path inside the run, and a page must not publish the host's: a session or run folder reads as the
|
|
834
|
+
* path under it (`inputs/x.md`, or `./` for the folder itself), any other home folder as `~/`.
|
|
835
|
+
*/
|
|
836
|
+
const HOST_PATH = /\/(?:home|Users)\/[^/\s"'`]+(?:\/\.cache\/disco\/host\/[^/\s"'`]+(?:\/sessions\/[^/\s"'`]+)?)?(\/?)/g;
|
|
837
|
+
function hostless(text) {
|
|
838
|
+
if (!text || !(text.includes("/home/") || text.includes("/Users/"))) return text;
|
|
839
|
+
return text.replace(HOST_PATH, (match, slash, at, all) => {
|
|
840
|
+
if (!match.includes("/.cache/disco/host/")) return slash ? "~/" : "~";
|
|
841
|
+
const next = all[at + match.length];
|
|
842
|
+
return slash && next !== void 0 && !/[\s"'`)]/.test(next) ? "" : "./";
|
|
843
|
+
});
|
|
844
|
+
}
|
|
845
|
+
/** An event with every recorded text read through `hostless`; an event without a host path is returned as it is. */
|
|
846
|
+
function withoutHostPaths(event) {
|
|
847
|
+
const d = event.detail;
|
|
848
|
+
const text = hostless(d.publicText), reasoning = hostless(d.reasoning);
|
|
849
|
+
const calls = d.publicToolCalls?.map((call) => call.input && hostless(call.input) !== call.input ? {
|
|
850
|
+
...call,
|
|
851
|
+
input: hostless(call.input)
|
|
852
|
+
} : call);
|
|
853
|
+
if (text === d.publicText && reasoning === d.reasoning && calls?.every((call, i) => call === d.publicToolCalls[i]) !== false) return event;
|
|
854
|
+
return {
|
|
855
|
+
...event,
|
|
856
|
+
detail: {
|
|
857
|
+
...d,
|
|
858
|
+
...text !== void 0 ? { publicText: text } : {},
|
|
859
|
+
...reasoning !== void 0 ? { reasoning } : {},
|
|
860
|
+
...calls ? { publicToolCalls: calls } : {}
|
|
861
|
+
}
|
|
862
|
+
};
|
|
863
|
+
}
|
|
864
|
+
const fmt = (value) => new Intl.NumberFormat("en-US", {
|
|
865
|
+
notation: "compact",
|
|
866
|
+
maximumFractionDigits: 1
|
|
867
|
+
}).format(value);
|
|
868
|
+
const interval = (value) => value < 1e3 ? `${Math.round(value)} ms` : value < 6e4 ? `${(value / 1e3).toFixed(1)} s` : `${(value / 6e4).toFixed(1)} min`;
|
|
869
|
+
const roleOf = (node) => node?.role ?? (node?.kind === "session" ? "Session" : node?.kind === "finding" ? "Finding" : "Agent");
|
|
870
|
+
const textOf = (event) => event.detail.publicText ?? event.detail.recordedClaim ?? "";
|
|
871
|
+
const toolKey = (node, id) => JSON.stringify([node, id]);
|
|
872
|
+
function callsOf(event) {
|
|
873
|
+
const published = event.detail.publicToolCalls ?? [];
|
|
874
|
+
const byId = new Map(published.map((call) => [call.id, call]));
|
|
875
|
+
return [.../* @__PURE__ */ new Set([...event.detail.toolCallIds ?? [], ...published.map((call) => call.id)])].map((id) => byId.get(id) ?? {
|
|
876
|
+
id,
|
|
877
|
+
name: event.detail.toolNames?.length === 1 ? event.detail.toolNames[0] : "Tool call"
|
|
878
|
+
});
|
|
879
|
+
}
|
|
880
|
+
/** A run-scoped id (`<run>:s37`) reads as its own part. */
|
|
881
|
+
const local = (value) => value.replace(/^[a-z0-9][\w.-]*-\d{8}[a-z]?:/i, "");
|
|
882
|
+
/** A tool call's subject: its command, path or question, never its JSON. */
|
|
883
|
+
function preview(call) {
|
|
884
|
+
if (call.input === void 0) return "args not captured";
|
|
885
|
+
try {
|
|
886
|
+
const value = JSON.parse(call.input);
|
|
887
|
+
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
888
|
+
const args = value;
|
|
889
|
+
for (const key of [
|
|
890
|
+
"command",
|
|
891
|
+
"cmd",
|
|
892
|
+
"filePath",
|
|
893
|
+
"file_path",
|
|
894
|
+
"path",
|
|
895
|
+
"query",
|
|
896
|
+
"question",
|
|
897
|
+
"pattern",
|
|
898
|
+
"url",
|
|
899
|
+
"pageId",
|
|
900
|
+
"title",
|
|
901
|
+
"task",
|
|
902
|
+
"prompt",
|
|
903
|
+
"message"
|
|
904
|
+
]) if (typeof args[key] === "string" && args[key]) return compact(local(args[key]));
|
|
905
|
+
const words = Object.values(args).filter((item) => [
|
|
906
|
+
"string",
|
|
907
|
+
"number",
|
|
908
|
+
"boolean"
|
|
909
|
+
].includes(typeof item));
|
|
910
|
+
if (words.length) return compact(words.map((item) => local(String(item))).join(" · "));
|
|
911
|
+
}
|
|
912
|
+
} catch {}
|
|
913
|
+
return compact(call.input);
|
|
914
|
+
}
|
|
915
|
+
/** Identity is established by the importer, never inferred from labels or timestamps. */
|
|
916
|
+
function indexRecord(record) {
|
|
917
|
+
const nodes = new Map(record.nodes.map((node) => [node.id, node]));
|
|
918
|
+
const canonical = (id) => nodes.get(id)?.agentId ?? id;
|
|
919
|
+
const actors = record.nodes.filter((node) => node.kind !== "session" || !node.agentId);
|
|
920
|
+
const events = record.events.map(withoutHostPaths).sort((a, b) => ms(a.at) - ms(b.at));
|
|
921
|
+
const byEvent = new Map(events.map((event) => [event.id, event]));
|
|
922
|
+
const recordedOrder = new Map(record.events.map((event, i) => [event.id, i]));
|
|
923
|
+
const byActor = /* @__PURE__ */ new Map();
|
|
924
|
+
const results = /* @__PURE__ */ new Map();
|
|
925
|
+
const calls = /* @__PURE__ */ new Map();
|
|
926
|
+
for (const event of events) {
|
|
927
|
+
const actor = canonical(event.node);
|
|
928
|
+
const lane = byActor.get(actor) ?? [];
|
|
929
|
+
lane.push(event);
|
|
930
|
+
byActor.set(actor, lane);
|
|
931
|
+
for (const call of callsOf(event)) {
|
|
932
|
+
const key = toolKey(event.node, call.id);
|
|
933
|
+
const matching = calls.get(key) ?? [];
|
|
934
|
+
matching.push({
|
|
935
|
+
event,
|
|
936
|
+
call
|
|
937
|
+
});
|
|
938
|
+
calls.set(key, matching);
|
|
939
|
+
}
|
|
940
|
+
if (event.detail.toolCallId !== void 0) {
|
|
941
|
+
const key = toolKey(event.node, event.detail.toolCallId);
|
|
942
|
+
const matching = results.get(key) ?? [];
|
|
943
|
+
matching.push(event);
|
|
944
|
+
results.set(key, matching);
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
const times = events.map((event) => ms(event.at));
|
|
948
|
+
const start = times[0] ?? 0;
|
|
949
|
+
return {
|
|
950
|
+
nodes,
|
|
951
|
+
canonical,
|
|
952
|
+
actors,
|
|
953
|
+
events,
|
|
954
|
+
byEvent,
|
|
955
|
+
recordedOrder,
|
|
956
|
+
byActor,
|
|
957
|
+
calls,
|
|
958
|
+
results,
|
|
959
|
+
start,
|
|
960
|
+
end: events.reduce((latest, event) => Math.max(latest, event.end ? ms(event.end) : ms(event.at)), times.at(-1) ?? start)
|
|
961
|
+
};
|
|
962
|
+
}
|
|
963
|
+
/** Whether any event of this agent recorded token usage or a list price: a status alone measures nothing. */
|
|
964
|
+
function measuredUsage(index, id) {
|
|
965
|
+
return (index.byActor.get(id) ?? []).some((event) => typeof event.detail.costListUsd === "number" || Object.values(event.detail.usage ?? {}).some((value) => typeof value === "number" && value > 0));
|
|
966
|
+
}
|
|
967
|
+
/** An agent's recorded state, qualified when nothing about it was measured or captured. A live agent has no terminal
|
|
968
|
+
* state yet: `live` is its state while the run runs (Runtime's progress document), and it reads as working. */
|
|
969
|
+
function agentState(node, measured, live) {
|
|
970
|
+
if (!node?.status && live) return live === "running" ? "working" : live.replaceAll("-", " ");
|
|
971
|
+
const status = node?.status ?? "no terminal state recorded";
|
|
972
|
+
if (measured) return status;
|
|
973
|
+
const capture = (node?.capture)?.status;
|
|
974
|
+
return `${status} · no usage recorded${capture === "absent" ? "; capture missing" : capture === "lossy" ? "; capture partial" : ""}`;
|
|
975
|
+
}
|
|
976
|
+
function eventMatches(event, index, query, category) {
|
|
977
|
+
return (category === "all" || category === "prompts" && (event.detail.promptKind === "initial" || event.detail.promptKind === "steering") || event.category === category || category === "findings" && (event.kind === "finding-record" || event.detail.recordedClaim !== void 0)) && (!query || JSON.stringify({
|
|
978
|
+
label: event.label,
|
|
979
|
+
node: index.nodes.get(index.canonical(event.node)),
|
|
980
|
+
detail: event.detail
|
|
981
|
+
}).toLowerCase().includes(query));
|
|
982
|
+
}
|
|
983
|
+
function eventSource(event, record, index) {
|
|
984
|
+
const node = index.nodes.get(event.node);
|
|
985
|
+
return {
|
|
986
|
+
...event.source,
|
|
987
|
+
runId: record.runId,
|
|
988
|
+
at: event.at,
|
|
989
|
+
observedNodeId: event.node,
|
|
990
|
+
agentId: node?.kind === "agent" ? node.id : node?.agentId ?? null,
|
|
991
|
+
nativeSessionId: node?.nativeSessionId ?? null,
|
|
992
|
+
joinBasis: node?.joinBasis ?? null,
|
|
993
|
+
joinProof: node?.joinProof ?? null
|
|
994
|
+
};
|
|
995
|
+
}
|
|
996
|
+
function hoverLines(event, index, cutoff) {
|
|
997
|
+
const lines = [`${index.nodes.get(index.canonical(event.node))?.label ?? event.node} · ${utcTime(event.at)}`, `${event.detail.role ?? event.kind}${event.detail.isError ? " · tool error" : event.detail.responseStatus === "error" ? " · request failed" : ""}`];
|
|
998
|
+
if (ms(event.at) > cutoff) return [...lines, "After the selected time. Select this event to advance."];
|
|
999
|
+
const usage = event.detail.usage;
|
|
1000
|
+
if (usage) lines.push(`Input ${usage.input === void 0 ? "unknown" : fmt(usage.input)} · output ${usage.output === void 0 ? "unknown" : fmt(usage.output)} · cache read ${usage.cacheRead === void 0 ? "unknown" : fmt(usage.cacheRead)} · cache write ${usage.cacheWrite === void 0 ? "unknown" : fmt(usage.cacheWrite)}`);
|
|
1001
|
+
const text = textOf(event);
|
|
1002
|
+
if (text) lines.push(text.slice(0, 220) + (text.length > 220 ? "…" : ""));
|
|
1003
|
+
for (const call of callsOf(event).slice(0, 3)) lines.push(`${call.name}: ${preview(call).slice(0, 220)}`);
|
|
1004
|
+
if (!text && !callsOf(event).length && event.detail.role) lines.push(event.detail.contentOmitted ?? event.detail.publicationNote ?? "No text body is present in this record.");
|
|
1005
|
+
return lines;
|
|
1006
|
+
}
|
|
1007
|
+
/** A declared model is configuration; only response metadata shows which model served. */
|
|
1008
|
+
function modelIdentity(node, record) {
|
|
1009
|
+
if (!node) return null;
|
|
1010
|
+
const served = [...new Set(record.nodes.filter((session) => session.agentId === node.id || session.id === node.id).map((session) => session.servedModel).filter((model) => Boolean(model)))];
|
|
1011
|
+
return served.length ? `${served.join(", ")} (served)` : node.model ? `${node.model} (declared; served model unknown)` : null;
|
|
1012
|
+
}
|
|
1013
|
+
function tooltipSections(event, index, cutoff) {
|
|
1014
|
+
const node = index.nodes.get(index.canonical(event.node));
|
|
1015
|
+
const isError = event.detail.isError === true || event.detail.responseStatus === "error";
|
|
1016
|
+
const category = event.detail.role ?? event.kind;
|
|
1017
|
+
const title = event.label || category;
|
|
1018
|
+
const sections = [{
|
|
1019
|
+
kind: "header",
|
|
1020
|
+
value: `${node?.label ?? event.node} · ${utcTime(event.at)}`,
|
|
1021
|
+
tone: "muted"
|
|
1022
|
+
}];
|
|
1023
|
+
if (ms(event.at) > cutoff) {
|
|
1024
|
+
sections.push({
|
|
1025
|
+
kind: "note",
|
|
1026
|
+
value: "After the selected time. Select this event to advance."
|
|
1027
|
+
});
|
|
1028
|
+
return {
|
|
1029
|
+
title,
|
|
1030
|
+
category,
|
|
1031
|
+
tone: isError ? "error" : "default",
|
|
1032
|
+
sections
|
|
1033
|
+
};
|
|
1034
|
+
}
|
|
1035
|
+
if (event.detail.isError) sections.push({
|
|
1036
|
+
kind: "status",
|
|
1037
|
+
value: "Tool error",
|
|
1038
|
+
tone: "error"
|
|
1039
|
+
});
|
|
1040
|
+
if (event.detail.responseStatus === "error") sections.push({
|
|
1041
|
+
kind: "status",
|
|
1042
|
+
value: "Request failed",
|
|
1043
|
+
tone: "error"
|
|
1044
|
+
});
|
|
1045
|
+
const text = textOf(event);
|
|
1046
|
+
if (text) sections.push({
|
|
1047
|
+
kind: "body",
|
|
1048
|
+
value: text.slice(0, 200) + (text.length > 200 ? "…" : "")
|
|
1049
|
+
});
|
|
1050
|
+
const calls = callsOf(event);
|
|
1051
|
+
for (const call of calls.slice(0, 2)) sections.push({
|
|
1052
|
+
kind: "body",
|
|
1053
|
+
label: call.name,
|
|
1054
|
+
value: preview(call).slice(0, 160)
|
|
1055
|
+
});
|
|
1056
|
+
const usage = event.detail.usage;
|
|
1057
|
+
if (usage && !event.detail.usageScope) {
|
|
1058
|
+
const parts = [];
|
|
1059
|
+
if (usage.input !== void 0) parts.push(`↑ ${fmt(usage.input)}`);
|
|
1060
|
+
if (usage.output !== void 0) parts.push(`↓ ${fmt(usage.output)}`);
|
|
1061
|
+
if (usage.cacheRead !== void 0) parts.push(`⟳ ${fmt(usage.cacheRead)}`);
|
|
1062
|
+
if (usage.cacheWrite !== void 0) parts.push(`→ ${fmt(usage.cacheWrite)}`);
|
|
1063
|
+
if (parts.length) sections.push({
|
|
1064
|
+
kind: "usage",
|
|
1065
|
+
value: parts.join(" "),
|
|
1066
|
+
tone: "accent"
|
|
1067
|
+
});
|
|
1068
|
+
}
|
|
1069
|
+
const duration = event.detail.durationMs;
|
|
1070
|
+
if (typeof duration === "number") sections.push({
|
|
1071
|
+
kind: "status",
|
|
1072
|
+
value: `⏱ ${duration < 1e3 ? `${duration}ms` : `${(duration / 1e3).toFixed(1)}s`}`,
|
|
1073
|
+
tone: "muted"
|
|
1074
|
+
});
|
|
1075
|
+
return {
|
|
1076
|
+
title,
|
|
1077
|
+
category,
|
|
1078
|
+
tone: isError ? "error" : "default",
|
|
1079
|
+
sections
|
|
1080
|
+
};
|
|
1081
|
+
}
|
|
1082
|
+
// src/run/record/conversation-rows.ts
|
|
1083
|
+
/**
|
|
1084
|
+
* A key that survives a live record growing. Event ids hash the whole source file, so every append renames every
|
|
1085
|
+
* event of that file. The harness's own record id (`nativeRecordId`, one per session line or part) does not change;
|
|
1086
|
+
* a call and its result can share one, so the kind and source item are part of the key. Without it, the node, source
|
|
1087
|
+
* path and position inside the file are stable too; without either, the event id is all there is.
|
|
1088
|
+
*/
|
|
1089
|
+
function anchorOf(event) {
|
|
1090
|
+
const source = event.source;
|
|
1091
|
+
const native = event.detail.nativeRecordId;
|
|
1092
|
+
if (typeof native === "string" && native) return JSON.stringify([
|
|
1093
|
+
event.node,
|
|
1094
|
+
"native",
|
|
1095
|
+
native,
|
|
1096
|
+
event.kind,
|
|
1097
|
+
source?.item ?? null
|
|
1098
|
+
]);
|
|
1099
|
+
if (source?.path) return JSON.stringify([
|
|
1100
|
+
event.node,
|
|
1101
|
+
source.path,
|
|
1102
|
+
source.line ?? null,
|
|
1103
|
+
source.pointer ?? null,
|
|
1104
|
+
source.item ?? null
|
|
1105
|
+
]);
|
|
1106
|
+
return event.id;
|
|
1107
|
+
}
|
|
1108
|
+
/** Anchors for events in recorded order; a repeated position gets an occurrence suffix so keys stay unique. */
|
|
1109
|
+
function anchorsOf(events) {
|
|
1110
|
+
const seen = /* @__PURE__ */ new Map();
|
|
1111
|
+
const result = /* @__PURE__ */ new Map();
|
|
1112
|
+
for (const event of events) {
|
|
1113
|
+
const base = anchorOf(event);
|
|
1114
|
+
const n = seen.get(base) ?? 0;
|
|
1115
|
+
seen.set(base, n + 1);
|
|
1116
|
+
result.set(event.id, n ? `${base}#${n}` : base);
|
|
1117
|
+
}
|
|
1118
|
+
return result;
|
|
1119
|
+
}
|
|
1120
|
+
/** Arguments the record holds no value for, or an empty object, are not distinguishable as content. */
|
|
1121
|
+
const emptyArgs = (input) => input === void 0 || /^\s*(\{\s*\}|null)?\s*$/.test(input);
|
|
1122
|
+
/** The argument text shown on a collapsed call row, or the honest absence of it. */
|
|
1123
|
+
function argsLabel(input) {
|
|
1124
|
+
if (input === void 0) return "args not captured";
|
|
1125
|
+
if (emptyArgs(input)) return "empty args";
|
|
1126
|
+
return null;
|
|
1127
|
+
}
|
|
1128
|
+
/** The status word for a call. A missing result is pending while its agent can still answer it. */
|
|
1129
|
+
function callStatus(state, live) {
|
|
1130
|
+
switch (state) {
|
|
1131
|
+
case "error": return "Error";
|
|
1132
|
+
case "returned": return "Returned";
|
|
1133
|
+
case "pending": return "Not returned";
|
|
1134
|
+
case "ambiguous": return "Ambiguous";
|
|
1135
|
+
default: return live ? "Pending" : "Result not captured";
|
|
1136
|
+
}
|
|
1137
|
+
}
|
|
1138
|
+
/** The output tokens a turn recorded, or null when its usage was not recorded. */
|
|
1139
|
+
function outputTokens(detail) {
|
|
1140
|
+
const usage = detail.usage;
|
|
1141
|
+
return typeof usage?.output === "number" && Number.isFinite(usage.output) ? usage.output : null;
|
|
1142
|
+
}
|
|
1143
|
+
/** A withheld-thinking turn is quiet glue when it recorded fewer output tokens than THINKING_SHOWN_TOKENS. */
|
|
1144
|
+
function quietThinking(detail) {
|
|
1145
|
+
const output = outputTokens(detail);
|
|
1146
|
+
return output !== null && output < 2e3;
|
|
1147
|
+
}
|
|
1148
|
+
/** Thinking whose text the record lacks, said as what happened. Never a reconstruction of the text. */
|
|
1149
|
+
function thinkingMarkers(detail) {
|
|
1150
|
+
const count = (value) => typeof value === "number" && Number.isFinite(value) && value > 0 ? Math.floor(value) : 0;
|
|
1151
|
+
const omitted = count(detail.reasoningOmitted);
|
|
1152
|
+
const redacted = count(detail.reasoningRedacted);
|
|
1153
|
+
const marks = [];
|
|
1154
|
+
if (omitted) marks.push(omitted === 1 ? "thinking not returned by provider" : `${omitted} thinking blocks not returned by provider`);
|
|
1155
|
+
if (redacted) marks.push(redacted === 1 ? "thinking redacted (encrypted)" : `${redacted} thinking blocks redacted (encrypted)`);
|
|
1156
|
+
return marks;
|
|
1157
|
+
}
|
|
1158
|
+
const WAITING = /await|wait|poll|sleep|observe|status/i;
|
|
1159
|
+
/**
|
|
1160
|
+
* A result that only says the call is still running: a JSON object with `pending: true` (Runtime's coordination tools
|
|
1161
|
+
* answer a long call this way, with its elapsed time, before its real answer). It carries no answer, so a call whose
|
|
1162
|
+
* every result is one still counts as unanswered for collapsing.
|
|
1163
|
+
*/
|
|
1164
|
+
function pendingReceipt(text) {
|
|
1165
|
+
if (typeof text !== "string" || !text.trimStart().startsWith("{")) return false;
|
|
1166
|
+
try {
|
|
1167
|
+
const value = JSON.parse(text);
|
|
1168
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value) && value.pending === true;
|
|
1169
|
+
} catch {
|
|
1170
|
+
return false;
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
1173
|
+
/** Whether any of a call's results answers it: a result that is not a pending receipt. */
|
|
1174
|
+
const answeredBy = (results) => (results ?? []).some((result) => !pendingReceipt(result.detail.publicText));
|
|
1175
|
+
/**
|
|
1176
|
+
* The one call of an event that is a poll: an assistant turn whose only content is a single tool call, with no text,
|
|
1177
|
+
* thinking, note or clipped body, and with no answer anywhere in the record: no result, or only pending receipts
|
|
1178
|
+
* (pendingReceipt). A call with any other result is never a poll, so collapsing can hide no returned answer. Its arguments are empty or absent, or the
|
|
1179
|
+
* tool is a waiting tool (await, wait, poll, sleep, observe, status).
|
|
1180
|
+
*/
|
|
1181
|
+
function pollCall(event, calls, answered) {
|
|
1182
|
+
const detail = event.detail;
|
|
1183
|
+
if (detail.role !== "assistant" || event.category === "lifecycle" || detail.lifecycle !== void 0) return null;
|
|
1184
|
+
if (detail.publicText || detail.recordedClaim || detail.reasoning || detail.responseStatus === "error" || detail.contentOmitted || detail.publicationNote || detail.clip || thinkingMarkers(detail).length) return null;
|
|
1185
|
+
if (calls.length !== 1) return null;
|
|
1186
|
+
const call = calls[0];
|
|
1187
|
+
if (call.clip || call.publicationNote || answered(call.id)) return null;
|
|
1188
|
+
if (!emptyArgs(call.input) && !WAITING.test(call.name)) return null;
|
|
1189
|
+
return call;
|
|
1190
|
+
}
|
|
1191
|
+
/**
|
|
1192
|
+
* The calls of a hollow turn: an assistant tool turn with no text, thinking, or notes whose every call has neither
|
|
1193
|
+
* captured arguments nor a captured answer. Nothing in it can be shown but the fact that it ran, so a run of them
|
|
1194
|
+
* collapses to one row. A call with any captured side keeps its own row.
|
|
1195
|
+
*/
|
|
1196
|
+
function hollowCalls(event, calls, captured) {
|
|
1197
|
+
const detail = event.detail;
|
|
1198
|
+
if (detail.role !== "assistant" || event.category === "lifecycle" || detail.lifecycle !== void 0) return null;
|
|
1199
|
+
if (detail.publicText || detail.recordedClaim || detail.reasoning || detail.responseStatus === "error" || detail.contentOmitted || detail.publicationNote || detail.clip || thinkingMarkers(detail).length) return null;
|
|
1200
|
+
if (!calls.length) return null;
|
|
1201
|
+
if (calls.some((call) => call.input !== void 0 || call.clip || call.publicationNote || captured(event, call.id))) return null;
|
|
1202
|
+
return calls;
|
|
1203
|
+
}
|
|
1204
|
+
/** Fold consecutive rows only; a user message, error or substantive work always breaks the group. */
|
|
1205
|
+
function foldRows(rows, identify, same, build, min = 2) {
|
|
1206
|
+
const out = [];
|
|
1207
|
+
let run = null;
|
|
1208
|
+
const close = () => {
|
|
1209
|
+
if (run) out.push(...run.rows.length >= min ? [build(run.rows.map((r) => r.event), run.value)] : run.rows);
|
|
1210
|
+
run = null;
|
|
1211
|
+
};
|
|
1212
|
+
for (const row of rows) {
|
|
1213
|
+
const value = row.kind === "event" ? identify(row.event) : null;
|
|
1214
|
+
if (value === null) {
|
|
1215
|
+
close();
|
|
1216
|
+
out.push(row);
|
|
1217
|
+
continue;
|
|
1218
|
+
}
|
|
1219
|
+
if (run && !same(run.value, value)) close();
|
|
1220
|
+
run ??= {
|
|
1221
|
+
rows: [],
|
|
1222
|
+
value
|
|
1223
|
+
};
|
|
1224
|
+
run.rows.push(row);
|
|
1225
|
+
}
|
|
1226
|
+
close();
|
|
1227
|
+
return out;
|
|
1228
|
+
}
|
|
1229
|
+
function collapseHollowTurns(rows, { callsOf, captured, keyOf, min = 2 }) {
|
|
1230
|
+
return foldRows(rows, (event) => hollowCalls(event, callsOf(event), captured), () => true, (events) => {
|
|
1231
|
+
const counts = /* @__PURE__ */ new Map();
|
|
1232
|
+
for (const event of events) for (const call of callsOf(event)) counts.set(call.name, (counts.get(call.name) ?? 0) + 1);
|
|
1233
|
+
const times = events.map((event) => Date.parse(event.at)).filter(Number.isFinite);
|
|
1234
|
+
return {
|
|
1235
|
+
kind: "hollow",
|
|
1236
|
+
key: `hollow:${keyOf(events[0])}`,
|
|
1237
|
+
events,
|
|
1238
|
+
calls: [...counts].map(([name, count]) => ({
|
|
1239
|
+
name,
|
|
1240
|
+
count
|
|
1241
|
+
})),
|
|
1242
|
+
spanMs: times.length > 1 ? Math.max(...times) - Math.min(...times) : 0
|
|
1243
|
+
};
|
|
1244
|
+
}, min);
|
|
1245
|
+
}
|
|
1246
|
+
function hollowSummary(calls) {
|
|
1247
|
+
const total = calls.reduce((sum, entry) => sum + entry.count, 0);
|
|
1248
|
+
return `${total} tool call${total === 1 ? "" : "s"} · ${calls.map((entry) => entry.count > 1 ? `${entry.name} ×${entry.count}` : entry.name).join(" · ")}`;
|
|
1249
|
+
}
|
|
1250
|
+
function collapsePolls(items, { callsOf, answered, keyOf, min = 2 }) {
|
|
1251
|
+
return foldRows(items.map((event) => ({
|
|
1252
|
+
kind: "event",
|
|
1253
|
+
key: keyOf(event),
|
|
1254
|
+
event
|
|
1255
|
+
})), (event) => pollCall(event, callsOf(event), (id) => answered(event, id)), (a, b) => a.name === b.name && (emptyArgs(a.input) ? "" : a.input.trim()) === (emptyArgs(b.input) ? "" : b.input.trim()), (events, call) => ({
|
|
1256
|
+
kind: "polls",
|
|
1257
|
+
key: `polls:${keyOf(events[0])}`,
|
|
1258
|
+
name: call.name,
|
|
1259
|
+
input: call.input,
|
|
1260
|
+
events
|
|
1261
|
+
}), min);
|
|
1262
|
+
}
|
|
1263
|
+
/** Exact repeated idle replies fold without hiding changed statements, tools, errors, prompts or source evidence. */
|
|
1264
|
+
function collapseIdleTurns(rows, callsOf) {
|
|
1265
|
+
return foldRows(rows, (event) => {
|
|
1266
|
+
const d = event.detail;
|
|
1267
|
+
if (d.role !== "assistant" || event.category === "lifecycle" || d.lifecycle || d.reasoning || d.recordedClaim || d.responseStatus === "error" || d.isError || d.contentOmitted || d.publicationNote || d.clip || thinkingMarkers(d).length || callsOf(event).length) return null;
|
|
1268
|
+
const text = typeof d.publicText === "string" ? d.publicText.trim() : "";
|
|
1269
|
+
return text.length <= 280 && /\b(waiting|ending the turn|end turn|no new updates|nothing new)\b/i.test(text) ? text : null;
|
|
1270
|
+
}, (a, b) => a === b, (events) => ({
|
|
1271
|
+
kind: "idle",
|
|
1272
|
+
key: `idle:${anchorOf(events[0])}`,
|
|
1273
|
+
events
|
|
1274
|
+
}));
|
|
1275
|
+
}
|
|
1276
|
+
/** `6m 12s`, `45s`, `1h 3m`. */
|
|
1277
|
+
function waited(milliseconds) {
|
|
1278
|
+
const seconds = Math.max(0, Math.round(milliseconds / 1e3));
|
|
1279
|
+
if (seconds < 60) return `${seconds}s`;
|
|
1280
|
+
const minutes = Math.floor(seconds / 60);
|
|
1281
|
+
if (minutes < 60) return `${minutes}m ${seconds % 60}s`;
|
|
1282
|
+
return `${Math.floor(minutes / 60)}h ${minutes % 60}m`;
|
|
1283
|
+
}
|
|
1284
|
+
/**
|
|
1285
|
+
* The collapsed row's facts. Tokens count only per-response usage recorded on the polls themselves; with none
|
|
1286
|
+
* recorded the count is unknown, never zero.
|
|
1287
|
+
*/
|
|
1288
|
+
function pollSummary(events) {
|
|
1289
|
+
const times = events.map((event) => Date.parse(event.at)).filter(Number.isFinite);
|
|
1290
|
+
const span = times.length ? Math.max(...times) - Math.min(...times) : 0;
|
|
1291
|
+
let tokens = 0;
|
|
1292
|
+
let measured = 0;
|
|
1293
|
+
for (const event of events) {
|
|
1294
|
+
const usage = event.detail.usage;
|
|
1295
|
+
if (!usage || event.detail.usageScope !== void 0) continue;
|
|
1296
|
+
const parts = [
|
|
1297
|
+
usage.input,
|
|
1298
|
+
usage.output,
|
|
1299
|
+
usage.cacheRead,
|
|
1300
|
+
usage.cacheWrite
|
|
1301
|
+
].filter((value) => typeof value === "number");
|
|
1302
|
+
if (!parts.length) continue;
|
|
1303
|
+
measured++;
|
|
1304
|
+
tokens += parts.reduce((sum, value) => sum + value, 0);
|
|
1305
|
+
}
|
|
1306
|
+
const count = new Intl.NumberFormat("en-US", {
|
|
1307
|
+
notation: "compact",
|
|
1308
|
+
maximumFractionDigits: 1
|
|
1309
|
+
});
|
|
1310
|
+
const tokenText = !measured ? "tokens unknown" : measured < events.length ? `~${count.format(tokens)} tokens (${measured} of ${events.length} measured)` : `~${count.format(tokens)} tokens`;
|
|
1311
|
+
return {
|
|
1312
|
+
polls: events.length,
|
|
1313
|
+
spanMs: span,
|
|
1314
|
+
tokens: measured ? tokens : null,
|
|
1315
|
+
measured,
|
|
1316
|
+
label: `waited ${waited(span)} · ${events.length} polls · ${tokenText}`
|
|
1317
|
+
};
|
|
1318
|
+
}
|
|
1319
|
+
// src/run/record/prompt-text.ts
|
|
1320
|
+
/**
|
|
1321
|
+
* A prompt as its reader wrote it (REVIEW-8 #5: the first message rendered raw JSON with literal \n\n). The CLI bridge
|
|
1322
|
+
* frames every user turn as "[system] <session resources and permission posture>\n\n[user] <payload>"; the frame is the
|
|
1323
|
+
* same on every turn and stays in the event's source (Details). A payload that is a JSON assignment (program, idea,
|
|
1324
|
+
* objective, instruction, ...) reads as its objective and instruction; any other payload is shown as written.
|
|
1325
|
+
*/
|
|
1326
|
+
function promptText(text) {
|
|
1327
|
+
const at = text.indexOf("[user] ");
|
|
1328
|
+
const payload = text.startsWith("[system] ") && at >= 0 ? text.slice(at + 7).trim() : text.trim();
|
|
1329
|
+
if (!payload.startsWith("{")) return payload;
|
|
1330
|
+
try {
|
|
1331
|
+
const value = JSON.parse(payload);
|
|
1332
|
+
const parts = ["objective", "instruction"].map((key) => value[key]).filter((part) => typeof part === "string" && part.trim().length > 0);
|
|
1333
|
+
return parts.length ? parts.join("\n\n") : payload;
|
|
1334
|
+
} catch {
|
|
1335
|
+
return payload;
|
|
1336
|
+
}
|
|
1337
|
+
}
|
|
1338
|
+
// src/run/record/ConversationRowView.tsx
|
|
1339
|
+
/** Shared chat bubbles, speaker headers and timeline steps; source rows retain virtualization and provenance. */
|
|
1340
|
+
/** A run-lifecycle event (start, stop, restart), drawn as a thin marker row. */
|
|
1341
|
+
const isLifecycle = (event) => event.category === "lifecycle" || event.detail.lifecycle !== void 0;
|
|
1342
|
+
const tokenLabel = (value) => new Intl.NumberFormat("en-US", {
|
|
1343
|
+
notation: "compact",
|
|
1344
|
+
maximumFractionDigits: 1
|
|
1345
|
+
}).format(value);
|
|
1346
|
+
const spanLabel = (value) => {
|
|
1347
|
+
const seconds = Math.round(value / 1e3);
|
|
1348
|
+
if (seconds < 60) return `${seconds} s`;
|
|
1349
|
+
const minutes = Math.floor(seconds / 60);
|
|
1350
|
+
return minutes < 60 ? `${minutes} m ${seconds % 60} s` : `${Math.floor(minutes / 60)} h ${minutes % 60} m`;
|
|
1351
|
+
};
|
|
1352
|
+
function ToolIcon({ name }) {
|
|
1353
|
+
const lower = name.toLowerCase();
|
|
1354
|
+
const path = /think|reason/.test(lower) ? "M8 2.5a4 4 0 0 0-2.5 7.1V11h5V9.6A4 4 0 0 0 8 2.5M6 13.5h4" : /search|grep|find/.test(lower) ? "M10 10l3.5 3.5M11 7a4 4 0 1 0-8 0 4 4 0 0 0 8 0" : /bash|shell|exec|run|command/.test(lower) ? "M2 3h12v10H2zM4 6l2 2-2 2M8 10h4" : /await|wait|sleep|poll/.test(lower) ? "M8 2.5a5.5 5.5 0 1 0 0 11 5.5 5.5 0 0 0 0-11M8 5v3l2 1.5" : "M10 8a2 2 0 1 0-4 0 2 2 0 0 0 4 0";
|
|
1355
|
+
return /* @__PURE__ */ jsx("svg", {
|
|
1356
|
+
viewBox: "0 0 16 16",
|
|
1357
|
+
width: "14",
|
|
1358
|
+
height: "14",
|
|
1359
|
+
"aria-hidden": "true",
|
|
1360
|
+
children: /* @__PURE__ */ jsx("path", {
|
|
1361
|
+
d: path,
|
|
1362
|
+
stroke: "currentColor",
|
|
1363
|
+
strokeWidth: "1.4",
|
|
1364
|
+
fill: "none",
|
|
1365
|
+
strokeLinecap: "round",
|
|
1366
|
+
strokeLinejoin: "round"
|
|
1367
|
+
})
|
|
1368
|
+
});
|
|
1369
|
+
}
|
|
1370
|
+
/** The tools the kit names itself ("Read", "Shell"); any other tool reads by its own name. */
|
|
1371
|
+
const KIT_TOOLS = /* @__PURE__ */ new Set([
|
|
1372
|
+
"bash",
|
|
1373
|
+
"shell",
|
|
1374
|
+
"command",
|
|
1375
|
+
"execute",
|
|
1376
|
+
"write",
|
|
1377
|
+
"write_file",
|
|
1378
|
+
"create_file",
|
|
1379
|
+
"edit",
|
|
1380
|
+
"patch",
|
|
1381
|
+
"read",
|
|
1382
|
+
"read_file",
|
|
1383
|
+
"cat",
|
|
1384
|
+
"grep",
|
|
1385
|
+
"search",
|
|
1386
|
+
"rg",
|
|
1387
|
+
"glob",
|
|
1388
|
+
"find",
|
|
1389
|
+
"ls",
|
|
1390
|
+
"web_search",
|
|
1391
|
+
"web_fetch",
|
|
1392
|
+
"fetch",
|
|
1393
|
+
"task",
|
|
1394
|
+
"agent",
|
|
1395
|
+
"spawn"
|
|
1396
|
+
]);
|
|
1397
|
+
const TYPES = {
|
|
1398
|
+
bash: "bash",
|
|
1399
|
+
shell: "bash",
|
|
1400
|
+
command: "bash",
|
|
1401
|
+
execute: "bash",
|
|
1402
|
+
read: "read",
|
|
1403
|
+
read_file: "read",
|
|
1404
|
+
cat: "read",
|
|
1405
|
+
write: "write",
|
|
1406
|
+
write_file: "write",
|
|
1407
|
+
create_file: "write",
|
|
1408
|
+
edit: "edit",
|
|
1409
|
+
patch: "edit",
|
|
1410
|
+
grep: "grep",
|
|
1411
|
+
search: "grep",
|
|
1412
|
+
rg: "grep",
|
|
1413
|
+
glob: "glob",
|
|
1414
|
+
find: "glob",
|
|
1415
|
+
ls: "list"
|
|
1416
|
+
};
|
|
1417
|
+
/** `mcp__server__tool` reads as `tool` and `agent-runtime-coordination_send` as `coordination_send`. */
|
|
1418
|
+
const toolName = (name) => (name.startsWith("mcp__") ? name.slice(name.lastIndexOf("__") + 2) || name : name).replace(/^agent-runtime-/, "");
|
|
1419
|
+
const inputOf = (raw) => {
|
|
1420
|
+
if (raw === void 0) return void 0;
|
|
1421
|
+
try {
|
|
1422
|
+
const value = JSON.parse(raw);
|
|
1423
|
+
return value && typeof value === "object" ? value : raw;
|
|
1424
|
+
} catch {
|
|
1425
|
+
return raw;
|
|
1426
|
+
}
|
|
1427
|
+
};
|
|
1428
|
+
/** A failure reads as its message: `{"error":"File not found: x"}` is `File not found: x`. */
|
|
1429
|
+
const failureText = (text) => {
|
|
1430
|
+
let body = text;
|
|
1431
|
+
try {
|
|
1432
|
+
const value = JSON.parse(text);
|
|
1433
|
+
const said = typeof value?.error === "string" ? value.error : typeof value?.message === "string" ? value.message : void 0;
|
|
1434
|
+
if (said) body = said;
|
|
1435
|
+
} catch {}
|
|
1436
|
+
return compact(body.split("\n").find((line) => line.trim()) ?? "").slice(0, 200);
|
|
1437
|
+
};
|
|
1438
|
+
/** One tool call as the kit's row: its verb and subject, collapsed; the input and output when opened; failures inline. */
|
|
1439
|
+
function toolItems(id, call, result, state, durationMs, live, opener) {
|
|
1440
|
+
const name = toolName(call.name);
|
|
1441
|
+
const output = result ? textOf(result) || result.detail.contentOmitted || "" : void 0;
|
|
1442
|
+
const status = state === "error" ? "error" : state === "pending" || state === "missing" && live ? "running" : "completed";
|
|
1443
|
+
const note = state === "ambiguous" || state === "missing" && !live ? callStatus(state, live) : void 0;
|
|
1444
|
+
const part = {
|
|
1445
|
+
type: "tool",
|
|
1446
|
+
id,
|
|
1447
|
+
tool: name,
|
|
1448
|
+
state: {
|
|
1449
|
+
status,
|
|
1450
|
+
input: inputOf(call.input),
|
|
1451
|
+
...output !== void 0 ? { output } : {},
|
|
1452
|
+
...status === "error" ? { error: failureText(output ?? "") || "The call failed." } : {},
|
|
1453
|
+
...durationMs !== null && durationMs >= 0 ? { time: {
|
|
1454
|
+
start: 0,
|
|
1455
|
+
end: durationMs
|
|
1456
|
+
} } : {},
|
|
1457
|
+
...note ? { metadata: { note } } : {}
|
|
1458
|
+
}
|
|
1459
|
+
};
|
|
1460
|
+
const known = KIT_TOOLS.has(name.toLowerCase());
|
|
1461
|
+
const meta = getToolDisplayMetadata(part);
|
|
1462
|
+
const detail = argsLabel(call.input) ?? (meta.description || (known ? void 0 : preview(call)));
|
|
1463
|
+
const item = {
|
|
1464
|
+
id,
|
|
1465
|
+
kind: "tool",
|
|
1466
|
+
part,
|
|
1467
|
+
call: {
|
|
1468
|
+
id,
|
|
1469
|
+
type: TYPES[name.toLowerCase()] ?? "unknown",
|
|
1470
|
+
label: known ? meta.title : name.replaceAll("_", " "),
|
|
1471
|
+
status: status === "error" ? "error" : status === "running" ? "running" : "success",
|
|
1472
|
+
...detail ? { detail } : {},
|
|
1473
|
+
...durationMs !== null && durationMs >= 0 ? { duration: durationMs } : {}
|
|
1474
|
+
}
|
|
1475
|
+
};
|
|
1476
|
+
const clip = clipOf(result?.detail.clip);
|
|
1477
|
+
return result && clip ? [item, {
|
|
1478
|
+
id: `${id}:clip`,
|
|
1479
|
+
kind: "custom",
|
|
1480
|
+
content: /* @__PURE__ */ jsx(ClipNote, {
|
|
1481
|
+
clip,
|
|
1482
|
+
open: opener(result)
|
|
1483
|
+
})
|
|
1484
|
+
}] : [item];
|
|
1485
|
+
}
|
|
1486
|
+
const toolNote = (part) => {
|
|
1487
|
+
const note = part.state.metadata?.note;
|
|
1488
|
+
return note ? /* @__PURE__ */ jsx("span", {
|
|
1489
|
+
className: "tool-note",
|
|
1490
|
+
children: note
|
|
1491
|
+
}) : null;
|
|
1492
|
+
};
|
|
1493
|
+
const clipOf = (value) => value && typeof value === "object" && "bytes" in value ? value : void 0;
|
|
1494
|
+
/** A clipped body names what was left out and opens its full source text. */
|
|
1495
|
+
function ClipNote({ clip, open }) {
|
|
1496
|
+
if (!clip) return null;
|
|
1497
|
+
return /* @__PURE__ */ jsxs("p", {
|
|
1498
|
+
className: "clip-note",
|
|
1499
|
+
children: [/* @__PURE__ */ jsxs("span", { children: [new Intl.NumberFormat("en-US").format(clip.bytes), " bytes, middle omitted."] }), clip.sha256 && open ? /* @__PURE__ */ jsx("button", {
|
|
1500
|
+
type: "button",
|
|
1501
|
+
className: "link-button",
|
|
1502
|
+
"data-load-full": clip.sha256,
|
|
1503
|
+
onClick: () => open(clip.sha256),
|
|
1504
|
+
children: "Full output"
|
|
1505
|
+
}) : null]
|
|
1506
|
+
});
|
|
1507
|
+
}
|
|
1508
|
+
/** Repeated calls answered only by pending receipts: one row, the calls listed when opened. */
|
|
1509
|
+
function pollItem(row) {
|
|
1510
|
+
const summary = pollSummary(row.events);
|
|
1511
|
+
const args = argsLabel(row.input) ?? compact(row.input ?? "");
|
|
1512
|
+
return {
|
|
1513
|
+
id: row.key,
|
|
1514
|
+
kind: "custom",
|
|
1515
|
+
content: /* @__PURE__ */ jsx(RunRowShell, {
|
|
1516
|
+
icon: /* @__PURE__ */ jsx(ToolIcon, { name: row.name }),
|
|
1517
|
+
title: toolName(row.name),
|
|
1518
|
+
description: [summary.label, args].filter(Boolean).join(" · "),
|
|
1519
|
+
descriptionMono: true,
|
|
1520
|
+
children: /* @__PURE__ */ jsx("ol", {
|
|
1521
|
+
className: "poll-calls",
|
|
1522
|
+
children: row.events.map((event) => /* @__PURE__ */ jsxs("li", {
|
|
1523
|
+
"data-message": event.id,
|
|
1524
|
+
children: [/* @__PURE__ */ jsx("time", {
|
|
1525
|
+
dateTime: event.at,
|
|
1526
|
+
children: utcTime(event.at)
|
|
1527
|
+
}), /* @__PURE__ */ jsx("span", {
|
|
1528
|
+
className: "mono",
|
|
1529
|
+
children: argsLabel(callsOf(event)[0]?.input) ?? compact(callsOf(event)[0]?.input ?? "")
|
|
1530
|
+
})]
|
|
1531
|
+
}, event.id))
|
|
1532
|
+
})
|
|
1533
|
+
})
|
|
1534
|
+
};
|
|
1535
|
+
}
|
|
1536
|
+
/** Consecutive tool turns whose arguments and results were not captured: one row naming the calls. */
|
|
1537
|
+
const hollowItem = (row) => ({
|
|
1538
|
+
id: row.key,
|
|
1539
|
+
kind: "custom",
|
|
1540
|
+
content: /* @__PURE__ */ jsx(RunRowShell, {
|
|
1541
|
+
icon: /* @__PURE__ */ jsx(ToolIcon, { name: row.calls[0]?.name ?? "tool" }),
|
|
1542
|
+
title: hollowSummary(row.calls),
|
|
1543
|
+
description: "args not captured",
|
|
1544
|
+
durationMs: row.spanMs || void 0
|
|
1545
|
+
})
|
|
1546
|
+
});
|
|
1547
|
+
/** What one record row shows, as the kit's timeline items. */
|
|
1548
|
+
function rowItems(row, context) {
|
|
1549
|
+
if (row.kind === "polls") return [pollItem(row)];
|
|
1550
|
+
if (row.kind === "hollow") return [hollowItem(row)];
|
|
1551
|
+
if (row.kind === "idle") return [{
|
|
1552
|
+
id: `${row.key}:${context.selected ?? ""}`,
|
|
1553
|
+
kind: "fold",
|
|
1554
|
+
summary: `${row.events.length} idle turns · ${textOf(row.events[0])}`,
|
|
1555
|
+
defaultOpen: row.events.some((event) => event.id === context.selected),
|
|
1556
|
+
items: row.events.map((event) => ({
|
|
1557
|
+
id: event.id,
|
|
1558
|
+
kind: "custom",
|
|
1559
|
+
content: /* @__PURE__ */ jsxs("div", {
|
|
1560
|
+
className: "ar-idle-event",
|
|
1561
|
+
"data-message": event.id,
|
|
1562
|
+
children: [/* @__PURE__ */ jsx("time", {
|
|
1563
|
+
dateTime: event.at,
|
|
1564
|
+
children: utcTime(event.at)
|
|
1565
|
+
}), /* @__PURE__ */ jsx(AgentTimelineStep, { item: {
|
|
1566
|
+
id: event.id,
|
|
1567
|
+
kind: "message",
|
|
1568
|
+
role: "assistant",
|
|
1569
|
+
content: textOf(event)
|
|
1570
|
+
} })]
|
|
1571
|
+
})
|
|
1572
|
+
}))
|
|
1573
|
+
}];
|
|
1574
|
+
const { index, cutoff, anchor, thinkingSpan, opened, paired, live } = context;
|
|
1575
|
+
const event = row.event;
|
|
1576
|
+
if (isLifecycle(event)) {
|
|
1577
|
+
const text = textOf(event);
|
|
1578
|
+
return [{
|
|
1579
|
+
id: event.id,
|
|
1580
|
+
kind: "custom",
|
|
1581
|
+
content: /* @__PURE__ */ jsxs("p", {
|
|
1582
|
+
className: `lifecycle-note${event.detail.isError ? " lifecycle-error" : ""}`,
|
|
1583
|
+
children: [/* @__PURE__ */ jsx("b", { children: event.label }), text && /* @__PURE__ */ jsx("span", { children: compact(text).slice(0, 280) })]
|
|
1584
|
+
})
|
|
1585
|
+
}];
|
|
1586
|
+
}
|
|
1587
|
+
const message = textOf(event);
|
|
1588
|
+
const calls = callsOf(event);
|
|
1589
|
+
const reasoning = typeof event.detail.reasoning === "string" ? event.detail.reasoning : "";
|
|
1590
|
+
const withheld = thinkingMarkers(event.detail);
|
|
1591
|
+
const failed = event.detail.responseStatus === "error";
|
|
1592
|
+
const finding = event.detail.recordedClaim !== void 0;
|
|
1593
|
+
const standalone = event.detail.toolCallId !== void 0 || event.detail.role === "toolResult";
|
|
1594
|
+
if (event.detail.role === "user" && message && !standalone) {
|
|
1595
|
+
const body = promptText(message);
|
|
1596
|
+
const short = body.length > 280 && !opened.has(`prompt:${anchor(event)}`);
|
|
1597
|
+
return [{
|
|
1598
|
+
id: event.id,
|
|
1599
|
+
kind: "message",
|
|
1600
|
+
role: "user",
|
|
1601
|
+
content: short ? `${body.slice(0, 280).trimEnd()}…` : body,
|
|
1602
|
+
timestamp: new Date(event.at)
|
|
1603
|
+
}];
|
|
1604
|
+
}
|
|
1605
|
+
const items = [];
|
|
1606
|
+
if (reasoning) items.push({
|
|
1607
|
+
id: `${event.id}:reasoning`,
|
|
1608
|
+
kind: "custom",
|
|
1609
|
+
content: /* @__PURE__ */ jsx(InlineThinkingItem, {
|
|
1610
|
+
part: {
|
|
1611
|
+
type: "reasoning",
|
|
1612
|
+
text: reasoning
|
|
1613
|
+
},
|
|
1614
|
+
autoCollapse: false
|
|
1615
|
+
})
|
|
1616
|
+
});
|
|
1617
|
+
if (withheld.length) {
|
|
1618
|
+
const output = outputTokens(event.detail);
|
|
1619
|
+
const span = thinkingSpan.get(event.id);
|
|
1620
|
+
const label = output !== null ? `${span !== void 0 && span >= 1e3 ? `for ${spanLabel(span)} · ` : ""}${tokenLabel(output)} output tokens · ${withheld.join(" · ")}` : withheld.join(" · ");
|
|
1621
|
+
items.push({
|
|
1622
|
+
id: `${event.id}:thinking`,
|
|
1623
|
+
kind: "custom",
|
|
1624
|
+
content: /* @__PURE__ */ jsx("span", {
|
|
1625
|
+
"data-thinking-withheld": true,
|
|
1626
|
+
children: /* @__PURE__ */ jsx(RunRowShell, {
|
|
1627
|
+
icon: /* @__PURE__ */ jsx(ToolIcon, { name: "think" }),
|
|
1628
|
+
title: "Thought",
|
|
1629
|
+
description: label
|
|
1630
|
+
})
|
|
1631
|
+
})
|
|
1632
|
+
});
|
|
1633
|
+
}
|
|
1634
|
+
if (failed) items.push({
|
|
1635
|
+
id: event.id,
|
|
1636
|
+
kind: "status",
|
|
1637
|
+
tone: "error",
|
|
1638
|
+
label: "The request failed",
|
|
1639
|
+
detail: message ? compact(message).slice(0, 400) : "No assistant response is present in this record."
|
|
1640
|
+
});
|
|
1641
|
+
else if (finding) items.push({
|
|
1642
|
+
id: event.id,
|
|
1643
|
+
kind: "status",
|
|
1644
|
+
tone: "info",
|
|
1645
|
+
label: "Recorded finding",
|
|
1646
|
+
detail: compact(message).slice(0, 600)
|
|
1647
|
+
});
|
|
1648
|
+
else if (standalone) {
|
|
1649
|
+
const unpaired = event.detail.toolCallId !== void 0 && (index.calls.get(toolKey(event.node, event.detail.toolCallId))?.length ?? 0) > 1;
|
|
1650
|
+
items.push(...toolItems(event.id, {
|
|
1651
|
+
id: event.id,
|
|
1652
|
+
name: event.label || "Tool result"
|
|
1653
|
+
}, event, unpaired ? "ambiguous" : event.detail.isError ? "error" : "returned", null, live, context.opener));
|
|
1654
|
+
} else if (message) items.push({
|
|
1655
|
+
id: event.id,
|
|
1656
|
+
kind: "message",
|
|
1657
|
+
role: "assistant",
|
|
1658
|
+
content: message
|
|
1659
|
+
});
|
|
1660
|
+
else if (!calls.length && !reasoning && !withheld.length) items.push({
|
|
1661
|
+
id: event.id,
|
|
1662
|
+
kind: "status",
|
|
1663
|
+
label: event.detail.contentOmitted ?? "No text body is present in this record."
|
|
1664
|
+
});
|
|
1665
|
+
const at = anchor(event);
|
|
1666
|
+
for (const call of calls) {
|
|
1667
|
+
const all = paired(event, call);
|
|
1668
|
+
const last = all.filter((result) => ms(result.at) <= cutoff).at(-1);
|
|
1669
|
+
const state = (index.calls.get(toolKey(event.node, call.id))?.length ?? 0) > 1 ? "ambiguous" : last ? last.detail.isError ? "error" : "returned" : all.some((result) => ms(result.at) > cutoff) ? "pending" : "missing";
|
|
1670
|
+
const duration = (typeof last?.detail.durationMs === "number" ? last.detail.durationMs : null) ?? (last && last.detail.atBasis !== "carried" ? ms(last.at) - ms(event.at) : null);
|
|
1671
|
+
items.push(...toolItems(`tool:${at}:${call.id}`, call, last, state, duration, live, context.opener));
|
|
1672
|
+
}
|
|
1673
|
+
return items;
|
|
1674
|
+
}
|
|
1675
|
+
/** The kind of a row's last item, which sets the gap above the next row. */
|
|
1676
|
+
const lastKind = (items) => items.length ? agentTimelineStepKind(items[items.length - 1]) : void 0;
|
|
1677
|
+
function ConversationRowView({ row, items, previous, turn, context }) {
|
|
1678
|
+
const { selected, opened, anchor, setOpen, flagMarks, opener, flags } = context;
|
|
1679
|
+
const events = row.kind === "event" ? [row.event] : row.events;
|
|
1680
|
+
const event = events[0];
|
|
1681
|
+
const steps = /* @__PURE__ */ jsxs(Fragment, { children: [turn && /* @__PURE__ */ jsxs("div", {
|
|
1682
|
+
className: "ar-turn",
|
|
1683
|
+
"data-turn-start": true,
|
|
1684
|
+
children: [/* @__PURE__ */ jsx(SpeakerHeader, { speaker: {
|
|
1685
|
+
id: event.node,
|
|
1686
|
+
name: context.index.nodes.get(event.node)?.label ?? "Agent"
|
|
1687
|
+
} }), /* @__PURE__ */ jsx("time", {
|
|
1688
|
+
dateTime: event.at,
|
|
1689
|
+
title: event.at,
|
|
1690
|
+
children: utcTime(event.at)
|
|
1691
|
+
})]
|
|
1692
|
+
}), items.map((item, i) => /* @__PURE__ */ jsx(AgentTimelineStep, {
|
|
1693
|
+
item,
|
|
1694
|
+
previous: i ? agentTimelineStepKind(items[i - 1]) : previous,
|
|
1695
|
+
renderToolActions: toolNote
|
|
1696
|
+
}, item.id))] });
|
|
1697
|
+
const rowProps = {
|
|
1698
|
+
className: [
|
|
1699
|
+
"chat-row",
|
|
1700
|
+
events.some((e) => e.id === selected) && "selected",
|
|
1701
|
+
row.kind === "event" && flags?.has(event.id) && "flagged"
|
|
1702
|
+
].filter(Boolean).join(" "),
|
|
1703
|
+
"data-entry": event.id,
|
|
1704
|
+
title: utcTime(event.at),
|
|
1705
|
+
...row.kind === "event" ? {
|
|
1706
|
+
"data-message": event.id,
|
|
1707
|
+
"data-message-node": event.node
|
|
1708
|
+
} : { [`data-${row.kind === "polls" ? "poll" : row.kind}-group`]: row.events.length }
|
|
1709
|
+
};
|
|
1710
|
+
if (row.kind !== "event" || isLifecycle(event)) return /* @__PURE__ */ jsxs("div", {
|
|
1711
|
+
...rowProps,
|
|
1712
|
+
children: [steps, row.kind === "event" && flagMarks(event)]
|
|
1713
|
+
});
|
|
1714
|
+
const message = textOf(event);
|
|
1715
|
+
const standalone = event.detail.toolCallId !== void 0 || event.detail.role === "toolResult";
|
|
1716
|
+
const clip = clipOf(event.detail.clip);
|
|
1717
|
+
if (event.detail.role === "user" && message && !standalone) {
|
|
1718
|
+
const key = `prompt:${anchor(event)}`;
|
|
1719
|
+
const long = promptText(message).length > 280;
|
|
1720
|
+
const short = long && !opened.has(key);
|
|
1721
|
+
return /* @__PURE__ */ jsxs("article", {
|
|
1722
|
+
...rowProps,
|
|
1723
|
+
children: [
|
|
1724
|
+
steps,
|
|
1725
|
+
long && /* @__PURE__ */ jsx("p", {
|
|
1726
|
+
className: "ar-prompt-more",
|
|
1727
|
+
children: /* @__PURE__ */ jsx("button", {
|
|
1728
|
+
type: "button",
|
|
1729
|
+
className: "link-button",
|
|
1730
|
+
"data-open-key": key,
|
|
1731
|
+
"aria-expanded": !short,
|
|
1732
|
+
onClick: () => setOpen(key, short),
|
|
1733
|
+
children: short ? "Full prompt" : "Shorter"
|
|
1734
|
+
})
|
|
1735
|
+
}),
|
|
1736
|
+
!short && /* @__PURE__ */ jsx(ClipNote, {
|
|
1737
|
+
clip,
|
|
1738
|
+
open: opener(event)
|
|
1739
|
+
}),
|
|
1740
|
+
flagMarks(event)
|
|
1741
|
+
]
|
|
1742
|
+
});
|
|
1743
|
+
}
|
|
1744
|
+
const finding = event.detail.recordedClaim !== void 0;
|
|
1745
|
+
const notes = [event.detail.publicationNote, finding ? event.detail.assessment : void 0].filter((note) => Boolean(note));
|
|
1746
|
+
return /* @__PURE__ */ jsxs("article", {
|
|
1747
|
+
...rowProps,
|
|
1748
|
+
children: [
|
|
1749
|
+
steps,
|
|
1750
|
+
message && !standalone && /* @__PURE__ */ jsx(ClipNote, {
|
|
1751
|
+
clip: callsOf(event).length ? void 0 : clip,
|
|
1752
|
+
open: opener(event)
|
|
1753
|
+
}),
|
|
1754
|
+
notes.map((note) => /* @__PURE__ */ jsx("p", {
|
|
1755
|
+
className: "publication-note",
|
|
1756
|
+
children: note
|
|
1757
|
+
}, note)),
|
|
1758
|
+
flagMarks(event)
|
|
1759
|
+
]
|
|
1760
|
+
});
|
|
1761
|
+
}
|
|
1762
|
+
// src/run/record/Conversation.tsx
|
|
1763
|
+
function Conversation({ index, actor, cutoff, query, category, selected, openSource, flags, onFlag, lifecycle = false, live = false, follow = live, opening }) {
|
|
1764
|
+
const [opened, setOpened] = useState(() => {
|
|
1765
|
+
const at = anchorsOf(index.events);
|
|
1766
|
+
return new Set(index.events.filter((event) => event.detail.promptKind === "initial").map((event) => `prompt:${at.get(event.id) ?? event.id}`));
|
|
1767
|
+
});
|
|
1768
|
+
const scroll = useRef(null);
|
|
1769
|
+
const anchors = useMemo(() => anchorsOf(index.events), [index]);
|
|
1770
|
+
const anchor = useCallback((event) => anchors.get(event.id) ?? event.id, [anchors]);
|
|
1771
|
+
const opener = (event) => openSource ? (sha256) => openSource(sha256, event) : void 0;
|
|
1772
|
+
const setOpen = (key, open) => setOpened((current) => {
|
|
1773
|
+
if (current.has(key) === open) return current;
|
|
1774
|
+
const next = new Set(current);
|
|
1775
|
+
if (open) next.add(key);
|
|
1776
|
+
else next.delete(key);
|
|
1777
|
+
return next;
|
|
1778
|
+
});
|
|
1779
|
+
const paired = (event, call) => {
|
|
1780
|
+
const key = toolKey(event.node, call.id);
|
|
1781
|
+
return index.calls.get(key)?.length === 1 ? index.results.get(key) ?? [] : [];
|
|
1782
|
+
};
|
|
1783
|
+
const target = useMemo(() => {
|
|
1784
|
+
if (!selected) return void 0;
|
|
1785
|
+
const event = index.byEvent.get(selected);
|
|
1786
|
+
if (event?.detail.toolCallId !== void 0) {
|
|
1787
|
+
const calls = index.calls.get(toolKey(event.node, event.detail.toolCallId));
|
|
1788
|
+
if (calls?.length === 1) return calls[0].event.id;
|
|
1789
|
+
}
|
|
1790
|
+
return selected;
|
|
1791
|
+
}, [selected, index]);
|
|
1792
|
+
const items = useMemo(() => (index.byActor.get(actor) ?? []).filter((event) => {
|
|
1793
|
+
if (ms(event.at) > cutoff) return false;
|
|
1794
|
+
if (event.id === target) return true;
|
|
1795
|
+
if (isLifecycle(event)) return lifecycle && eventMatches(event, index, query, category);
|
|
1796
|
+
if (event.detail.toolCallId !== void 0) {
|
|
1797
|
+
const matching = index.calls.get(toolKey(event.node, event.detail.toolCallId));
|
|
1798
|
+
if (matching?.length === 1 && ms(matching[0].event.at) <= cutoff) return false;
|
|
1799
|
+
}
|
|
1800
|
+
const calls = callsOf(event);
|
|
1801
|
+
if (!(event.detail.role || textOf(event) || event.detail.reasoning || event.detail.responseStatus === "error" || calls.length)) return false;
|
|
1802
|
+
if (event.detail.role === "assistant" && !textOf(event) && !event.detail.reasoning && !calls.length && event.detail.responseStatus !== "error" && !event.detail.contentOmitted && (!thinkingMarkers(event.detail).length || quietThinking(event.detail))) return false;
|
|
1803
|
+
if (eventMatches(event, index, query, category)) return true;
|
|
1804
|
+
return calls.some((call) => {
|
|
1805
|
+
const key = toolKey(event.node, call.id);
|
|
1806
|
+
return index.calls.get(key)?.length === 1 && (index.results.get(key) ?? []).some((result) => ms(result.at) <= cutoff && eventMatches(result, index, query, category));
|
|
1807
|
+
});
|
|
1808
|
+
}), [
|
|
1809
|
+
index,
|
|
1810
|
+
actor,
|
|
1811
|
+
cutoff,
|
|
1812
|
+
query,
|
|
1813
|
+
category,
|
|
1814
|
+
lifecycle,
|
|
1815
|
+
target
|
|
1816
|
+
]);
|
|
1817
|
+
const shown = useMemo(() => {
|
|
1818
|
+
if (!opening?.text || query || category !== "all" || items.some((event) => event.detail.role === "user" && event.detail.promptKind !== "steering")) return items;
|
|
1819
|
+
const first = items[0]?.at ?? opening.at;
|
|
1820
|
+
return [{
|
|
1821
|
+
id: `${actor}:opening`,
|
|
1822
|
+
node: actor,
|
|
1823
|
+
at: first < opening.at ? first : opening.at,
|
|
1824
|
+
kind: "message",
|
|
1825
|
+
category: "other",
|
|
1826
|
+
label: "prompt",
|
|
1827
|
+
detail: {
|
|
1828
|
+
role: "user",
|
|
1829
|
+
publicText: opening.text,
|
|
1830
|
+
promptKind: "initial"
|
|
1831
|
+
}
|
|
1832
|
+
}, ...items];
|
|
1833
|
+
}, [
|
|
1834
|
+
items,
|
|
1835
|
+
opening,
|
|
1836
|
+
actor,
|
|
1837
|
+
query,
|
|
1838
|
+
category
|
|
1839
|
+
]);
|
|
1840
|
+
const rows = useMemo(() => collapseIdleTurns(collapseHollowTurns(collapsePolls(shown, {
|
|
1841
|
+
callsOf,
|
|
1842
|
+
answered: (event, id) => answeredBy(index.results.get(toolKey(event.node, id))),
|
|
1843
|
+
keyOf: anchor
|
|
1844
|
+
}), {
|
|
1845
|
+
callsOf,
|
|
1846
|
+
captured: (event, id) => (index.results.get(toolKey(event.node, id)) ?? []).some((result) => textOf(result)),
|
|
1847
|
+
keyOf: anchor
|
|
1848
|
+
}), callsOf), [
|
|
1849
|
+
shown,
|
|
1850
|
+
index,
|
|
1851
|
+
anchor
|
|
1852
|
+
]);
|
|
1853
|
+
const thinkingSpan = useMemo(() => {
|
|
1854
|
+
const spans = /* @__PURE__ */ new Map();
|
|
1855
|
+
let previous = null;
|
|
1856
|
+
for (const event of index.byActor.get(actor) ?? []) {
|
|
1857
|
+
const at = ms(event.at);
|
|
1858
|
+
if (previous !== null && Number.isFinite(at) && thinkingMarkers(event.detail).length) spans.set(event.id, Math.max(0, at - previous));
|
|
1859
|
+
if (Number.isFinite(at)) previous = at;
|
|
1860
|
+
}
|
|
1861
|
+
return spans;
|
|
1862
|
+
}, [index, actor]);
|
|
1863
|
+
const rowOf = useMemo(() => {
|
|
1864
|
+
const map = /* @__PURE__ */ new Map();
|
|
1865
|
+
for (const row of rows) if (row.kind !== "event") for (const event of row.events) map.set(event.id, row.key);
|
|
1866
|
+
else map.set(row.event.id, row.key);
|
|
1867
|
+
return map;
|
|
1868
|
+
}, [rows]);
|
|
1869
|
+
const retained = (index.byActor.get(actor) ?? []).some((item) => !isLifecycle(item) && (item.detail.role || textOf(item) || callsOf(item).length));
|
|
1870
|
+
const targetRef = useRef(target);
|
|
1871
|
+
targetRef.current = target;
|
|
1872
|
+
const following = useRef(false);
|
|
1873
|
+
const followRef = useRef(follow);
|
|
1874
|
+
followRef.current = follow;
|
|
1875
|
+
useLayoutEffect(() => {
|
|
1876
|
+
following.current = follow && !targetRef.current;
|
|
1877
|
+
if (scroll.current && !targetRef.current) scroll.current.scrollTop = follow ? scroll.current.scrollHeight : 0;
|
|
1878
|
+
}, [actor]);
|
|
1879
|
+
useLayoutEffect(() => {
|
|
1880
|
+
if (!follow) following.current = false;
|
|
1881
|
+
}, [follow]);
|
|
1882
|
+
useEffect(() => {
|
|
1883
|
+
const element = scroll.current;
|
|
1884
|
+
if (!element) return;
|
|
1885
|
+
const track = () => {
|
|
1886
|
+
following.current = followRef.current && element.scrollHeight - element.scrollTop - element.clientHeight < 24;
|
|
1887
|
+
};
|
|
1888
|
+
element.addEventListener("scroll", track, { passive: true });
|
|
1889
|
+
return () => element.removeEventListener("scroll", track);
|
|
1890
|
+
}, []);
|
|
1891
|
+
useLayoutEffect(() => {
|
|
1892
|
+
const element = scroll.current;
|
|
1893
|
+
if (!element || !following.current) return;
|
|
1894
|
+
element.scrollTop = element.scrollHeight;
|
|
1895
|
+
let frames = 0;
|
|
1896
|
+
let frame = requestAnimationFrame(function pin() {
|
|
1897
|
+
if (!following.current) return;
|
|
1898
|
+
element.scrollTop = element.scrollHeight;
|
|
1899
|
+
if (++frames < 20) frame = requestAnimationFrame(pin);
|
|
1900
|
+
});
|
|
1901
|
+
return () => cancelAnimationFrame(frame);
|
|
1902
|
+
}, [rows]);
|
|
1903
|
+
useEffect(() => {
|
|
1904
|
+
if (!selected) return;
|
|
1905
|
+
const event = index.byEvent.get(selected);
|
|
1906
|
+
if (!event) return;
|
|
1907
|
+
const keys = [`prompt:${anchor(event)}`, `reasoning:${anchor(event)}`];
|
|
1908
|
+
if (event.detail.toolCallId !== void 0) for (const match of index.calls.get(toolKey(event.node, event.detail.toolCallId)) ?? []) keys.push(`tool:${anchor(match.event)}:${match.call.id}`, rowOf.get(match.event.id) ?? "");
|
|
1909
|
+
for (const call of callsOf(event)) keys.push(`tool:${anchor(event)}:${call.id}`);
|
|
1910
|
+
const row = rowOf.get(selected);
|
|
1911
|
+
if (row?.startsWith("polls:")) keys.push(row);
|
|
1912
|
+
setOpened((current) => /* @__PURE__ */ new Set([...current, ...keys.filter(Boolean)]));
|
|
1913
|
+
}, [selected, index]);
|
|
1914
|
+
const flagMarks = (event) => {
|
|
1915
|
+
const marks = flags?.get(event.id);
|
|
1916
|
+
if (!marks?.length) return null;
|
|
1917
|
+
return /* @__PURE__ */ jsx("span", {
|
|
1918
|
+
className: "event-flags",
|
|
1919
|
+
children: marks.map((flag) => /* @__PURE__ */ jsx("button", {
|
|
1920
|
+
type: "button",
|
|
1921
|
+
className: `event-flag polarity-${flag.polarity}`,
|
|
1922
|
+
title: flag.title,
|
|
1923
|
+
"data-flag": flag.dimension,
|
|
1924
|
+
onClick: () => onFlag?.(flag, event),
|
|
1925
|
+
children: flag.dimension
|
|
1926
|
+
}, `${flag.dimension}:${flag.label}`))
|
|
1927
|
+
});
|
|
1928
|
+
};
|
|
1929
|
+
const keyOf = useCallback((row) => row.key, []);
|
|
1930
|
+
const estimate = useCallback((row) => row.kind !== "event" ? 36 : isLifecycle(row.event) ? 26 : !textOf(row.event) && callsOf(row.event).length ? 16 + 36 * callsOf(row.event).length : 96, []);
|
|
1931
|
+
const context = {
|
|
1932
|
+
index,
|
|
1933
|
+
cutoff,
|
|
1934
|
+
selected,
|
|
1935
|
+
opened,
|
|
1936
|
+
anchor,
|
|
1937
|
+
opener,
|
|
1938
|
+
setOpen,
|
|
1939
|
+
flagMarks,
|
|
1940
|
+
paired,
|
|
1941
|
+
thinkingSpan,
|
|
1942
|
+
flags,
|
|
1943
|
+
live
|
|
1944
|
+
};
|
|
1945
|
+
const itemsOf = useMemo(() => rows.map((row) => rowItems(row, context)), [
|
|
1946
|
+
rows,
|
|
1947
|
+
opened,
|
|
1948
|
+
cutoff,
|
|
1949
|
+
selected,
|
|
1950
|
+
live,
|
|
1951
|
+
index,
|
|
1952
|
+
thinkingSpan
|
|
1953
|
+
]);
|
|
1954
|
+
const turns = useMemo(() => {
|
|
1955
|
+
let previous;
|
|
1956
|
+
return rows.map((row) => {
|
|
1957
|
+
const event = row.kind === "event" ? row.event : row.events[0];
|
|
1958
|
+
const turn = event.detail.role === "assistant" && !isLifecycle(event) && (!previous || previous.detail.role === "user" || (event.detail.attempt !== void 0 ? event.detail.attempt !== previous.detail.attempt : ms(event.at) - ms(previous.at) > 12e4));
|
|
1959
|
+
previous = row.kind === "event" ? event : row.events.at(-1);
|
|
1960
|
+
return turn;
|
|
1961
|
+
});
|
|
1962
|
+
}, [rows]);
|
|
1963
|
+
const render = (row, i) => /* @__PURE__ */ jsx(ConversationRowView, {
|
|
1964
|
+
row,
|
|
1965
|
+
items: itemsOf[i] ?? [],
|
|
1966
|
+
previous: i ? lastKind(itemsOf[i - 1] ?? []) : void 0,
|
|
1967
|
+
turn: turns[i],
|
|
1968
|
+
context
|
|
1969
|
+
});
|
|
1970
|
+
return /* @__PURE__ */ jsx(Fragment, { children: /* @__PURE__ */ jsxs("div", {
|
|
1971
|
+
className: "record-conversation conversation-scroll",
|
|
1972
|
+
"data-chat": true,
|
|
1973
|
+
ref: scroll,
|
|
1974
|
+
tabIndex: 0,
|
|
1975
|
+
"aria-label": "Scrollable retained conversation",
|
|
1976
|
+
children: [!rows.length && /* @__PURE__ */ jsx("p", {
|
|
1977
|
+
className: "chat-empty",
|
|
1978
|
+
children: query || category !== "all" ? "No messages match this filter." : retained ? "No messages at the selected time." : "Conversation not retained. Its absence does not mean the agent did no work."
|
|
1979
|
+
}), /* @__PURE__ */ jsx(AgentTimelineColumn, {
|
|
1980
|
+
className: "ar-chat",
|
|
1981
|
+
children: /* @__PURE__ */ jsx(VirtualList, {
|
|
1982
|
+
items: rows,
|
|
1983
|
+
keyOf,
|
|
1984
|
+
render,
|
|
1985
|
+
scroller: scroll,
|
|
1986
|
+
target: target ? rowOf.get(target) : void 0,
|
|
1987
|
+
estimate
|
|
1988
|
+
})
|
|
1989
|
+
})]
|
|
1990
|
+
}) });
|
|
1991
|
+
}
|
|
1992
|
+
// src/run/record/prompts.ts
|
|
1993
|
+
/** The conversation needs an opening prompt even when the provider did not retain its first user turn. */
|
|
1994
|
+
function withPrompts(record) {
|
|
1995
|
+
const events = record.events.map((event) => ({
|
|
1996
|
+
...event,
|
|
1997
|
+
detail: { ...event.detail }
|
|
1998
|
+
}));
|
|
1999
|
+
const owners = new Map(record.nodes.map((node) => [node.id, node.agentId ?? node.id]));
|
|
2000
|
+
for (const node of record.nodes.filter((item) => item.kind === "agent")) {
|
|
2001
|
+
const own = events.filter((event) => (owners.get(event.node) ?? event.node) === node.id);
|
|
2002
|
+
const firstReply = own.find((event) => event.detail.role === "assistant")?.at;
|
|
2003
|
+
const userTurns = own.filter((event) => event.detail.role === "user" && typeof event.detail.publicText === "string" && !event.detail.publicText.trimStart().startsWith("<task-notification>"));
|
|
2004
|
+
const initial = userTurns.find((event) => event.detail.promptKind !== "steering" && (!firstReply || event.at <= firstReply));
|
|
2005
|
+
if (initial) initial.detail.promptKind = "initial";
|
|
2006
|
+
else if (node.assignment?.trim()) {
|
|
2007
|
+
const at = node.start ?? own[0]?.at;
|
|
2008
|
+
if (at) events.push({
|
|
2009
|
+
id: `prompt:${node.id}`,
|
|
2010
|
+
node: node.id,
|
|
2011
|
+
at,
|
|
2012
|
+
kind: "message",
|
|
2013
|
+
category: "other",
|
|
2014
|
+
label: "Prompt",
|
|
2015
|
+
source: null,
|
|
2016
|
+
detail: {
|
|
2017
|
+
role: "user",
|
|
2018
|
+
publicText: node.assignment,
|
|
2019
|
+
promptKind: "initial",
|
|
2020
|
+
promptReconstructed: true,
|
|
2021
|
+
promptSource: "node.assignment"
|
|
2022
|
+
}
|
|
2023
|
+
});
|
|
2024
|
+
}
|
|
2025
|
+
for (const event of userTurns) {
|
|
2026
|
+
if (event === initial || event.detail.promptKind === "initial") continue;
|
|
2027
|
+
event.detail.promptKind ??= "steering";
|
|
2028
|
+
}
|
|
2029
|
+
}
|
|
2030
|
+
events.sort((a, b) => a.at.localeCompare(b.at) || Number(b.detail.promptKind === "initial") - Number(a.detail.promptKind === "initial"));
|
|
2031
|
+
return {
|
|
2032
|
+
...record,
|
|
2033
|
+
events
|
|
2034
|
+
};
|
|
2035
|
+
}
|
|
2036
|
+
export { model_exports as C, TraceWaterfall as D, useAgentStream as E, traceSpanMark as O, indexRecord as S, foldAgentRows as T, statusMeta$1 as _, InlineToolItem as a, withPrompts as b, RunPhaseList as c, RunStatusIcon$1 as d, TaskList as f, showsTaskPriority as g, runStatus$1 as h, InlineThinkingItem$1 as i, RunRowShell$1 as l, parseToolEvent as m, ApprovalDiffSummary as n, LiveDuration as o, ToolCallFeed as p, ExpandedToolDetail as r, RunGroup as s, ApprovalCard as t, RunRowStatusDot as u, taskProgress as v, TraceWorkspace as w, Conversation as x, taskStatus as y };
|