@tangle-network/agent-app 0.8.1 → 0.9.1

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.
@@ -0,0 +1,145 @@
1
+ // src/trace/index.ts
2
+ function timedEventsFromLines(lines) {
3
+ const out = [];
4
+ for (const line of lines) {
5
+ try {
6
+ const parsed = JSON.parse(line);
7
+ if (typeof parsed._t === "number") out.push({ t: parsed._t, event: parsed });
8
+ } catch {
9
+ }
10
+ }
11
+ return out.sort((a, b) => a.t - b.t);
12
+ }
13
+ function innerOf(e) {
14
+ return (e.kind === "event" ? e.event : e) ?? {};
15
+ }
16
+ function buildFlowTrace(events, opts) {
17
+ const spans = [];
18
+ let promptTokens = 0;
19
+ let completionTokens = 0;
20
+ let toolCalls = 0;
21
+ const first = events[0]?.t ?? 0;
22
+ if (first > 0) {
23
+ spans.push({ kind: "pipeline", name: "dispatch \u2192 first event", startMs: 0, endMs: first });
24
+ }
25
+ let segStart = null;
26
+ let segEnd = 0;
27
+ let segKinds = /* @__PURE__ */ new Set();
28
+ let lastDeltaT = first;
29
+ const openCalls = /* @__PURE__ */ new Map();
30
+ const closeSegment = () => {
31
+ if (segStart !== null) {
32
+ spans.push({
33
+ kind: "model",
34
+ name: segKinds.has("reasoning") ? "model turn (reasoning + text)" : "model turn",
35
+ startMs: segStart,
36
+ endMs: segEnd,
37
+ approx: true
38
+ });
39
+ segStart = null;
40
+ segKinds = /* @__PURE__ */ new Set();
41
+ }
42
+ };
43
+ for (const { t, event } of events) {
44
+ const inner = innerOf(event);
45
+ const type = String(event.kind === "tool_result" ? "tool_result" : inner.type ?? "");
46
+ if (type === "text" || type === "reasoning") {
47
+ if (segStart === null) segStart = t;
48
+ segEnd = t;
49
+ segKinds.add(type);
50
+ lastDeltaT = t;
51
+ } else if (type === "tool_call") {
52
+ closeSegment();
53
+ toolCalls++;
54
+ const call = inner.call ?? inner;
55
+ const id = String(call.toolCallId ?? `call_${toolCalls}`);
56
+ openCalls.set(id, { name: String(call.toolName ?? "tool"), emitT: t, lastDeltaT });
57
+ } else if (type === "tool_result") {
58
+ const id = String(event.toolCallId ?? inner.toolCallId ?? "");
59
+ const open = openCalls.get(id);
60
+ if (open) {
61
+ spans.push({
62
+ kind: "tool",
63
+ name: open.name,
64
+ // Execution happens between the end of the model turn that emitted
65
+ // the call and the result landing in the buffer.
66
+ startMs: open.lastDeltaT,
67
+ endMs: t,
68
+ approx: true,
69
+ meta: { ok: (event.outcome ?? inner.outcome)?.ok }
70
+ });
71
+ openCalls.delete(id);
72
+ }
73
+ } else if (type === "usage") {
74
+ const u = inner.usage ?? {};
75
+ promptTokens += u.promptTokens ?? 0;
76
+ completionTokens += u.completionTokens ?? 0;
77
+ }
78
+ }
79
+ closeSegment();
80
+ const totalMs = events.length ? events[events.length - 1].t : 0;
81
+ const trace = { spans, totalMs, promptTokens, completionTokens, toolCalls };
82
+ const p = opts?.pricing;
83
+ if (p && (p.prompt != null || p.completion != null)) {
84
+ trace.costUsd = promptTokens * Number(p.prompt ?? 0) + completionTokens * Number(p.completion ?? 0);
85
+ }
86
+ return trace;
87
+ }
88
+ var fmtS = (ms) => `${(ms / 1e3).toFixed(1)}s`;
89
+ function renderWaterfall(trace, opts) {
90
+ const width = opts?.width ?? 40;
91
+ const scale = trace.totalMs > 0 ? width / trace.totalMs : 0;
92
+ const lines = [];
93
+ const spans = [...trace.spans].sort((a, b) => a.startMs - b.startMs);
94
+ for (let i = 0; i < spans.length; i++) {
95
+ const s = spans[i];
96
+ const offset = Math.round(s.startMs * scale);
97
+ const len = Math.max(1, Math.round((s.endMs - s.startMs) * scale));
98
+ const bar = " ".repeat(offset) + (s.kind === "tool" ? "\u2593" : s.kind === "pipeline" ? "\u2591" : "\u2588").repeat(len);
99
+ const branch = i === spans.length - 1 ? "\u2514\u2500" : "\u251C\u2500";
100
+ const dur = `${fmtS(s.endMs - s.startMs)}${s.approx ? "~" : ""}`;
101
+ lines.push(`${fmtS(s.startMs).padStart(7)} ${branch} ${bar.padEnd(width + 2)} ${s.name} (${dur})`);
102
+ }
103
+ const cost = trace.costUsd != null ? ` $${trace.costUsd.toFixed(trace.costUsd < 0.01 ? 6 : 4)}` : "";
104
+ lines.push(
105
+ `${fmtS(trace.totalMs).padStart(7)} \u2500\u2500 total \xB7 ${trace.promptTokens}p + ${trace.completionTokens}c tok \xB7 ${trace.toolCalls} tool calls${cost}`
106
+ );
107
+ return lines.join("\n");
108
+ }
109
+ function summarize(values) {
110
+ const sorted = [...values].sort((a, b) => a - b);
111
+ const q = (p) => sorted[Math.min(sorted.length - 1, Math.floor(p * sorted.length))] ?? 0;
112
+ return { n: sorted.length, min: sorted[0] ?? 0, p50: q(0.5), p90: q(0.9), max: sorted[sorted.length - 1] ?? 0 };
113
+ }
114
+ function renderHistogram(values, opts) {
115
+ if (!values.length) return "(no samples)";
116
+ const buckets = opts?.buckets ?? 6;
117
+ const width = opts?.width ?? 24;
118
+ const fmt = opts?.format ?? ((v) => `${Math.round(v)}${opts?.unit ?? ""}`);
119
+ const s = summarize(values);
120
+ const lo = s.min;
121
+ const hi = s.max === s.min ? s.min + 1 : s.max;
122
+ const counts = new Array(buckets).fill(0);
123
+ for (const v of values) {
124
+ counts[Math.min(buckets - 1, Math.floor((v - lo) / (hi - lo) * buckets))]++;
125
+ }
126
+ const maxCount = Math.max(...counts);
127
+ const lines = [
128
+ `n=${s.n} min=${fmt(s.min)} p50=${fmt(s.p50)} p90=${fmt(s.p90)} max=${fmt(s.max)}`
129
+ ];
130
+ for (let i = 0; i < buckets; i++) {
131
+ const a = lo + (hi - lo) * i / buckets;
132
+ const b = lo + (hi - lo) * (i + 1) / buckets;
133
+ const bar = "\u2588".repeat(Math.max(counts[i] > 0 ? 1 : 0, Math.round(counts[i] / maxCount * width)));
134
+ lines.push(`${fmt(a).padStart(8)}-${fmt(b).padEnd(8)} ${bar} ${counts[i]}`);
135
+ }
136
+ return lines.join("\n");
137
+ }
138
+ export {
139
+ buildFlowTrace,
140
+ renderHistogram,
141
+ renderWaterfall,
142
+ summarize,
143
+ timedEventsFromLines
144
+ };
145
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/trace/index.ts"],"sourcesContent":["/**\n * `@tangle-network/agent-app/trace` — flow observability for agent turns.\n *\n * The turn buffer stamps `_t` (ms since turn start) on every event, so any\n * live stream OR any historical turn replayed from a TurnEventStore can be\n * reconstructed into a span trace: pipeline overhead, model segments (with\n * thinking TTFT), tool executions, token usage, and cost. Renderers turn\n * traces and multi-run samples into ASCII waterfalls and histograms — the\n * default artifact for \"how did this run actually behave\" questions across\n * evals, hill-climbs, and production debugging.\n *\n * Span boundaries derived from a buffered stream are quantized by the\n * pump's flush window and the reader's poll cadence (~100–400ms); spans\n * carry `approx: true` to keep reports honest about that.\n */\n\nexport interface TimedEvent {\n /** ms since turn start (`_t` stamped by pumpBufferedTurn). */\n t: number\n event: Record<string, unknown>\n}\n\nexport interface FlowSpan {\n kind: 'pipeline' | 'model' | 'tool'\n name: string\n startMs: number\n endMs: number\n approx?: boolean\n meta?: Record<string, unknown>\n}\n\nexport interface FlowTrace {\n spans: FlowSpan[]\n totalMs: number\n promptTokens: number\n completionTokens: number\n /** Computed when per-token pricing is supplied. */\n costUsd?: number\n toolCalls: number\n}\n\n/** Parse stored turn-event lines (JSON strings with `_t`) into TimedEvents. */\nexport function timedEventsFromLines(lines: string[]): TimedEvent[] {\n const out: TimedEvent[] = []\n for (const line of lines) {\n try {\n const parsed = JSON.parse(line) as Record<string, unknown>\n if (typeof parsed._t === 'number') out.push({ t: parsed._t, event: parsed })\n } catch {\n /* skip torn lines */\n }\n }\n return out.sort((a, b) => a.t - b.t)\n}\n\nfunction innerOf(e: Record<string, unknown>): Record<string, unknown> {\n return (e.kind === 'event' ? (e.event as Record<string, unknown>) : e) ?? {}\n}\n\n/**\n * Derive a span trace from timestamped turn events. Model segments are runs\n * of text/reasoning deltas; a tool span opens at the last delta before its\n * tool_call emission and closes at the matching tool_result.\n */\nexport function buildFlowTrace(\n events: TimedEvent[],\n opts?: { pricing?: { prompt?: string | number; completion?: string | number } },\n): FlowTrace {\n const spans: FlowSpan[] = []\n let promptTokens = 0\n let completionTokens = 0\n let toolCalls = 0\n\n const first = events[0]?.t ?? 0\n if (first > 0) {\n spans.push({ kind: 'pipeline', name: 'dispatch → first event', startMs: 0, endMs: first })\n }\n\n let segStart: number | null = null\n let segEnd = 0\n let segKinds = new Set<string>()\n let lastDeltaT = first\n const openCalls = new Map<string, { name: string; emitT: number; lastDeltaT: number }>()\n\n const closeSegment = () => {\n if (segStart !== null) {\n spans.push({\n kind: 'model',\n name: segKinds.has('reasoning') ? 'model turn (reasoning + text)' : 'model turn',\n startMs: segStart,\n endMs: segEnd,\n approx: true,\n })\n segStart = null\n segKinds = new Set()\n }\n }\n\n for (const { t, event } of events) {\n const inner = innerOf(event)\n const type = String(event.kind === 'tool_result' ? 'tool_result' : (inner.type ?? ''))\n\n if (type === 'text' || type === 'reasoning') {\n if (segStart === null) segStart = t\n segEnd = t\n segKinds.add(type)\n lastDeltaT = t\n } else if (type === 'tool_call') {\n closeSegment()\n toolCalls++\n const call = (inner.call ?? inner) as Record<string, unknown>\n const id = String(call.toolCallId ?? `call_${toolCalls}`)\n openCalls.set(id, { name: String(call.toolName ?? 'tool'), emitT: t, lastDeltaT })\n } else if (type === 'tool_result') {\n const id = String(event.toolCallId ?? inner.toolCallId ?? '')\n const open = openCalls.get(id)\n if (open) {\n spans.push({\n kind: 'tool',\n name: open.name,\n // Execution happens between the end of the model turn that emitted\n // the call and the result landing in the buffer.\n startMs: open.lastDeltaT,\n endMs: t,\n approx: true,\n meta: { ok: ((event.outcome ?? inner.outcome) as { ok?: boolean } | undefined)?.ok },\n })\n openCalls.delete(id)\n }\n } else if (type === 'usage') {\n const u = (inner.usage ?? {}) as { promptTokens?: number; completionTokens?: number }\n promptTokens += u.promptTokens ?? 0\n completionTokens += u.completionTokens ?? 0\n }\n }\n closeSegment()\n\n const totalMs = events.length ? events[events.length - 1]!.t : 0\n const trace: FlowTrace = { spans, totalMs, promptTokens, completionTokens, toolCalls }\n const p = opts?.pricing\n if (p && (p.prompt != null || p.completion != null)) {\n trace.costUsd = promptTokens * Number(p.prompt ?? 0) + completionTokens * Number(p.completion ?? 0)\n }\n return trace\n}\n\nconst fmtS = (ms: number) => `${(ms / 1000).toFixed(1)}s`\n\n/** ASCII waterfall cascade — the default artifact for explaining a flow. */\nexport function renderWaterfall(trace: FlowTrace, opts?: { width?: number }): string {\n const width = opts?.width ?? 40\n const scale = trace.totalMs > 0 ? width / trace.totalMs : 0\n const lines: string[] = []\n const spans = [...trace.spans].sort((a, b) => a.startMs - b.startMs)\n for (let i = 0; i < spans.length; i++) {\n const s = spans[i]!\n const offset = Math.round(s.startMs * scale)\n const len = Math.max(1, Math.round((s.endMs - s.startMs) * scale))\n const bar = ' '.repeat(offset) + (s.kind === 'tool' ? '▓' : s.kind === 'pipeline' ? '░' : '█').repeat(len)\n const branch = i === spans.length - 1 ? '└─' : '├─'\n const dur = `${fmtS(s.endMs - s.startMs)}${s.approx ? '~' : ''}`\n lines.push(`${fmtS(s.startMs).padStart(7)} ${branch} ${bar.padEnd(width + 2)} ${s.name} (${dur})`)\n }\n const cost = trace.costUsd != null ? ` $${trace.costUsd.toFixed(trace.costUsd < 0.01 ? 6 : 4)}` : ''\n lines.push(\n `${fmtS(trace.totalMs).padStart(7)} ── total · ${trace.promptTokens}p + ${trace.completionTokens}c tok · ${trace.toolCalls} tool calls${cost}`,\n )\n return lines.join('\\n')\n}\n\nexport interface DistributionSummary {\n n: number\n min: number\n p50: number\n p90: number\n max: number\n}\n\nexport function summarize(values: number[]): DistributionSummary {\n const sorted = [...values].sort((a, b) => a - b)\n const q = (p: number) => sorted[Math.min(sorted.length - 1, Math.floor(p * sorted.length))] ?? 0\n return { n: sorted.length, min: sorted[0] ?? 0, p50: q(0.5), p90: q(0.9), max: sorted[sorted.length - 1] ?? 0 }\n}\n\n/** ASCII histogram for multi-run samples (eval latencies, costs, scores). */\nexport function renderHistogram(\n values: number[],\n opts?: { buckets?: number; width?: number; unit?: string; format?: (v: number) => string },\n): string {\n if (!values.length) return '(no samples)'\n const buckets = opts?.buckets ?? 6\n const width = opts?.width ?? 24\n const fmt = opts?.format ?? ((v: number) => `${Math.round(v)}${opts?.unit ?? ''}`)\n const s = summarize(values)\n const lo = s.min\n const hi = s.max === s.min ? s.min + 1 : s.max\n const counts = new Array<number>(buckets).fill(0)\n for (const v of values) {\n counts[Math.min(buckets - 1, Math.floor(((v - lo) / (hi - lo)) * buckets))]!++\n }\n const maxCount = Math.max(...counts)\n const lines = [\n `n=${s.n} min=${fmt(s.min)} p50=${fmt(s.p50)} p90=${fmt(s.p90)} max=${fmt(s.max)}`,\n ]\n for (let i = 0; i < buckets; i++) {\n const a = lo + ((hi - lo) * i) / buckets\n const b = lo + ((hi - lo) * (i + 1)) / buckets\n const bar = '█'.repeat(Math.max(counts[i]! > 0 ? 1 : 0, Math.round((counts[i]! / maxCount) * width)))\n lines.push(`${fmt(a).padStart(8)}-${fmt(b).padEnd(8)} ${bar} ${counts[i]}`)\n }\n return lines.join('\\n')\n}\n"],"mappings":"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","names":[]}
@@ -185,8 +185,11 @@ interface ChatToolCallInfo {
185
185
  id: string;
186
186
  name: string;
187
187
  status: 'running' | 'done' | 'error';
188
+ /** The call arguments, captured from the tool_call event — shown in the
189
+ * expanded card so users see exactly what the agent invoked. */
190
+ args?: Record<string, unknown>;
188
191
  /** The tool outcome (`{ok, result}` shape). When `result.status` is
189
- * 'queued_for_approval' the chip renders the approval state. */
192
+ * 'queued_for_approval' the card renders the approval state. */
190
193
  result?: unknown;
191
194
  }
192
195
  /** Extract `{proposalId, status}` from a tool outcome when it is a proposal
@@ -216,19 +219,24 @@ interface ChatMessagesProps {
216
219
  /** Approve/Reject handlers for proposals awaiting approval. When omitted the
217
220
  * chip still shows "awaiting approval" but without action buttons. */
218
221
  approval?: ProposalApprovalHandlers;
219
- /** Make tool chips clickable (e.g. open a {@link RunDrillIn} panel). */
222
+ /** Open a full-transcript view (e.g. {@link RunDrillIn}) from a tool card. */
220
223
  onToolCallClick?: (call: ChatToolCallInfo, message: ChatUiMessage) => void;
224
+ /** Per-tool custom detail renderers for expanded tool cards. */
225
+ toolRenderers?: ToolDetailRenderers;
221
226
  }
222
227
  interface ProposalApprovalHandlers {
223
228
  onApprove: (proposalId: string, toolCallId: string) => void | Promise<void>;
224
229
  onReject: (proposalId: string, toolCallId: string) => void | Promise<void>;
225
230
  }
231
+ /** Per-tool custom detail renderers for the expanded card body — keyed by
232
+ * tool name. Return null to fall back to the generic detail view. */
233
+ type ToolDetailRenderers = Record<string, (call: ChatToolCallInfo, message: ChatUiMessage) => ReactNode>;
226
234
  /**
227
235
  * The message thread: one centered column; user messages are right-aligned
228
236
  * bubbles with a User label; agent messages carry an Agent meta line with
229
237
  * model id, tokens/sec, and cost, plus a collapsible thinking section and
230
238
  * tool-call chips.
231
239
  */
232
- declare function ChatMessages({ messages, models, renderMarkdown, renderExtras, userLabel, agentLabel, loading, approval, onToolCallClick, }: ChatMessagesProps): react.JSX.Element;
240
+ declare function ChatMessages({ messages, models, renderMarkdown, renderExtras, userLabel, agentLabel, loading, approval, onToolCallClick, toolRenderers, }: ChatMessagesProps): react.JSX.Element;
233
241
 
234
- export { type ChatMessageMetrics, ChatMessages, type ChatMessagesProps, type ChatStreamCallbacks, type ChatStreamToolCall, type ChatStreamToolResult, type ChatToolCallInfo, type ChatUiMessage, type ConsumeChatStreamResult, EffortPicker, type EffortPickerProps, ModelPicker, type ModelPickerProps, type ProposalApprovalHandlers, ProviderLogo, type ProviderLogoProps, RunDrillIn, type RunDrillInProps, type SmoothRevealOptions, type StreamChatOptions, type ToolRunRecord, type ToolRunStep, consumeChatStream, dispatchChatStreamLine, formatModelCost, formatTokensPerSecond, nextRevealCount, pendingApprovalOf, streamChatTurn, useSmoothText };
242
+ export { type ChatMessageMetrics, ChatMessages, type ChatMessagesProps, type ChatStreamCallbacks, type ChatStreamToolCall, type ChatStreamToolResult, type ChatToolCallInfo, type ChatUiMessage, type ConsumeChatStreamResult, EffortPicker, type EffortPickerProps, ModelPicker, type ModelPickerProps, type ProposalApprovalHandlers, ProviderLogo, type ProviderLogoProps, RunDrillIn, type RunDrillInProps, type SmoothRevealOptions, type StreamChatOptions, type ToolDetailRenderers, type ToolRunRecord, type ToolRunStep, consumeChatStream, dispatchChatStreamLine, formatModelCost, formatTokensPerSecond, nextRevealCount, pendingApprovalOf, streamChatTurn, useSmoothText };
@@ -449,62 +449,125 @@ function pendingApprovalOf(call) {
449
449
  if (!outcome?.ok || outcome.result?.status !== "queued_for_approval" || !outcome.result.proposalId) return null;
450
450
  return { proposalId: outcome.result.proposalId };
451
451
  }
452
- function ToolChips({
453
- toolCalls,
452
+ function toolOutcomeOf(call) {
453
+ return call.result;
454
+ }
455
+ function friendlyToolTitle(call) {
456
+ const a = call.args ?? {};
457
+ switch (call.name) {
458
+ case "submit_proposal":
459
+ return `Proposal \xB7 ${String(a.type ?? "")}${a.title ? `: ${String(a.title)}` : ""}`;
460
+ case "sandbox_create":
461
+ return `Create sandbox (${String(a.environment ?? "universal")})`;
462
+ case "sandbox_run_command":
463
+ return `$ ${String(a.command ?? "")}`;
464
+ case "sandbox_destroy":
465
+ return `Destroy sandbox ${String(a.sandbox_id ?? "")}`;
466
+ case "schedule_followup":
467
+ return `Follow-up \xB7 ${String(a.title ?? "")}`;
468
+ case "render_ui":
469
+ return `Render view \xB7 ${String(a.title ?? "")}`;
470
+ case "add_citation":
471
+ return `Citation \xB7 ${String(a.path ?? "")}`;
472
+ default:
473
+ return call.name;
474
+ }
475
+ }
476
+ function truncate(v, max = 240) {
477
+ const s = typeof v === "string" ? v : JSON.stringify(v);
478
+ return s.length > max ? `${s.slice(0, max)}\u2026` : s;
479
+ }
480
+ function KvRows({ data }) {
481
+ const entries = Object.entries(data).filter(([, v]) => v !== void 0 && v !== null && v !== "");
482
+ if (!entries.length) return null;
483
+ return /* @__PURE__ */ jsx2("dl", { className: "grid grid-cols-[auto_1fr] gap-x-3 gap-y-1", children: entries.map(([k, v]) => /* @__PURE__ */ jsxs2("div", { className: "contents", children: [
484
+ /* @__PURE__ */ jsx2("dt", { className: "font-mono text-[11px] text-muted-foreground/70", children: k }),
485
+ /* @__PURE__ */ jsx2("dd", { className: "min-w-0 whitespace-pre-wrap break-words font-mono text-[11px] text-muted-foreground", children: truncate(v) })
486
+ ] }, k)) });
487
+ }
488
+ function DefaultToolDetail({ call }) {
489
+ const outcome = toolOutcomeOf(call);
490
+ return /* @__PURE__ */ jsxs2("div", { className: "space-y-2", children: [
491
+ call.args && Object.keys(call.args).length > 0 && /* @__PURE__ */ jsxs2("div", { children: [
492
+ /* @__PURE__ */ jsx2("p", { className: "mb-1 text-[10px] font-semibold uppercase tracking-wide text-muted-foreground/50", children: "Called with" }),
493
+ /* @__PURE__ */ jsx2(KvRows, { data: call.args })
494
+ ] }),
495
+ outcome && /* @__PURE__ */ jsxs2("div", { children: [
496
+ /* @__PURE__ */ jsx2("p", { className: "mb-1 text-[10px] font-semibold uppercase tracking-wide text-muted-foreground/50", children: outcome.ok === false ? "Failed" : "Result" }),
497
+ outcome.ok === false ? /* @__PURE__ */ jsx2("p", { className: "text-xs text-red-600", children: outcome.message ?? "Tool failed" }) : outcome.result && typeof outcome.result === "object" ? /* @__PURE__ */ jsx2(KvRows, { data: outcome.result }) : /* @__PURE__ */ jsx2("p", { className: "font-mono text-[11px] text-muted-foreground", children: truncate(outcome.result) })
498
+ ] })
499
+ ] });
500
+ }
501
+ function ToolCallCard({
502
+ call,
503
+ message,
454
504
  approval,
455
- onClick
505
+ onOpenRun,
506
+ renderers
456
507
  }) {
457
- return /* @__PURE__ */ jsx2("div", { className: "mt-2 flex flex-col gap-1", children: toolCalls.map((tc) => {
458
- const pending = tc.status === "done" ? pendingApprovalOf(tc) : null;
459
- if (pending) {
460
- return /* @__PURE__ */ jsxs2(
461
- "div",
462
- {
463
- className: "inline-flex w-fit items-center gap-2 rounded-md bg-amber-500/10 px-2.5 py-1 text-xs text-amber-700",
464
- children: [
465
- /* @__PURE__ */ jsx2("span", { className: "font-mono opacity-70", children: "\u23F8" }),
466
- /* @__PURE__ */ jsx2("span", { className: "font-medium", children: tc.name }),
467
- /* @__PURE__ */ jsx2("span", { className: "opacity-60", children: "awaiting approval" }),
468
- approval && /* @__PURE__ */ jsxs2("span", { className: "ml-1 inline-flex items-center gap-1", children: [
508
+ const [expanded, setExpanded] = useState2(false);
509
+ const pending = call.status === "done" ? pendingApprovalOf(call) : null;
510
+ const failed = call.status === "error" || toolOutcomeOf(call)?.ok === false;
511
+ const custom = renderers?.[call.name]?.(call, message);
512
+ return /* @__PURE__ */ jsxs2(
513
+ "div",
514
+ {
515
+ className: `w-fit min-w-[280px] max-w-full rounded-lg border text-xs transition ${pending ? "border-amber-300/60 bg-amber-500/5" : failed ? "border-red-300/60 bg-red-500/5" : "border-border/60 bg-muted/20"}`,
516
+ children: [
517
+ /* @__PURE__ */ jsxs2(
518
+ "button",
519
+ {
520
+ type: "button",
521
+ onClick: () => setExpanded((v) => !v),
522
+ className: "flex w-full items-center gap-2 px-3 py-2 text-left",
523
+ children: [
469
524
  /* @__PURE__ */ jsx2(
470
- "button",
525
+ "span",
471
526
  {
472
- type: "button",
473
- onClick: () => approval.onApprove(pending.proposalId, tc.id),
474
- className: "rounded bg-green-600/90 px-2 py-0.5 text-[11px] font-semibold text-white transition hover:bg-green-600",
475
- children: "Approve"
527
+ className: `h-2 w-2 shrink-0 rounded-full ${call.status === "running" ? "animate-pulse bg-yellow-500" : pending ? "bg-amber-500" : failed ? "bg-red-500" : "bg-green-500"}`
476
528
  }
477
529
  ),
478
- /* @__PURE__ */ jsx2(
479
- "button",
480
- {
481
- type: "button",
482
- onClick: () => approval.onReject(pending.proposalId, tc.id),
483
- className: "rounded border border-border bg-card px-2 py-0.5 text-[11px] font-medium text-foreground transition hover:bg-accent/30",
484
- children: "Reject"
485
- }
486
- )
487
- ] })
488
- ]
489
- },
490
- tc.id
491
- );
530
+ /* @__PURE__ */ jsx2("span", { className: "min-w-0 flex-1 truncate font-medium", children: friendlyToolTitle(call) }),
531
+ pending && approval && /* @__PURE__ */ jsxs2("span", { className: "flex shrink-0 items-center gap-1", onClick: (e) => e.stopPropagation(), children: [
532
+ /* @__PURE__ */ jsx2(
533
+ "button",
534
+ {
535
+ type: "button",
536
+ onClick: () => approval.onApprove(pending.proposalId, call.id),
537
+ className: "rounded bg-green-600/90 px-2 py-0.5 text-[11px] font-semibold text-white transition hover:bg-green-600",
538
+ children: "Approve"
539
+ }
540
+ ),
541
+ /* @__PURE__ */ jsx2(
542
+ "button",
543
+ {
544
+ type: "button",
545
+ onClick: () => approval.onReject(pending.proposalId, call.id),
546
+ className: "rounded border border-border bg-card px-2 py-0.5 text-[11px] font-medium text-foreground transition hover:bg-accent/30",
547
+ children: "Reject"
548
+ }
549
+ )
550
+ ] }),
551
+ /* @__PURE__ */ jsx2("span", { className: "shrink-0 text-[11px] text-muted-foreground/70", children: call.status === "running" ? "running\u2026" : pending ? "awaiting approval" : failed ? "failed" : "done" }),
552
+ /* @__PURE__ */ jsx2(ChevronDown, { className: `h-3 w-3 shrink-0 text-muted-foreground transition-transform ${expanded ? "rotate-180" : ""}` })
553
+ ]
554
+ }
555
+ ),
556
+ expanded && /* @__PURE__ */ jsxs2("div", { className: "border-t border-border/40 px-3 py-2.5", children: [
557
+ custom ?? /* @__PURE__ */ jsx2(DefaultToolDetail, { call }),
558
+ onOpenRun && call.name.startsWith("sandbox_") && /* @__PURE__ */ jsx2(
559
+ "button",
560
+ {
561
+ type: "button",
562
+ onClick: () => onOpenRun(call, message),
563
+ className: "mt-2 rounded border border-border bg-card px-2 py-1 text-[11px] font-medium transition hover:bg-accent/30",
564
+ children: "Open full transcript \u2192"
565
+ }
566
+ )
567
+ ] })
568
+ ]
492
569
  }
493
- const Tag = onClick ? "button" : "div";
494
- return /* @__PURE__ */ jsxs2(
495
- Tag,
496
- {
497
- ...onClick ? { type: "button", onClick: () => onClick(tc) } : {},
498
- className: `inline-flex w-fit items-center gap-2 rounded-md px-2.5 py-1 text-xs ${tc.status === "running" ? "bg-yellow-500/10 text-yellow-700" : tc.status === "error" ? "bg-red-500/10 text-red-700" : "bg-green-500/10 text-green-700"} ${onClick ? "cursor-pointer transition hover:ring-1 hover:ring-border" : ""}`,
499
- children: [
500
- /* @__PURE__ */ jsx2("span", { className: "font-mono opacity-70", children: tc.status === "running" ? "\u26A1" : tc.status === "error" ? "\u2717" : "\u2713" }),
501
- /* @__PURE__ */ jsx2("span", { className: "font-medium", children: tc.name }),
502
- /* @__PURE__ */ jsx2("span", { className: "opacity-60", children: tc.status === "running" ? "running\u2026" : tc.status === "error" ? "failed" : "done" })
503
- ]
504
- },
505
- tc.id
506
- );
507
- }) });
570
+ );
508
571
  }
509
572
  function AssistantMessage({
510
573
  msg,
@@ -514,6 +577,7 @@ function AssistantMessage({
514
577
  renderBody,
515
578
  approval,
516
579
  onToolCallClick,
580
+ toolRenderers,
517
581
  renderExtras
518
582
  }) {
519
583
  const content = useSmoothText(msg.content, streaming);
@@ -538,14 +602,17 @@ function AssistantMessage({
538
602
  /* @__PURE__ */ jsx2("div", { ref: reasoningScrollRef, className: "mt-2 max-h-48 overflow-y-auto whitespace-pre-wrap text-sm text-muted-foreground/80", children: reasoning })
539
603
  ] }),
540
604
  /* @__PURE__ */ jsx2("div", { className: "text-base leading-[1.75]", children: renderBody(content) }),
541
- msg.toolCalls && msg.toolCalls.length > 0 && /* @__PURE__ */ jsx2(
542
- ToolChips,
605
+ msg.toolCalls && msg.toolCalls.length > 0 && /* @__PURE__ */ jsx2("div", { className: "mt-2 flex flex-col gap-1.5", children: msg.toolCalls.map((tc) => /* @__PURE__ */ jsx2(
606
+ ToolCallCard,
543
607
  {
544
- toolCalls: msg.toolCalls,
608
+ call: tc,
609
+ message: msg,
545
610
  approval,
546
- onClick: onToolCallClick ? (tc) => onToolCallClick(tc, msg) : void 0
547
- }
548
- ),
611
+ onOpenRun: onToolCallClick,
612
+ renderers: toolRenderers
613
+ },
614
+ tc.id
615
+ )) }),
549
616
  renderExtras?.(msg)
550
617
  ] });
551
618
  }
@@ -573,7 +640,8 @@ function ChatMessages({
573
640
  agentLabel = "Agent",
574
641
  loading,
575
642
  approval,
576
- onToolCallClick
643
+ onToolCallClick,
644
+ toolRenderers
577
645
  }) {
578
646
  const renderBody = renderMarkdown ?? ((content) => /* @__PURE__ */ jsx2("p", { className: "whitespace-pre-wrap", children: content }));
579
647
  const lastIsUser = messages[messages.length - 1]?.role === "user";
@@ -592,6 +660,7 @@ function ChatMessages({
592
660
  renderBody,
593
661
  approval,
594
662
  onToolCallClick,
663
+ toolRenderers,
595
664
  renderExtras
596
665
  },
597
666
  msg.id