@volter-ai-dev/supercode-ui 0.1.8 → 0.1.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/conversation.mjs CHANGED
@@ -59,6 +59,108 @@ var EMPTY_UI_STATE = Object.freeze({
59
59
  owned: null,
60
60
  attachError: null
61
61
  });
62
+ var TOOL_CATEGORIES = /* @__PURE__ */ new Set(["read", "search", "edit", "command", "test", "web", "agent", "plan", "other"]);
63
+ var MAX_TOOL_FIELDS = 8;
64
+ var MAX_TOOL_FIELD_CHARS = 800;
65
+ var MAX_TOOL_PREVIEW_CHARS = 4e3;
66
+ function record(value) {
67
+ return value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
68
+ }
69
+ function boundedString(value, max) {
70
+ if (typeof value !== "string") return "";
71
+ return value.length <= max ? value : `${value.slice(0, max)}\u2026`;
72
+ }
73
+ function argumentObject(argumentsText) {
74
+ if (!argumentsText) return null;
75
+ try {
76
+ return record(JSON.parse(argumentsText));
77
+ } catch {
78
+ return null;
79
+ }
80
+ }
81
+ function firstString(source, keys) {
82
+ for (const key of keys) {
83
+ if (typeof source?.[key] === "string" && source[key].trim()) return source[key].trim();
84
+ }
85
+ return "";
86
+ }
87
+ function explicitNumber(sources, keys) {
88
+ for (const source of sources) {
89
+ for (const key of keys) {
90
+ const value = source?.[key];
91
+ const parsed = typeof value === "string" && value.trim() !== "" ? Number(value) : value;
92
+ if (typeof parsed === "number" && Number.isFinite(parsed)) return parsed;
93
+ }
94
+ }
95
+ return null;
96
+ }
97
+ function toolDetail(category) {
98
+ return { read: "file", search: "matches", edit: "diff", command: "terminal", test: "terminal", web: "web", agent: "agent", plan: "plan" }[category] ?? "fields";
99
+ }
100
+ function usefulToolFields(args) {
101
+ if (!args) return [];
102
+ const hidden = /* @__PURE__ */ new Set(["command", "cmd", "file_path", "target_file", "path", "query", "pattern", "url", "patch", "diff", "old_string", "new_string", "content", "prompt", "description", "task", "plan", "todos"]);
103
+ return Object.entries(args).flatMap(([key, value]) => {
104
+ if (hidden.has(key) || value === null || value === void 0) return [];
105
+ const rendered = typeof value === "string" ? value : JSON.stringify(value);
106
+ if (!rendered) return [];
107
+ return [{ label: key.replaceAll(/[_-]+/g, " ").replace(/^./, (letter) => letter.toLocaleUpperCase()), value: boundedString(rendered, MAX_TOOL_FIELD_CHARS) }];
108
+ }).slice(0, MAX_TOOL_FIELDS);
109
+ }
110
+ function planItems(args) {
111
+ const source = Array.isArray(args?.plan) ? args.plan : Array.isArray(args?.todos) ? args.todos : [];
112
+ return source.flatMap((item) => {
113
+ if (typeof item === "string" && item.trim()) return [{ label: boundedString(item.trim(), 300), status: "" }];
114
+ const value = record(item);
115
+ const label = firstString(value, ["step", "title", "content", "text"]);
116
+ if (!label) return [];
117
+ return [{ label: boundedString(label, 300), status: firstString(value, ["status"]) }];
118
+ }).slice(0, 12);
119
+ }
120
+ function editPreview(args, resultText) {
121
+ const direct = firstString(args, ["patch", "diff"]);
122
+ if (direct) return direct;
123
+ const oldText = firstString(args, ["old_string"]);
124
+ const newText = firstString(args, ["new_string"]);
125
+ if (oldText || newText) {
126
+ return [
127
+ ...oldText.split("\n").map((line) => `- ${line}`),
128
+ ...newText.split("\n").map((line) => `+ ${line}`)
129
+ ].join("\n");
130
+ }
131
+ return /^(?:diff --git|@@ |--- |\+\+\+ )/m.test(resultText ?? "") ? resultText : "";
132
+ }
133
+ function createToolPresentation(entry) {
134
+ const args = argumentObject(entry.arguments);
135
+ const category = toolCategory({ label: entry.label, arguments: entry.arguments });
136
+ const command = firstString(args, ["command", "cmd"]);
137
+ const path = firstString(args, ["file_path", "target_file", "path"]);
138
+ const query = firstString(args, ["query", "pattern"]);
139
+ const url = firstString(args, ["url"]);
140
+ const subject = firstString(args, ["description", "task", "prompt"]);
141
+ const target = path || command || query || url || subject || toolTarget(entry.arguments);
142
+ const previewSource = category === "edit" ? editPreview(args, entry.resultText) : entry.resultText ?? "";
143
+ const result = record(entry.resultContent);
144
+ const metadata = record(entry.metadata);
145
+ return {
146
+ category,
147
+ detail: toolDetail(category),
148
+ target: boundedString(target, 300),
149
+ command: boundedString(command, MAX_TOOL_FIELD_CHARS),
150
+ path: boundedString(path, MAX_TOOL_FIELD_CHARS),
151
+ query: boundedString(query, MAX_TOOL_FIELD_CHARS),
152
+ url: boundedString(url, MAX_TOOL_FIELD_CHARS),
153
+ subject: boundedString(subject, MAX_TOOL_FIELD_CHARS),
154
+ preview: boundedString(previewSource, MAX_TOOL_PREVIEW_CHARS),
155
+ fields: usefulToolFields(args),
156
+ items: planItems(args),
157
+ exitCode: explicitNumber([result, metadata], ["exit_code", "exitCode"]),
158
+ durationMs: explicitNumber([result, metadata], ["duration_ms", "durationMs", "elapsed_ms", "elapsedMs"]),
159
+ additions: explicitNumber([result, metadata], ["additions", "lines_added"]),
160
+ deletions: explicitNumber([result, metadata], ["deletions", "lines_removed"]),
161
+ matches: explicitNumber([result, metadata], ["matches", "match_count", "result_count"])
162
+ };
163
+ }
62
164
  function harnessDisplayName(id) {
63
165
  return HARNESS_NAMES[id] ?? id;
64
166
  }
@@ -76,6 +178,7 @@ function groupConversation(entries) {
76
178
  return blocks;
77
179
  }
78
180
  function toolCategory(entry) {
181
+ if (TOOL_CATEGORIES.has(entry.presentation?.category)) return entry.presentation.category;
79
182
  const name = entry.label?.toLocaleLowerCase() ?? "";
80
183
  if (/read|view|open_file|list_dir/.test(name)) return "read";
81
184
  if (/search|find|grep|glob/.test(name)) return "search";
@@ -83,7 +186,8 @@ function toolCategory(entry) {
83
186
  if (/test|typecheck|lint|build/.test(name)) return "test";
84
187
  if (/terminal|bash|shell|command|exec/.test(name)) return /\b(test|typecheck|lint|build)\b/i.test(entry.arguments ?? "") ? "test" : "command";
85
188
  if (/browser|web|fetch|url/.test(name)) return "web";
86
- if (/subagent|spawn|task/.test(name)) return "agent";
189
+ if (/update.?plan|todo|checklist/.test(name)) return "plan";
190
+ if (/subagent|spawn.?agent|delegate|^task$/.test(name)) return "agent";
87
191
  return "other";
88
192
  }
89
193
  function toolTarget(argumentsText) {
@@ -212,6 +316,7 @@ var TOOL_ICONS = {
212
316
  /* @__PURE__ */ jsx2("circle", { cx: "9", cy: "6", r: "2.5" }),
213
317
  /* @__PURE__ */ jsx2("path", { d: "M4 15c.4-3 2-4.5 5-4.5s4.6 1.5 5 4.5" })
214
318
  ] }),
319
+ plan: () => /* @__PURE__ */ jsx2(Fragment, { children: /* @__PURE__ */ jsx2("path", { d: "m3 5 1 1 2-2M3 9l1 1 2-2M3 13l1 1 2-2M8 5h7M8 9h7M8 13h7" }) }),
215
320
  other: () => /* @__PURE__ */ jsxs(Fragment, { children: [
216
321
  /* @__PURE__ */ jsx2("path", { d: "M9 2.5v3M9 12.5v3M2.5 9h3M12.5 9h3" }),
217
322
  /* @__PURE__ */ jsx2("circle", { cx: "9", cy: "9", r: "3.5" })
@@ -230,7 +335,8 @@ function toolAction(entry, category) {
230
335
  command: ["Running command", "Ran command", "Command failed"],
231
336
  test: ["Running tests", "Ran tests", "Tests failed"],
232
337
  web: ["Browsing", "Browsed", "Browser action failed"],
233
- agent: ["Starting agent", "Started agent", "Agent failed"]
338
+ agent: ["Starting agent", "Started agent", "Agent failed"],
339
+ plan: ["Updating plan", "Updated plan", "Plan update failed"]
234
340
  };
235
341
  const position = status === "pending" ? 0 : status === "error" ? 2 : 1;
236
342
  if (actions[category]) return actions[category][position];
@@ -256,6 +362,113 @@ function argumentRows(argumentsText) {
256
362
  }
257
363
  return [{ key: "arguments", label: "Details", value: argumentsText }];
258
364
  }
365
+ function stripAnsi(value) {
366
+ return value?.replaceAll(/[\u001B\u009B][[\]()#;?]*(?:(?:(?:[a-zA-Z\d]*(?:;[-a-zA-Z\d\/#&.:=?%@~_]+)*)?\u0007)|(?:(?:\d{1,4}(?:[;:]\d{0,4})*)?[\dA-PR-TZcf-nq-uy=><~]))/g, "") ?? "";
367
+ }
368
+ function formatDuration(value) {
369
+ if (value === null) return "";
370
+ if (value < 1e3) return `${Math.round(value)}ms`;
371
+ return `${(value / 1e3).toFixed(value < 1e4 ? 1 : 0)}s`;
372
+ }
373
+ function ToolMetrics({ presentation }) {
374
+ const metrics = [
375
+ presentation.matches === null ? "" : `${presentation.matches} ${presentation.matches === 1 ? "match" : "matches"}`,
376
+ presentation.additions === null ? "" : `+${presentation.additions}`,
377
+ presentation.deletions === null ? "" : `\u2212${presentation.deletions}`,
378
+ presentation.exitCode === null ? "" : `exit ${presentation.exitCode}`,
379
+ formatDuration(presentation.durationMs)
380
+ ].filter(Boolean);
381
+ return metrics.length ? /* @__PURE__ */ jsx2("div", { class: "scui-tool-metrics", children: metrics.map((metric) => /* @__PURE__ */ jsx2("span", { children: metric }, metric)) }) : null;
382
+ }
383
+ function LinePreview({ value, kind }) {
384
+ if (!value) return null;
385
+ return /* @__PURE__ */ jsx2("ol", { class: "scui-code-preview", "data-kind": kind, children: stripAnsi(value).split("\n").map((line, index) => {
386
+ const tone = kind === "diff" ? line.startsWith("+") && !line.startsWith("+++") ? "add" : line.startsWith("-") && !line.startsWith("---") ? "remove" : line.startsWith("@@") ? "hunk" : "plain" : "plain";
387
+ return /* @__PURE__ */ jsxs("li", { "data-tone": tone, children: [
388
+ /* @__PURE__ */ jsx2("span", { children: index + 1 }),
389
+ /* @__PURE__ */ jsx2("code", { children: line || " " })
390
+ ] }, index);
391
+ }) });
392
+ }
393
+ function TerminalPreview({ presentation, pending, failed }) {
394
+ return /* @__PURE__ */ jsxs("section", { class: "scui-terminal", children: [
395
+ /* @__PURE__ */ jsxs("header", { children: [
396
+ /* @__PURE__ */ jsxs("span", { "aria-hidden": "true", children: [
397
+ /* @__PURE__ */ jsx2("i", {}),
398
+ /* @__PURE__ */ jsx2("i", {}),
399
+ /* @__PURE__ */ jsx2("i", {})
400
+ ] }),
401
+ /* @__PURE__ */ jsx2("code", { children: presentation.command ? `$ ${presentation.command}` : "Terminal" })
402
+ ] }),
403
+ presentation.preview ? /* @__PURE__ */ jsx2("pre", { "data-error": failed, children: stripAnsi(presentation.preview) }) : pending ? /* @__PURE__ */ jsxs("div", { class: "scui-terminal-wait", children: [
404
+ /* @__PURE__ */ jsx2("i", {}),
405
+ " Waiting for output"
406
+ ] }) : /* @__PURE__ */ jsx2("div", { class: "scui-terminal-empty", children: "No output" })
407
+ ] });
408
+ }
409
+ function SearchPreview({ presentation }) {
410
+ const lines = stripAnsi(presentation.preview).split("\n").filter(Boolean);
411
+ return /* @__PURE__ */ jsxs("section", { class: "scui-search-preview", children: [
412
+ presentation.query ? /* @__PURE__ */ jsxs("header", { children: [
413
+ /* @__PURE__ */ jsx2("span", { children: "Search" }),
414
+ /* @__PURE__ */ jsx2("code", { children: presentation.query })
415
+ ] }) : null,
416
+ lines.length ? /* @__PURE__ */ jsx2("ol", { children: lines.map((line, index) => {
417
+ const match = /^(.*?):(\d+)(?::(\d+))?:(.*)$/.exec(line);
418
+ return /* @__PURE__ */ jsx2("li", { children: match ? /* @__PURE__ */ jsxs(Fragment, { children: [
419
+ /* @__PURE__ */ jsx2("code", { children: match[1] }),
420
+ /* @__PURE__ */ jsxs("small", { children: [
421
+ match[2],
422
+ match[3] ? `:${match[3]}` : ""
423
+ ] }),
424
+ /* @__PURE__ */ jsx2("span", { children: match[4] })
425
+ ] }) : /* @__PURE__ */ jsx2("span", { children: line }) }, index);
426
+ }) }) : /* @__PURE__ */ jsx2("p", { children: "No textual results" })
427
+ ] });
428
+ }
429
+ function ToolPreview({ presentation, entry }) {
430
+ const pending = entry.status === "pending";
431
+ const failed = entry.status === "error";
432
+ if (presentation.detail === "terminal") return /* @__PURE__ */ jsx2(TerminalPreview, { presentation, pending, failed });
433
+ if (presentation.detail === "diff") return presentation.preview ? /* @__PURE__ */ jsx2(LinePreview, { value: presentation.preview, kind: "diff" }) : /* @__PURE__ */ jsx2("div", { class: "scui-tool-empty", children: "Edit completed without a textual diff" });
434
+ if (presentation.detail === "file") return presentation.preview ? /* @__PURE__ */ jsx2(LinePreview, { value: presentation.preview, kind: "file" }) : /* @__PURE__ */ jsx2("div", { class: "scui-tool-empty", children: "File contents were not included in this event" });
435
+ if (presentation.detail === "matches") return /* @__PURE__ */ jsx2(SearchPreview, { presentation });
436
+ if (presentation.detail === "web") return /* @__PURE__ */ jsxs("section", { class: "scui-web-preview", children: [
437
+ presentation.url ? /* @__PURE__ */ jsx2("code", { children: presentation.url }) : null,
438
+ presentation.preview ? /* @__PURE__ */ jsx2("p", { children: stripAnsi(presentation.preview) }) : /* @__PURE__ */ jsx2("small", { children: pending ? "Waiting for the page" : "No page summary returned" })
439
+ ] });
440
+ if (presentation.detail === "agent") return /* @__PURE__ */ jsxs("section", { class: "scui-agent-preview", children: [
441
+ presentation.subject ? /* @__PURE__ */ jsx2("p", { children: presentation.subject }) : null,
442
+ presentation.preview ? /* @__PURE__ */ jsx2("pre", { children: stripAnsi(presentation.preview) }) : /* @__PURE__ */ jsx2("small", { children: pending ? "Agent is working" : "No textual handoff returned" })
443
+ ] });
444
+ if (presentation.detail === "plan") return /* @__PURE__ */ jsx2("ol", { class: "scui-plan-preview", children: presentation.items?.map((item, index) => /* @__PURE__ */ jsxs("li", { "data-status": item.status, children: [
445
+ /* @__PURE__ */ jsx2("i", { "aria-hidden": "true" }),
446
+ /* @__PURE__ */ jsx2("span", { children: item.label })
447
+ ] }, `${item.label}:${index}`)) });
448
+ return presentation.preview ? /* @__PURE__ */ jsx2("pre", { class: "scui-tool-output", "data-error": failed, children: stripAnsi(presentation.preview) }) : null;
449
+ }
450
+ function TechnicalDetails({ entry }) {
451
+ if (!entry.arguments && !entry.resultText) return null;
452
+ return /* @__PURE__ */ jsxs("details", { class: "scui-tool-technical", children: [
453
+ /* @__PURE__ */ jsx2("summary", { children: "Technical details" }),
454
+ /* @__PURE__ */ jsxs("div", { children: [
455
+ entry.arguments ? /* @__PURE__ */ jsxs("section", { children: [
456
+ /* @__PURE__ */ jsx2("strong", { children: "Native arguments" }),
457
+ /* @__PURE__ */ jsx2("dl", { children: argumentRows(entry.arguments).map((row) => /* @__PURE__ */ jsxs("div", { children: [
458
+ /* @__PURE__ */ jsx2("dt", { children: row.label }),
459
+ /* @__PURE__ */ jsx2("dd", { children: /* @__PURE__ */ jsx2("pre", { children: row.value }) })
460
+ ] }, row.key)) })
461
+ ] }) : null,
462
+ entry.resultText ? /* @__PURE__ */ jsxs("section", { children: [
463
+ /* @__PURE__ */ jsx2("strong", { children: "Native result" }),
464
+ /* @__PURE__ */ jsxs("pre", { "data-error": entry.status === "error", children: [
465
+ entry.resultText,
466
+ entry.truncated ? "\n[truncated]" : ""
467
+ ] })
468
+ ] }) : null
469
+ ] })
470
+ ] });
471
+ }
259
472
  function TranscriptEntry({ entry, state, adapter }) {
260
473
  if (entry.role === "request") return /* @__PURE__ */ jsx2(RequestCard, { entry, adapter, canRespond: state.canRespond });
261
474
  if (entry.role === "reasoning") {
@@ -271,33 +484,30 @@ function TranscriptEntry({ entry, state, adapter }) {
271
484
  entry.truncated ? /* @__PURE__ */ jsx2("small", { class: "scui-truncated", children: "Entry truncated by host \xB7 load native session for the complete value" }) : null
272
485
  ] });
273
486
  }
274
- function ToolRow({ entry, workspace }) {
275
- const target = compactToolTarget(toolTarget(entry.arguments), workspace);
276
- const hasDetail = Boolean(entry.arguments || entry.resultText);
277
- const category = toolCategory(entry);
487
+ function ToolRow({ entry, workspace, open = false }) {
488
+ const presentation = entry.presentation ?? createToolPresentation(entry);
489
+ const target = compactToolTarget(presentation.target, workspace);
490
+ const hasDetail = Boolean(entry.arguments || entry.resultText || presentation.preview || presentation.fields.length);
491
+ const category = presentation.category ?? toolCategory(entry);
278
492
  const summary = /* @__PURE__ */ jsxs(Fragment, { children: [
279
493
  /* @__PURE__ */ jsx2(ToolIcon, { category }),
280
494
  /* @__PURE__ */ jsx2("strong", { children: toolAction(entry, category) }),
281
- target ? /* @__PURE__ */ jsx2("code", { class: "scui-tool-target", title: toolTarget(entry.arguments), children: target }) : null,
495
+ target ? /* @__PURE__ */ jsx2("code", { class: "scui-tool-target", title: presentation.target, children: target }) : null,
282
496
  /* @__PURE__ */ jsx2("span", { class: "scui-spacer" }),
283
497
  /* @__PURE__ */ jsx2("span", { class: "scui-tool-status", role: "status", "data-status": entry.status ?? "completed", "aria-label": entry.status ?? "completed", children: entry.status === "pending" ? /* @__PURE__ */ jsx2("i", {}) : entry.status === "error" ? "\xD7" : "\u2713" }),
284
498
  hasDetail ? /* @__PURE__ */ jsx2("span", { class: "scui-tool-chevron", "aria-hidden": "true", children: "\u203A" }) : null
285
499
  ] });
286
500
  if (!hasDetail) return /* @__PURE__ */ jsx2("div", { class: "scui-tool", "data-status": entry.status ?? "completed", "data-category": category, children: /* @__PURE__ */ jsx2("div", { class: "scui-tool-head", children: summary }) });
287
- return /* @__PURE__ */ jsxs("details", { class: "scui-tool", "data-status": entry.status ?? "completed", "data-category": category, children: [
501
+ return /* @__PURE__ */ jsxs("details", { class: "scui-tool", "data-status": entry.status ?? "completed", "data-category": category, open: open || entry.status === "pending", children: [
288
502
  /* @__PURE__ */ jsx2("summary", { class: "scui-tool-head", children: summary }),
289
503
  /* @__PURE__ */ jsxs("div", { class: "scui-tool-detail", children: [
290
- entry.arguments ? /* @__PURE__ */ jsx2("dl", { children: argumentRows(entry.arguments).map((row) => /* @__PURE__ */ jsxs("div", { children: [
291
- /* @__PURE__ */ jsx2("dt", { children: row.label }),
292
- /* @__PURE__ */ jsx2("dd", { children: /* @__PURE__ */ jsx2("pre", { children: row.value }) })
293
- ] }, row.key)) }) : null,
294
- entry.resultText ? /* @__PURE__ */ jsxs("section", { children: [
295
- /* @__PURE__ */ jsx2("strong", { children: entry.status === "error" ? "Error" : "Result" }),
296
- /* @__PURE__ */ jsxs("pre", { "data-error": entry.status === "error", children: [
297
- entry.resultText,
298
- entry.truncated ? "\n[truncated]" : ""
299
- ] })
300
- ] }) : null
504
+ /* @__PURE__ */ jsx2(ToolMetrics, { presentation }),
505
+ /* @__PURE__ */ jsx2(ToolPreview, { presentation, entry }),
506
+ presentation.fields.length ? /* @__PURE__ */ jsx2("dl", { class: "scui-tool-fields", children: presentation.fields.map((field) => /* @__PURE__ */ jsxs("div", { children: [
507
+ /* @__PURE__ */ jsx2("dt", { children: field.label }),
508
+ /* @__PURE__ */ jsx2("dd", { children: field.value })
509
+ ] }, field.label)) }) : null,
510
+ /* @__PURE__ */ jsx2(TechnicalDetails, { entry })
301
511
  ] })
302
512
  ] });
303
513
  }
@@ -462,5 +672,6 @@ export {
462
672
  RequestCard,
463
673
  SessionDetails,
464
674
  TaskPlan,
675
+ ToolRow,
465
676
  TranscriptEntry
466
677
  };
package/core.d.ts CHANGED
@@ -19,6 +19,8 @@ export type {
19
19
  TranscriptContext,
20
20
  TranscriptEntryModel,
21
21
  TranscriptRequest,
22
+ ToolCategory,
23
+ ToolPresentationModel,
22
24
  UiTone,
23
25
  } from './index.js';
24
26
  export {
@@ -27,6 +29,7 @@ export {
27
29
  activitySummary,
28
30
  canContinueHere,
29
31
  compactToolTarget,
32
+ createToolPresentation,
30
33
  filterSessions,
31
34
  groupConversation,
32
35
  harnessDisplayName,
package/core.mjs CHANGED
@@ -63,6 +63,11 @@ const MODES = new Set(['none', 'control', 'mirror']);
63
63
  const STRATEGIES = new Set(['start', 'resume', 'attach', 'branch', 'reduce']);
64
64
  const STARTUP = new Set(['connecting', 'starting', 'discovering', 'ready']);
65
65
  const FIDELITY = new Set(['byte_lossless', 'value_lossless', 'semantic']);
66
+ const TOOL_CATEGORIES = new Set(['read', 'search', 'edit', 'command', 'test', 'web', 'agent', 'plan', 'other']);
67
+ const TOOL_DETAILS = new Set(['file', 'matches', 'diff', 'terminal', 'web', 'agent', 'plan', 'fields']);
68
+ const MAX_TOOL_FIELDS = 8;
69
+ const MAX_TOOL_FIELD_CHARS = 800;
70
+ const MAX_TOOL_PREVIEW_CHARS = 4_000;
66
71
 
67
72
  function record(value) {
68
73
  return value !== null && typeof value === 'object' && !Array.isArray(value) ? value : null;
@@ -80,6 +85,146 @@ function nullableNumber(value) {
80
85
  return typeof value === 'number' && Number.isFinite(value) ? value : null;
81
86
  }
82
87
 
88
+ function boundedString(value, max) {
89
+ if (typeof value !== 'string') return '';
90
+ return value.length <= max ? value : `${value.slice(0, max)}…`;
91
+ }
92
+
93
+ function argumentObject(argumentsText) {
94
+ if (!argumentsText) return null;
95
+ try {
96
+ return record(JSON.parse(argumentsText));
97
+ } catch {
98
+ return null;
99
+ }
100
+ }
101
+
102
+ function firstString(source, keys) {
103
+ for (const key of keys) {
104
+ if (typeof source?.[key] === 'string' && source[key].trim()) return source[key].trim();
105
+ }
106
+ return '';
107
+ }
108
+
109
+ function explicitNumber(sources, keys) {
110
+ for (const source of sources) {
111
+ for (const key of keys) {
112
+ const value = source?.[key];
113
+ const parsed = typeof value === 'string' && value.trim() !== '' ? Number(value) : value;
114
+ if (typeof parsed === 'number' && Number.isFinite(parsed)) return parsed;
115
+ }
116
+ }
117
+ return null;
118
+ }
119
+
120
+ function toolDetail(category) {
121
+ return ({ read: 'file', search: 'matches', edit: 'diff', command: 'terminal', test: 'terminal', web: 'web', agent: 'agent', plan: 'plan' })[category] ?? 'fields';
122
+ }
123
+
124
+ function usefulToolFields(args) {
125
+ if (!args) return [];
126
+ const hidden = new Set(['command', 'cmd', 'file_path', 'target_file', 'path', 'query', 'pattern', 'url', 'patch', 'diff', 'old_string', 'new_string', 'content', 'prompt', 'description', 'task', 'plan', 'todos']);
127
+ return Object.entries(args).flatMap(([key, value]) => {
128
+ if (hidden.has(key) || value === null || value === undefined) return [];
129
+ const rendered = typeof value === 'string' ? value : JSON.stringify(value);
130
+ if (!rendered) return [];
131
+ return [{ label: key.replaceAll(/[_-]+/g, ' ').replace(/^./, (letter) => letter.toLocaleUpperCase()), value: boundedString(rendered, MAX_TOOL_FIELD_CHARS) }];
132
+ }).slice(0, MAX_TOOL_FIELDS);
133
+ }
134
+
135
+ function planItems(args) {
136
+ const source = Array.isArray(args?.plan) ? args.plan : Array.isArray(args?.todos) ? args.todos : [];
137
+ return source.flatMap((item) => {
138
+ if (typeof item === 'string' && item.trim()) return [{ label: boundedString(item.trim(), 300), status: '' }];
139
+ const value = record(item);
140
+ const label = firstString(value, ['step', 'title', 'content', 'text']);
141
+ if (!label) return [];
142
+ return [{ label: boundedString(label, 300), status: firstString(value, ['status']) }];
143
+ }).slice(0, 12);
144
+ }
145
+
146
+ function editPreview(args, resultText) {
147
+ const direct = firstString(args, ['patch', 'diff']);
148
+ if (direct) return direct;
149
+ const oldText = firstString(args, ['old_string']);
150
+ const newText = firstString(args, ['new_string']);
151
+ if (oldText || newText) {
152
+ return [
153
+ ...oldText.split('\n').map((line) => `- ${line}`),
154
+ ...newText.split('\n').map((line) => `+ ${line}`),
155
+ ].join('\n');
156
+ }
157
+ return /^(?:diff --git|@@ |--- |\+\+\+ )/m.test(resultText ?? '') ? resultText : '';
158
+ }
159
+
160
+ export function createToolPresentation(entry) {
161
+ const args = argumentObject(entry.arguments);
162
+ const category = toolCategory({ label: entry.label, arguments: entry.arguments });
163
+ const command = firstString(args, ['command', 'cmd']);
164
+ const path = firstString(args, ['file_path', 'target_file', 'path']);
165
+ const query = firstString(args, ['query', 'pattern']);
166
+ const url = firstString(args, ['url']);
167
+ const subject = firstString(args, ['description', 'task', 'prompt']);
168
+ const target = path || command || query || url || subject || toolTarget(entry.arguments);
169
+ const previewSource = category === 'edit' ? editPreview(args, entry.resultText) : (entry.resultText ?? '');
170
+ const result = record(entry.resultContent);
171
+ const metadata = record(entry.metadata);
172
+ return {
173
+ category,
174
+ detail: toolDetail(category),
175
+ target: boundedString(target, 300),
176
+ command: boundedString(command, MAX_TOOL_FIELD_CHARS),
177
+ path: boundedString(path, MAX_TOOL_FIELD_CHARS),
178
+ query: boundedString(query, MAX_TOOL_FIELD_CHARS),
179
+ url: boundedString(url, MAX_TOOL_FIELD_CHARS),
180
+ subject: boundedString(subject, MAX_TOOL_FIELD_CHARS),
181
+ preview: boundedString(previewSource, MAX_TOOL_PREVIEW_CHARS),
182
+ fields: usefulToolFields(args),
183
+ items: planItems(args),
184
+ exitCode: explicitNumber([result, metadata], ['exit_code', 'exitCode']),
185
+ durationMs: explicitNumber([result, metadata], ['duration_ms', 'durationMs', 'elapsed_ms', 'elapsedMs']),
186
+ additions: explicitNumber([result, metadata], ['additions', 'lines_added']),
187
+ deletions: explicitNumber([result, metadata], ['deletions', 'lines_removed']),
188
+ matches: explicitNumber([result, metadata], ['matches', 'match_count', 'result_count']),
189
+ };
190
+ }
191
+
192
+ function readToolPresentation(value, entry) {
193
+ const generated = createToolPresentation(entry);
194
+ const item = record(value);
195
+ if (!item) return generated;
196
+ const fields = Array.isArray(item.fields) ? item.fields.flatMap((raw) => {
197
+ const field = record(raw);
198
+ return field && typeof field.label === 'string' && typeof field.value === 'string'
199
+ ? [{ label: boundedString(field.label, 80), value: boundedString(field.value, MAX_TOOL_FIELD_CHARS) }]
200
+ : [];
201
+ }).slice(0, MAX_TOOL_FIELDS) : generated.fields;
202
+ const items = Array.isArray(item.items) ? item.items.flatMap((raw) => {
203
+ const planItem = record(raw);
204
+ return planItem && typeof planItem.label === 'string'
205
+ ? [{ label: boundedString(planItem.label, 300), status: boundedString(planItem.status, 40) }]
206
+ : [];
207
+ }).slice(0, 12) : generated.items;
208
+ return {
209
+ category: TOOL_CATEGORIES.has(item.category) ? item.category : generated.category,
210
+ detail: TOOL_DETAILS.has(item.detail) ? item.detail : generated.detail,
211
+ target: boundedString(item.target, 300) || generated.target,
212
+ command: boundedString(item.command, MAX_TOOL_FIELD_CHARS) || generated.command,
213
+ path: boundedString(item.path, MAX_TOOL_FIELD_CHARS) || generated.path,
214
+ query: boundedString(item.query, MAX_TOOL_FIELD_CHARS) || generated.query,
215
+ url: boundedString(item.url, MAX_TOOL_FIELD_CHARS) || generated.url,
216
+ subject: boundedString(item.subject, MAX_TOOL_FIELD_CHARS) || generated.subject,
217
+ preview: boundedString(item.preview, MAX_TOOL_PREVIEW_CHARS) || generated.preview,
218
+ fields,
219
+ items,
220
+ exitCode: nullableNumber(item.exitCode) ?? generated.exitCode,
221
+ durationMs: nullableNumber(item.durationMs) ?? generated.durationMs,
222
+ additions: nullableNumber(item.additions) ?? generated.additions,
223
+ deletions: nullableNumber(item.deletions) ?? generated.deletions,
224
+ matches: nullableNumber(item.matches) ?? generated.matches,
225
+ };
226
+ }
227
+
83
228
  function readTranscript(value) {
84
229
  if (!Array.isArray(value)) return [];
85
230
  const result = [];
@@ -97,6 +242,7 @@ function readTranscript(value) {
97
242
  if (typeof item[key] === 'string') entry[key] = item[key];
98
243
  }
99
244
  if (['pending', 'completed', 'error'].includes(item.status)) entry.status = item.status;
245
+ if (item.role === 'tool') entry.presentation = readToolPresentation(item.presentation, entry);
100
246
  if (typeof item.streaming === 'boolean') entry.streaming = item.streaming;
101
247
  if (Array.isArray(item.context)) {
102
248
  entry.context = item.context.flatMap((raw) => {
@@ -358,6 +504,7 @@ export function groupConversation(entries) {
358
504
  }
359
505
 
360
506
  export function toolCategory(entry) {
507
+ if (TOOL_CATEGORIES.has(entry.presentation?.category)) return entry.presentation.category;
361
508
  const name = entry.label?.toLocaleLowerCase() ?? '';
362
509
  if (/read|view|open_file|list_dir/.test(name)) return 'read';
363
510
  if (/search|find|grep|glob/.test(name)) return 'search';
@@ -365,7 +512,8 @@ export function toolCategory(entry) {
365
512
  if (/test|typecheck|lint|build/.test(name)) return 'test';
366
513
  if (/terminal|bash|shell|command|exec/.test(name)) return /\b(test|typecheck|lint|build)\b/i.test(entry.arguments ?? '') ? 'test' : 'command';
367
514
  if (/browser|web|fetch|url/.test(name)) return 'web';
368
- if (/subagent|spawn|task/.test(name)) return 'agent';
515
+ if (/update.?plan|todo|checklist/.test(name)) return 'plan';
516
+ if (/subagent|spawn.?agent|delegate|^task$/.test(name)) return 'agent';
369
517
  return 'other';
370
518
  }
371
519