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

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/components.mjs CHANGED
@@ -85,10 +85,10 @@ function boundedString(value, max) {
85
85
  if (typeof value !== "string") return "";
86
86
  return value.length <= max ? value : `${value.slice(0, max)}\u2026`;
87
87
  }
88
- function argumentObject(argumentsText) {
88
+ function argumentValue(argumentsText) {
89
89
  if (!argumentsText) return null;
90
90
  try {
91
- return record(JSON.parse(argumentsText));
91
+ return JSON.parse(argumentsText);
92
92
  } catch {
93
93
  return null;
94
94
  }
@@ -99,6 +99,33 @@ function firstString(source, keys) {
99
99
  }
100
100
  return "";
101
101
  }
102
+ function decodedLiteral(value) {
103
+ if (!value) return "";
104
+ if (value.startsWith('"')) {
105
+ try {
106
+ return JSON.parse(value);
107
+ } catch {
108
+ return "";
109
+ }
110
+ }
111
+ return value.slice(1, -1).replaceAll("\\n", "\n").replaceAll("\\t", " ").replaceAll("\\r", "\r").replaceAll("\\`", "`").replaceAll("\\'", "'").replaceAll("\\\\", "\\");
112
+ }
113
+ function sourceString(source, keys) {
114
+ if (!source) return "";
115
+ const names = keys.join("|");
116
+ const match = new RegExp(`\\b(?:${names})\\s*:\\s*("(?:\\\\.|[^"\\\\])*"|'(?:\\\\.|[^'\\\\])*'|\`(?:\\\\.|[^\`\\\\])*\`)`).exec(source);
117
+ return decodedLiteral(match?.[1]);
118
+ }
119
+ function toolEnvelope(entry) {
120
+ const value = argumentValue(entry.arguments);
121
+ const source = typeof value === "string" ? value : "";
122
+ const tools = source ? [...new Set([...source.matchAll(/\btools\.([A-Za-z0-9_]+)/g)].map((match) => match[1]))].slice(0, 8) : [];
123
+ const name = tools.length === 1 ? tools[0] : entry.label ?? "tool";
124
+ return { args: record(value), source, tools, name };
125
+ }
126
+ function patchPath(source) {
127
+ return /\*\*\* (?:Update|Add|Delete) File:\s*([^\r\n]+)/.exec(source)?.[1]?.trim() ?? "";
128
+ }
102
129
  function explicitNumber(sources, keys) {
103
130
  for (const source of sources) {
104
131
  for (const key of keys) {
@@ -132,7 +159,7 @@ function planItems(args) {
132
159
  return [{ label: boundedString(label, 300), status: firstString(value, ["status"]) }];
133
160
  }).slice(0, 12);
134
161
  }
135
- function editPreview(args, resultText) {
162
+ function editPreview(args, resultText, source) {
136
163
  const direct = firstString(args, ["patch", "diff"]);
137
164
  if (direct) return direct;
138
165
  const oldText = firstString(args, ["old_string"]);
@@ -143,21 +170,63 @@ function editPreview(args, resultText) {
143
170
  ...newText.split("\n").map((line) => `+ ${line}`)
144
171
  ].join("\n");
145
172
  }
173
+ const patch = /\*\*\* Begin Patch[\s\S]*?\*\*\* End Patch/.exec(source ?? "")?.[0];
174
+ if (patch) return patch;
146
175
  return /^(?:diff --git|@@ |--- |\+\+\+ )/m.test(resultText ?? "") ? resultText : "";
147
176
  }
177
+ function classifyTool(name, command) {
178
+ const normalized = name.toLocaleLowerCase();
179
+ if (/write_stdin|^wait$/.test(normalized)) return "command";
180
+ if (/update.?plan|todo|checklist|taskcreate|taskupdate/.test(normalized)) return "plan";
181
+ if (/search.?replace|edit|write|patch|replace|create_file|apply_patch/.test(normalized)) return "edit";
182
+ if (/read|view|open_file|list_dir/.test(normalized)) return "read";
183
+ if (/search|find|grep|glob|toolsearch/.test(normalized)) return "search";
184
+ if (/browser|web|fetch|url/.test(normalized)) return "web";
185
+ if (/agent|subagent|sendmessage|delegate|^task$/.test(normalized)) return "agent";
186
+ if (/test|typecheck|lint|build/.test(normalized)) return "test";
187
+ if (/terminal|bash|shell|command|exec|write_stdin|^wait$/.test(normalized)) return /\b(test|typecheck|lint|build)\b/i.test(command) ? "test" : "command";
188
+ return "other";
189
+ }
190
+ function toolAction(status, category, name, tools) {
191
+ const position = status === "pending" ? 0 : status === "error" ? 2 : 1;
192
+ if (tools.length > 1) return [`Running ${tools.length} actions`, `Ran ${tools.length} actions`, `${tools.length} actions failed`][position];
193
+ const normalized = name.toLocaleLowerCase();
194
+ if (/sendmessage/.test(normalized)) return ["Messaging agent", "Messaged agent", "Agent message failed"][position];
195
+ if (/kill_command_or_subagent/.test(normalized)) return ["Stopping", "Stopped", "Stop failed"][position];
196
+ if (/get_command_or_subagent_output|write_stdin|^wait$/.test(normalized)) return ["Waiting for", "Checked", "Check failed"][position];
197
+ const actions = {
198
+ read: ["Reading", "Read", "Read failed"],
199
+ search: ["Searching", "Searched", "Search failed"],
200
+ edit: ["Editing", "Edited", "Edit failed"],
201
+ command: ["Running command", "Ran command", "Command failed"],
202
+ test: ["Running tests", "Ran tests", "Tests failed"],
203
+ web: ["Browsing", "Browsed", "Browser action failed"],
204
+ agent: ["Starting agent", "Started agent", "Agent failed"],
205
+ plan: ["Updating plan", "Updated plan", "Plan update failed"]
206
+ };
207
+ return actions[category]?.[position] ?? (status === "error" ? `${name} failed` : name.replaceAll(/[_-]+/g, " "));
208
+ }
148
209
  function createToolPresentation(entry) {
149
- const args = argumentObject(entry.arguments);
150
- const category = toolCategory({ label: entry.label, arguments: entry.arguments });
151
- const command = firstString(args, ["command", "cmd"]);
152
- const path = firstString(args, ["file_path", "target_file", "path"]);
153
- const query = firstString(args, ["query", "pattern"]);
154
- const url = firstString(args, ["url"]);
155
- const subject = firstString(args, ["description", "task", "prompt"]);
156
- const target = path || command || query || url || subject || toolTarget(entry.arguments);
157
- const previewSource = category === "edit" ? editPreview(args, entry.resultText) : entry.resultText ?? "";
210
+ const envelope = toolEnvelope(entry);
211
+ const args = envelope.args;
212
+ const command = firstString(args, ["command", "cmd"]) || sourceString(envelope.source, ["command", "cmd"]);
213
+ const category = classifyTool(envelope.name, command || envelope.source || entry.arguments || "");
214
+ const path = firstString(args, ["file_path", "target_file", "target_directory", "path"]) || sourceString(envelope.source, ["file_path", "target_file", "target_directory", "path"]) || patchPath(envelope.source);
215
+ const query = firstString(args, ["query", "pattern"]) || sourceString(envelope.source, ["query", "pattern", "q"]);
216
+ const url = firstString(args, ["url"]) || sourceString(envelope.source, ["url", "ref_id"]);
217
+ const subject = firstString(args, ["subject", "description", "summary", "task", "prompt"]) || sourceString(envelope.source, ["subject", "description", "summary", "task", "prompt"]);
218
+ const background = /get_command_or_subagent_output|kill_command_or_subagent/i.test(envelope.name) ? "background task" : /write_stdin|^wait$/i.test(envelope.name) ? "background command" : "";
219
+ const items = planItems(args);
220
+ const taskId = firstString(args, ["taskId", "task_id"]);
221
+ const planTarget = category === "plan" ? items.length ? `${items.length} ${items.length === 1 ? "item" : "items"}` : taskId ? `task ${taskId}` : "" : "";
222
+ const target = path || command || query || url || subject || background || planTarget || (envelope.tools.length > 1 ? `${envelope.tools.length} coordinated actions` : toolTarget(entry.arguments));
223
+ const previewSource = category === "edit" ? editPreview(args, entry.resultText, envelope.source) : entry.resultText ?? "";
158
224
  const result = record(entry.resultContent);
159
225
  const metadata = record(entry.metadata);
226
+ const resultMetadata = record(result?.metadata);
160
227
  return {
228
+ name: boundedString(envelope.name, 120),
229
+ action: boundedString(toolAction(entry.status ?? "completed", category, envelope.name, envelope.tools), 120),
161
230
  category,
162
231
  detail: toolDetail(category),
163
232
  target: boundedString(target, 300),
@@ -168,12 +237,13 @@ function createToolPresentation(entry) {
168
237
  subject: boundedString(subject, MAX_TOOL_FIELD_CHARS),
169
238
  preview: boundedString(previewSource, MAX_TOOL_PREVIEW_CHARS),
170
239
  fields: usefulToolFields(args),
171
- items: planItems(args),
172
- exitCode: explicitNumber([result, metadata], ["exit_code", "exitCode"]),
173
- durationMs: explicitNumber([result, metadata], ["duration_ms", "durationMs", "elapsed_ms", "elapsedMs"]),
174
- additions: explicitNumber([result, metadata], ["additions", "lines_added"]),
175
- deletions: explicitNumber([result, metadata], ["deletions", "lines_removed"]),
176
- matches: explicitNumber([result, metadata], ["matches", "match_count", "result_count"])
240
+ items,
241
+ tools: envelope.tools,
242
+ exitCode: explicitNumber([result, resultMetadata, metadata], ["exit_code", "exitCode", "pi_bash_exit_code"]),
243
+ durationMs: explicitNumber([result, resultMetadata, metadata], ["duration_ms", "durationMs", "elapsed_ms", "elapsedMs", "totalDurationMs"]),
244
+ additions: explicitNumber([result, resultMetadata, metadata], ["additions", "lines_added"]),
245
+ deletions: explicitNumber([result, resultMetadata, metadata], ["deletions", "lines_removed"]),
246
+ matches: explicitNumber([result, resultMetadata, metadata], ["matches", "match_count", "result_count"])
177
247
  };
178
248
  }
179
249
  function readToolPresentation(value, entry) {
@@ -188,7 +258,10 @@ function readToolPresentation(value, entry) {
188
258
  const planItem = record(raw);
189
259
  return planItem && typeof planItem.label === "string" ? [{ label: boundedString(planItem.label, 300), status: boundedString(planItem.status, 40) }] : [];
190
260
  }).slice(0, 12) : generated.items;
261
+ const tools = Array.isArray(item.tools) ? item.tools.filter((tool) => typeof tool === "string").map((tool) => boundedString(tool, 120)).slice(0, 8) : generated.tools;
191
262
  return {
263
+ name: boundedString(item.name, 120) || generated.name,
264
+ action: boundedString(item.action, 120) || generated.action,
192
265
  category: TOOL_CATEGORIES.has(item.category) ? item.category : generated.category,
193
266
  detail: TOOL_DETAILS.has(item.detail) ? item.detail : generated.detail,
194
267
  target: boundedString(item.target, 300) || generated.target,
@@ -200,6 +273,7 @@ function readToolPresentation(value, entry) {
200
273
  preview: boundedString(item.preview, MAX_TOOL_PREVIEW_CHARS) || generated.preview,
201
274
  fields,
202
275
  items,
276
+ tools,
203
277
  exitCode: nullableNumber(item.exitCode) ?? generated.exitCode,
204
278
  durationMs: nullableNumber(item.durationMs) ?? generated.durationMs,
205
279
  additions: nullableNumber(item.additions) ?? generated.additions,
@@ -433,16 +507,9 @@ function groupConversation(entries) {
433
507
  }
434
508
  function toolCategory(entry) {
435
509
  if (TOOL_CATEGORIES.has(entry.presentation?.category)) return entry.presentation.category;
436
- const name = entry.label?.toLocaleLowerCase() ?? "";
437
- if (/read|view|open_file|list_dir/.test(name)) return "read";
438
- if (/search|find|grep|glob/.test(name)) return "search";
439
- if (/edit|write|patch|replace|create_file/.test(name)) return "edit";
440
- if (/test|typecheck|lint|build/.test(name)) return "test";
441
- if (/terminal|bash|shell|command|exec/.test(name)) return /\b(test|typecheck|lint|build)\b/i.test(entry.arguments ?? "") ? "test" : "command";
442
- if (/browser|web|fetch|url/.test(name)) return "web";
443
- if (/update.?plan|todo|checklist/.test(name)) return "plan";
444
- if (/subagent|spawn.?agent|delegate|^task$/.test(name)) return "agent";
445
- return "other";
510
+ const envelope = toolEnvelope(entry);
511
+ const command = firstString(envelope.args, ["command", "cmd"]) || sourceString(envelope.source, ["command", "cmd"]);
512
+ return classifyTool(envelope.name, command || envelope.source || entry.arguments || "");
446
513
  }
447
514
  function toolTarget(argumentsText) {
448
515
  if (!argumentsText) return "";
@@ -468,7 +535,8 @@ function activitySummary(entries) {
468
535
  if (pending) return `${entries.length} actions in progress`;
469
536
  if (failed) return `${entries.length} actions \xB7 ${failed} failed`;
470
537
  if ([...counts.keys()].every((key) => key === "read" || key === "search")) return `Explored ${entries.length} ${entries.length === 1 ? "item" : "items"}`;
471
- return `${entries.length} actions`;
538
+ const nouns = { read: ["read", "reads"], search: ["search", "searches"], edit: ["edit", "edits"], command: ["command", "commands"], test: ["test run", "test runs"], web: ["web action", "web actions"], agent: ["agent action", "agent actions"], plan: ["plan update", "plan updates"], other: ["action", "actions"] };
539
+ return [...counts.entries()].sort((a, b) => b[1] - a[1]).slice(0, 3).map(([category, count]) => `${count} ${nouns[category][count === 1 ? 0 : 1]}`).join(" \xB7 ");
472
540
  }
473
541
  function canContinueHere(state) {
474
542
  if (state.mode !== "mirror" || state.canSend) return false;
@@ -688,7 +756,8 @@ function ToolIcon({ category }) {
688
756
  const Glyph = TOOL_ICONS[category] ?? TOOL_ICONS.other;
689
757
  return /* @__PURE__ */ jsx3("svg", { class: "scui-tool-icon", viewBox: "0 0 18 18", fill: "none", stroke: "currentColor", "stroke-width": "1.35", "stroke-linecap": "round", "stroke-linejoin": "round", "aria-hidden": "true", children: /* @__PURE__ */ jsx3(Glyph, {}) });
690
758
  }
691
- function toolAction(entry, category) {
759
+ function toolAction2(entry, category, presentation) {
760
+ if (presentation?.action) return presentation.action;
692
761
  const status = entry.status ?? "completed";
693
762
  const actions = {
694
763
  read: ["Reading", "Read", "Read failed"],
@@ -742,6 +811,16 @@ function ToolMetrics({ presentation }) {
742
811
  ].filter(Boolean);
743
812
  return metrics.length ? /* @__PURE__ */ jsx3("div", { class: "scui-tool-metrics", children: metrics.map((metric) => /* @__PURE__ */ jsx3("span", { children: metric }, metric)) }) : null;
744
813
  }
814
+ function PendingElapsed({ now }) {
815
+ const clock = now ?? Date.now;
816
+ const started = useRef2(clock());
817
+ const [elapsed, setElapsed] = useState2(0);
818
+ useEffect2(() => {
819
+ const timer = setInterval(() => setElapsed(Math.max(0, clock() - started.current)), 1e3);
820
+ return () => clearInterval(timer);
821
+ }, [clock]);
822
+ return /* @__PURE__ */ jsx3("small", { class: "scui-tool-elapsed", children: elapsed < 1e3 ? "now" : formatDuration(elapsed) });
823
+ }
745
824
  function LinePreview({ value, kind }) {
746
825
  if (!value) return null;
747
826
  return /* @__PURE__ */ jsx3("ol", { class: "scui-code-preview", "data-kind": kind, children: stripAnsi(value).split("\n").map((line, index) => {
@@ -799,16 +878,31 @@ function ToolPreview({ presentation, entry }) {
799
878
  presentation.url ? /* @__PURE__ */ jsx3("code", { children: presentation.url }) : null,
800
879
  presentation.preview ? /* @__PURE__ */ jsx3("p", { children: stripAnsi(presentation.preview) }) : /* @__PURE__ */ jsx3("small", { children: pending ? "Waiting for the page" : "No page summary returned" })
801
880
  ] });
802
- if (presentation.detail === "agent") return /* @__PURE__ */ jsxs2("section", { class: "scui-agent-preview", children: [
803
- presentation.subject ? /* @__PURE__ */ jsx3("p", { children: presentation.subject }) : null,
804
- presentation.preview ? /* @__PURE__ */ jsx3("pre", { children: stripAnsi(presentation.preview) }) : /* @__PURE__ */ jsx3("small", { children: pending ? "Agent is working" : "No textual handoff returned" })
805
- ] });
881
+ if (presentation.detail === "agent") return /* @__PURE__ */ jsx3("section", { class: "scui-agent-preview", children: presentation.preview ? /* @__PURE__ */ jsx3("pre", { children: stripAnsi(presentation.preview) }) : /* @__PURE__ */ jsx3("small", { children: pending ? "Agent is working" : "No textual handoff returned" }) });
806
882
  if (presentation.detail === "plan") return /* @__PURE__ */ jsx3("ol", { class: "scui-plan-preview", children: presentation.items?.map((item, index) => /* @__PURE__ */ jsxs2("li", { "data-status": item.status, children: [
807
883
  /* @__PURE__ */ jsx3("i", { "aria-hidden": "true" }),
808
884
  /* @__PURE__ */ jsx3("span", { children: item.label })
809
885
  ] }, `${item.label}:${index}`)) });
810
886
  return presentation.preview ? /* @__PURE__ */ jsx3("pre", { class: "scui-tool-output", "data-error": failed, children: stripAnsi(presentation.preview) }) : null;
811
887
  }
888
+ function ToolActions({ presentation, adapter }) {
889
+ const [copied, setCopied] = useState2(false);
890
+ const reset = useRef2(null);
891
+ useEffect2(() => () => clearTimeout(reset.current), []);
892
+ if (!adapter?.copyText) return null;
893
+ const action = presentation.command ? ["Copy command", presentation.command] : presentation.path ? ["Copy path", presentation.path] : presentation.url ? ["Copy URL", presentation.url] : presentation.query ? ["Copy query", presentation.query] : null;
894
+ const copy = async () => {
895
+ await adapter.copyText(action[1]);
896
+ setCopied(true);
897
+ clearTimeout(reset.current);
898
+ reset.current = setTimeout(() => setCopied(false), 1500);
899
+ };
900
+ return action ? /* @__PURE__ */ jsx3("div", { class: "scui-tool-actions", children: /* @__PURE__ */ jsx3("button", { type: "button", onClick: copy, children: copied ? "Copied" : action[0] }) }) : null;
901
+ }
902
+ function ToolStack({ tools }) {
903
+ if (tools.length < 2) return null;
904
+ return /* @__PURE__ */ jsx3("div", { class: "scui-tool-stack", "aria-label": "Coordinated tools", children: tools.map((tool) => /* @__PURE__ */ jsx3("span", { children: tool.replaceAll("__", " \xB7 ").replaceAll("_", " ") }, tool)) });
905
+ }
812
906
  function TechnicalDetails({ entry }) {
813
907
  if (!entry.arguments && !entry.resultText) return null;
814
908
  return /* @__PURE__ */ jsxs2("details", { class: "scui-tool-technical", children: [
@@ -846,35 +940,42 @@ function TranscriptEntry({ entry, state, adapter }) {
846
940
  entry.truncated ? /* @__PURE__ */ jsx3("small", { class: "scui-truncated", children: "Entry truncated by host \xB7 load native session for the complete value" }) : null
847
941
  ] });
848
942
  }
849
- function ToolRow({ entry, workspace, open = false }) {
943
+ function ToolRow({ entry, workspace, open = false, adapter }) {
850
944
  const presentation = entry.presentation ?? createToolPresentation(entry);
945
+ const [expanded, setExpanded] = useState2(open || entry.status === "pending");
946
+ useEffect2(() => {
947
+ if (entry.status === "pending") setExpanded(true);
948
+ }, [entry.status]);
851
949
  const target = compactToolTarget(presentation.target, workspace);
852
950
  const hasDetail = Boolean(entry.arguments || entry.resultText || presentation.preview || presentation.fields.length);
853
951
  const category = presentation.category ?? toolCategory(entry);
854
952
  const summary = /* @__PURE__ */ jsxs2(Fragment, { children: [
855
953
  /* @__PURE__ */ jsx3(ToolIcon, { category }),
856
- /* @__PURE__ */ jsx3("strong", { children: toolAction(entry, category) }),
954
+ /* @__PURE__ */ jsx3("strong", { children: toolAction2(entry, category, presentation) }),
857
955
  target ? /* @__PURE__ */ jsx3("code", { class: "scui-tool-target", title: presentation.target, children: target }) : null,
858
956
  /* @__PURE__ */ jsx3("span", { class: "scui-spacer" }),
957
+ entry.status === "pending" ? /* @__PURE__ */ jsx3(PendingElapsed, { now: adapter?.now }) : null,
859
958
  /* @__PURE__ */ jsx3("span", { class: "scui-tool-status", role: "status", "data-status": entry.status ?? "completed", "aria-label": entry.status ?? "completed", children: entry.status === "pending" ? /* @__PURE__ */ jsx3("i", {}) : entry.status === "error" ? "\xD7" : "\u2713" }),
860
959
  hasDetail ? /* @__PURE__ */ jsx3("span", { class: "scui-tool-chevron", "aria-hidden": "true", children: "\u203A" }) : null
861
960
  ] });
862
961
  if (!hasDetail) return /* @__PURE__ */ jsx3("div", { class: "scui-tool", "data-status": entry.status ?? "completed", "data-category": category, children: /* @__PURE__ */ jsx3("div", { class: "scui-tool-head", children: summary }) });
863
- return /* @__PURE__ */ jsxs2("details", { class: "scui-tool", "data-status": entry.status ?? "completed", "data-category": category, open: open || entry.status === "pending", children: [
962
+ return /* @__PURE__ */ jsxs2("details", { class: "scui-tool", "data-status": entry.status ?? "completed", "data-category": category, open: expanded, onToggle: (event) => setExpanded(event.currentTarget.open), children: [
864
963
  /* @__PURE__ */ jsx3("summary", { class: "scui-tool-head", children: summary }),
865
964
  /* @__PURE__ */ jsxs2("div", { class: "scui-tool-detail", children: [
866
965
  /* @__PURE__ */ jsx3(ToolMetrics, { presentation }),
966
+ /* @__PURE__ */ jsx3(ToolStack, { tools: presentation.tools ?? [] }),
867
967
  /* @__PURE__ */ jsx3(ToolPreview, { presentation, entry }),
868
968
  presentation.fields.length ? /* @__PURE__ */ jsx3("dl", { class: "scui-tool-fields", children: presentation.fields.map((field) => /* @__PURE__ */ jsxs2("div", { children: [
869
969
  /* @__PURE__ */ jsx3("dt", { children: field.label }),
870
970
  /* @__PURE__ */ jsx3("dd", { children: field.value })
871
971
  ] }, field.label)) }) : null,
972
+ /* @__PURE__ */ jsx3(ToolActions, { presentation, adapter }),
872
973
  /* @__PURE__ */ jsx3(TechnicalDetails, { entry })
873
974
  ] })
874
975
  ] });
875
976
  }
876
- function ActivityGroup({ entries, state }) {
877
- if (entries.length === 1) return /* @__PURE__ */ jsx3("section", { class: "scui-activity", "data-single": "true", children: /* @__PURE__ */ jsx3(ToolRow, { entry: entries[0], workspace: state.workspace }) });
977
+ function ActivityGroup({ entries, state, adapter }) {
978
+ if (entries.length === 1) return /* @__PURE__ */ jsx3("section", { class: "scui-activity", "data-single": "true", children: /* @__PURE__ */ jsx3(ToolRow, { entry: entries[0], workspace: state.workspace, adapter }) });
878
979
  const active = entries.some((entry) => entry.status === "pending");
879
980
  const [open, setOpen] = useState2(active);
880
981
  const id = useId();
@@ -892,7 +993,7 @@ function ActivityGroup({ entries, state }) {
892
993
  entries.length
893
994
  ] })
894
995
  ] }),
895
- open ? /* @__PURE__ */ jsx3("div", { id, children: entries.map((entry) => /* @__PURE__ */ jsx3(ToolRow, { entry, workspace: state.workspace }, entry.id)) }) : null
996
+ open ? /* @__PURE__ */ jsx3("div", { id, children: entries.map((entry) => /* @__PURE__ */ jsx3(ToolRow, { entry, workspace: state.workspace, adapter }, entry.id)) }) : null
896
997
  ] });
897
998
  }
898
999
  function TaskPlan({ plan }) {
package/conversation.mjs CHANGED
@@ -70,10 +70,10 @@ function boundedString(value, max) {
70
70
  if (typeof value !== "string") return "";
71
71
  return value.length <= max ? value : `${value.slice(0, max)}\u2026`;
72
72
  }
73
- function argumentObject(argumentsText) {
73
+ function argumentValue(argumentsText) {
74
74
  if (!argumentsText) return null;
75
75
  try {
76
- return record(JSON.parse(argumentsText));
76
+ return JSON.parse(argumentsText);
77
77
  } catch {
78
78
  return null;
79
79
  }
@@ -84,6 +84,33 @@ function firstString(source, keys) {
84
84
  }
85
85
  return "";
86
86
  }
87
+ function decodedLiteral(value) {
88
+ if (!value) return "";
89
+ if (value.startsWith('"')) {
90
+ try {
91
+ return JSON.parse(value);
92
+ } catch {
93
+ return "";
94
+ }
95
+ }
96
+ return value.slice(1, -1).replaceAll("\\n", "\n").replaceAll("\\t", " ").replaceAll("\\r", "\r").replaceAll("\\`", "`").replaceAll("\\'", "'").replaceAll("\\\\", "\\");
97
+ }
98
+ function sourceString(source, keys) {
99
+ if (!source) return "";
100
+ const names = keys.join("|");
101
+ const match = new RegExp(`\\b(?:${names})\\s*:\\s*("(?:\\\\.|[^"\\\\])*"|'(?:\\\\.|[^'\\\\])*'|\`(?:\\\\.|[^\`\\\\])*\`)`).exec(source);
102
+ return decodedLiteral(match?.[1]);
103
+ }
104
+ function toolEnvelope(entry) {
105
+ const value = argumentValue(entry.arguments);
106
+ const source = typeof value === "string" ? value : "";
107
+ const tools = source ? [...new Set([...source.matchAll(/\btools\.([A-Za-z0-9_]+)/g)].map((match) => match[1]))].slice(0, 8) : [];
108
+ const name = tools.length === 1 ? tools[0] : entry.label ?? "tool";
109
+ return { args: record(value), source, tools, name };
110
+ }
111
+ function patchPath(source) {
112
+ return /\*\*\* (?:Update|Add|Delete) File:\s*([^\r\n]+)/.exec(source)?.[1]?.trim() ?? "";
113
+ }
87
114
  function explicitNumber(sources, keys) {
88
115
  for (const source of sources) {
89
116
  for (const key of keys) {
@@ -117,7 +144,7 @@ function planItems(args) {
117
144
  return [{ label: boundedString(label, 300), status: firstString(value, ["status"]) }];
118
145
  }).slice(0, 12);
119
146
  }
120
- function editPreview(args, resultText) {
147
+ function editPreview(args, resultText, source) {
121
148
  const direct = firstString(args, ["patch", "diff"]);
122
149
  if (direct) return direct;
123
150
  const oldText = firstString(args, ["old_string"]);
@@ -128,21 +155,63 @@ function editPreview(args, resultText) {
128
155
  ...newText.split("\n").map((line) => `+ ${line}`)
129
156
  ].join("\n");
130
157
  }
158
+ const patch = /\*\*\* Begin Patch[\s\S]*?\*\*\* End Patch/.exec(source ?? "")?.[0];
159
+ if (patch) return patch;
131
160
  return /^(?:diff --git|@@ |--- |\+\+\+ )/m.test(resultText ?? "") ? resultText : "";
132
161
  }
162
+ function classifyTool(name, command) {
163
+ const normalized = name.toLocaleLowerCase();
164
+ if (/write_stdin|^wait$/.test(normalized)) return "command";
165
+ if (/update.?plan|todo|checklist|taskcreate|taskupdate/.test(normalized)) return "plan";
166
+ if (/search.?replace|edit|write|patch|replace|create_file|apply_patch/.test(normalized)) return "edit";
167
+ if (/read|view|open_file|list_dir/.test(normalized)) return "read";
168
+ if (/search|find|grep|glob|toolsearch/.test(normalized)) return "search";
169
+ if (/browser|web|fetch|url/.test(normalized)) return "web";
170
+ if (/agent|subagent|sendmessage|delegate|^task$/.test(normalized)) return "agent";
171
+ if (/test|typecheck|lint|build/.test(normalized)) return "test";
172
+ if (/terminal|bash|shell|command|exec|write_stdin|^wait$/.test(normalized)) return /\b(test|typecheck|lint|build)\b/i.test(command) ? "test" : "command";
173
+ return "other";
174
+ }
175
+ function toolAction(status, category, name, tools) {
176
+ const position = status === "pending" ? 0 : status === "error" ? 2 : 1;
177
+ if (tools.length > 1) return [`Running ${tools.length} actions`, `Ran ${tools.length} actions`, `${tools.length} actions failed`][position];
178
+ const normalized = name.toLocaleLowerCase();
179
+ if (/sendmessage/.test(normalized)) return ["Messaging agent", "Messaged agent", "Agent message failed"][position];
180
+ if (/kill_command_or_subagent/.test(normalized)) return ["Stopping", "Stopped", "Stop failed"][position];
181
+ if (/get_command_or_subagent_output|write_stdin|^wait$/.test(normalized)) return ["Waiting for", "Checked", "Check failed"][position];
182
+ const actions = {
183
+ read: ["Reading", "Read", "Read failed"],
184
+ search: ["Searching", "Searched", "Search failed"],
185
+ edit: ["Editing", "Edited", "Edit failed"],
186
+ command: ["Running command", "Ran command", "Command failed"],
187
+ test: ["Running tests", "Ran tests", "Tests failed"],
188
+ web: ["Browsing", "Browsed", "Browser action failed"],
189
+ agent: ["Starting agent", "Started agent", "Agent failed"],
190
+ plan: ["Updating plan", "Updated plan", "Plan update failed"]
191
+ };
192
+ return actions[category]?.[position] ?? (status === "error" ? `${name} failed` : name.replaceAll(/[_-]+/g, " "));
193
+ }
133
194
  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 ?? "";
195
+ const envelope = toolEnvelope(entry);
196
+ const args = envelope.args;
197
+ const command = firstString(args, ["command", "cmd"]) || sourceString(envelope.source, ["command", "cmd"]);
198
+ const category = classifyTool(envelope.name, command || envelope.source || entry.arguments || "");
199
+ const path = firstString(args, ["file_path", "target_file", "target_directory", "path"]) || sourceString(envelope.source, ["file_path", "target_file", "target_directory", "path"]) || patchPath(envelope.source);
200
+ const query = firstString(args, ["query", "pattern"]) || sourceString(envelope.source, ["query", "pattern", "q"]);
201
+ const url = firstString(args, ["url"]) || sourceString(envelope.source, ["url", "ref_id"]);
202
+ const subject = firstString(args, ["subject", "description", "summary", "task", "prompt"]) || sourceString(envelope.source, ["subject", "description", "summary", "task", "prompt"]);
203
+ const background = /get_command_or_subagent_output|kill_command_or_subagent/i.test(envelope.name) ? "background task" : /write_stdin|^wait$/i.test(envelope.name) ? "background command" : "";
204
+ const items = planItems(args);
205
+ const taskId = firstString(args, ["taskId", "task_id"]);
206
+ const planTarget = category === "plan" ? items.length ? `${items.length} ${items.length === 1 ? "item" : "items"}` : taskId ? `task ${taskId}` : "" : "";
207
+ const target = path || command || query || url || subject || background || planTarget || (envelope.tools.length > 1 ? `${envelope.tools.length} coordinated actions` : toolTarget(entry.arguments));
208
+ const previewSource = category === "edit" ? editPreview(args, entry.resultText, envelope.source) : entry.resultText ?? "";
143
209
  const result = record(entry.resultContent);
144
210
  const metadata = record(entry.metadata);
211
+ const resultMetadata = record(result?.metadata);
145
212
  return {
213
+ name: boundedString(envelope.name, 120),
214
+ action: boundedString(toolAction(entry.status ?? "completed", category, envelope.name, envelope.tools), 120),
146
215
  category,
147
216
  detail: toolDetail(category),
148
217
  target: boundedString(target, 300),
@@ -153,12 +222,13 @@ function createToolPresentation(entry) {
153
222
  subject: boundedString(subject, MAX_TOOL_FIELD_CHARS),
154
223
  preview: boundedString(previewSource, MAX_TOOL_PREVIEW_CHARS),
155
224
  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"])
225
+ items,
226
+ tools: envelope.tools,
227
+ exitCode: explicitNumber([result, resultMetadata, metadata], ["exit_code", "exitCode", "pi_bash_exit_code"]),
228
+ durationMs: explicitNumber([result, resultMetadata, metadata], ["duration_ms", "durationMs", "elapsed_ms", "elapsedMs", "totalDurationMs"]),
229
+ additions: explicitNumber([result, resultMetadata, metadata], ["additions", "lines_added"]),
230
+ deletions: explicitNumber([result, resultMetadata, metadata], ["deletions", "lines_removed"]),
231
+ matches: explicitNumber([result, resultMetadata, metadata], ["matches", "match_count", "result_count"])
162
232
  };
163
233
  }
164
234
  function harnessDisplayName(id) {
@@ -179,16 +249,9 @@ function groupConversation(entries) {
179
249
  }
180
250
  function toolCategory(entry) {
181
251
  if (TOOL_CATEGORIES.has(entry.presentation?.category)) return entry.presentation.category;
182
- const name = entry.label?.toLocaleLowerCase() ?? "";
183
- if (/read|view|open_file|list_dir/.test(name)) return "read";
184
- if (/search|find|grep|glob/.test(name)) return "search";
185
- if (/edit|write|patch|replace|create_file/.test(name)) return "edit";
186
- if (/test|typecheck|lint|build/.test(name)) return "test";
187
- if (/terminal|bash|shell|command|exec/.test(name)) return /\b(test|typecheck|lint|build)\b/i.test(entry.arguments ?? "") ? "test" : "command";
188
- if (/browser|web|fetch|url/.test(name)) return "web";
189
- if (/update.?plan|todo|checklist/.test(name)) return "plan";
190
- if (/subagent|spawn.?agent|delegate|^task$/.test(name)) return "agent";
191
- return "other";
252
+ const envelope = toolEnvelope(entry);
253
+ const command = firstString(envelope.args, ["command", "cmd"]) || sourceString(envelope.source, ["command", "cmd"]);
254
+ return classifyTool(envelope.name, command || envelope.source || entry.arguments || "");
192
255
  }
193
256
  function toolTarget(argumentsText) {
194
257
  if (!argumentsText) return "";
@@ -214,7 +277,8 @@ function activitySummary(entries) {
214
277
  if (pending) return `${entries.length} actions in progress`;
215
278
  if (failed) return `${entries.length} actions \xB7 ${failed} failed`;
216
279
  if ([...counts.keys()].every((key) => key === "read" || key === "search")) return `Explored ${entries.length} ${entries.length === 1 ? "item" : "items"}`;
217
- return `${entries.length} actions`;
280
+ const nouns = { read: ["read", "reads"], search: ["search", "searches"], edit: ["edit", "edits"], command: ["command", "commands"], test: ["test run", "test runs"], web: ["web action", "web actions"], agent: ["agent action", "agent actions"], plan: ["plan update", "plan updates"], other: ["action", "actions"] };
281
+ return [...counts.entries()].sort((a, b) => b[1] - a[1]).slice(0, 3).map(([category, count]) => `${count} ${nouns[category][count === 1 ? 0 : 1]}`).join(" \xB7 ");
218
282
  }
219
283
 
220
284
  // src/markdown.jsx
@@ -326,7 +390,8 @@ function ToolIcon({ category }) {
326
390
  const Glyph = TOOL_ICONS[category] ?? TOOL_ICONS.other;
327
391
  return /* @__PURE__ */ jsx2("svg", { class: "scui-tool-icon", viewBox: "0 0 18 18", fill: "none", stroke: "currentColor", "stroke-width": "1.35", "stroke-linecap": "round", "stroke-linejoin": "round", "aria-hidden": "true", children: /* @__PURE__ */ jsx2(Glyph, {}) });
328
392
  }
329
- function toolAction(entry, category) {
393
+ function toolAction2(entry, category, presentation) {
394
+ if (presentation?.action) return presentation.action;
330
395
  const status = entry.status ?? "completed";
331
396
  const actions = {
332
397
  read: ["Reading", "Read", "Read failed"],
@@ -380,6 +445,16 @@ function ToolMetrics({ presentation }) {
380
445
  ].filter(Boolean);
381
446
  return metrics.length ? /* @__PURE__ */ jsx2("div", { class: "scui-tool-metrics", children: metrics.map((metric) => /* @__PURE__ */ jsx2("span", { children: metric }, metric)) }) : null;
382
447
  }
448
+ function PendingElapsed({ now }) {
449
+ const clock = now ?? Date.now;
450
+ const started = useRef(clock());
451
+ const [elapsed, setElapsed] = useState(0);
452
+ useEffect(() => {
453
+ const timer = setInterval(() => setElapsed(Math.max(0, clock() - started.current)), 1e3);
454
+ return () => clearInterval(timer);
455
+ }, [clock]);
456
+ return /* @__PURE__ */ jsx2("small", { class: "scui-tool-elapsed", children: elapsed < 1e3 ? "now" : formatDuration(elapsed) });
457
+ }
383
458
  function LinePreview({ value, kind }) {
384
459
  if (!value) return null;
385
460
  return /* @__PURE__ */ jsx2("ol", { class: "scui-code-preview", "data-kind": kind, children: stripAnsi(value).split("\n").map((line, index) => {
@@ -437,16 +512,31 @@ function ToolPreview({ presentation, entry }) {
437
512
  presentation.url ? /* @__PURE__ */ jsx2("code", { children: presentation.url }) : null,
438
513
  presentation.preview ? /* @__PURE__ */ jsx2("p", { children: stripAnsi(presentation.preview) }) : /* @__PURE__ */ jsx2("small", { children: pending ? "Waiting for the page" : "No page summary returned" })
439
514
  ] });
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
- ] });
515
+ if (presentation.detail === "agent") return /* @__PURE__ */ jsx2("section", { class: "scui-agent-preview", children: presentation.preview ? /* @__PURE__ */ jsx2("pre", { children: stripAnsi(presentation.preview) }) : /* @__PURE__ */ jsx2("small", { children: pending ? "Agent is working" : "No textual handoff returned" }) });
444
516
  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
517
  /* @__PURE__ */ jsx2("i", { "aria-hidden": "true" }),
446
518
  /* @__PURE__ */ jsx2("span", { children: item.label })
447
519
  ] }, `${item.label}:${index}`)) });
448
520
  return presentation.preview ? /* @__PURE__ */ jsx2("pre", { class: "scui-tool-output", "data-error": failed, children: stripAnsi(presentation.preview) }) : null;
449
521
  }
522
+ function ToolActions({ presentation, adapter }) {
523
+ const [copied, setCopied] = useState(false);
524
+ const reset = useRef(null);
525
+ useEffect(() => () => clearTimeout(reset.current), []);
526
+ if (!adapter?.copyText) return null;
527
+ const action = presentation.command ? ["Copy command", presentation.command] : presentation.path ? ["Copy path", presentation.path] : presentation.url ? ["Copy URL", presentation.url] : presentation.query ? ["Copy query", presentation.query] : null;
528
+ const copy = async () => {
529
+ await adapter.copyText(action[1]);
530
+ setCopied(true);
531
+ clearTimeout(reset.current);
532
+ reset.current = setTimeout(() => setCopied(false), 1500);
533
+ };
534
+ return action ? /* @__PURE__ */ jsx2("div", { class: "scui-tool-actions", children: /* @__PURE__ */ jsx2("button", { type: "button", onClick: copy, children: copied ? "Copied" : action[0] }) }) : null;
535
+ }
536
+ function ToolStack({ tools }) {
537
+ if (tools.length < 2) return null;
538
+ return /* @__PURE__ */ jsx2("div", { class: "scui-tool-stack", "aria-label": "Coordinated tools", children: tools.map((tool) => /* @__PURE__ */ jsx2("span", { children: tool.replaceAll("__", " \xB7 ").replaceAll("_", " ") }, tool)) });
539
+ }
450
540
  function TechnicalDetails({ entry }) {
451
541
  if (!entry.arguments && !entry.resultText) return null;
452
542
  return /* @__PURE__ */ jsxs("details", { class: "scui-tool-technical", children: [
@@ -484,35 +574,42 @@ function TranscriptEntry({ entry, state, adapter }) {
484
574
  entry.truncated ? /* @__PURE__ */ jsx2("small", { class: "scui-truncated", children: "Entry truncated by host \xB7 load native session for the complete value" }) : null
485
575
  ] });
486
576
  }
487
- function ToolRow({ entry, workspace, open = false }) {
577
+ function ToolRow({ entry, workspace, open = false, adapter }) {
488
578
  const presentation = entry.presentation ?? createToolPresentation(entry);
579
+ const [expanded, setExpanded] = useState(open || entry.status === "pending");
580
+ useEffect(() => {
581
+ if (entry.status === "pending") setExpanded(true);
582
+ }, [entry.status]);
489
583
  const target = compactToolTarget(presentation.target, workspace);
490
584
  const hasDetail = Boolean(entry.arguments || entry.resultText || presentation.preview || presentation.fields.length);
491
585
  const category = presentation.category ?? toolCategory(entry);
492
586
  const summary = /* @__PURE__ */ jsxs(Fragment, { children: [
493
587
  /* @__PURE__ */ jsx2(ToolIcon, { category }),
494
- /* @__PURE__ */ jsx2("strong", { children: toolAction(entry, category) }),
588
+ /* @__PURE__ */ jsx2("strong", { children: toolAction2(entry, category, presentation) }),
495
589
  target ? /* @__PURE__ */ jsx2("code", { class: "scui-tool-target", title: presentation.target, children: target }) : null,
496
590
  /* @__PURE__ */ jsx2("span", { class: "scui-spacer" }),
591
+ entry.status === "pending" ? /* @__PURE__ */ jsx2(PendingElapsed, { now: adapter?.now }) : null,
497
592
  /* @__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" }),
498
593
  hasDetail ? /* @__PURE__ */ jsx2("span", { class: "scui-tool-chevron", "aria-hidden": "true", children: "\u203A" }) : null
499
594
  ] });
500
595
  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 }) });
501
- return /* @__PURE__ */ jsxs("details", { class: "scui-tool", "data-status": entry.status ?? "completed", "data-category": category, open: open || entry.status === "pending", children: [
596
+ return /* @__PURE__ */ jsxs("details", { class: "scui-tool", "data-status": entry.status ?? "completed", "data-category": category, open: expanded, onToggle: (event) => setExpanded(event.currentTarget.open), children: [
502
597
  /* @__PURE__ */ jsx2("summary", { class: "scui-tool-head", children: summary }),
503
598
  /* @__PURE__ */ jsxs("div", { class: "scui-tool-detail", children: [
504
599
  /* @__PURE__ */ jsx2(ToolMetrics, { presentation }),
600
+ /* @__PURE__ */ jsx2(ToolStack, { tools: presentation.tools ?? [] }),
505
601
  /* @__PURE__ */ jsx2(ToolPreview, { presentation, entry }),
506
602
  presentation.fields.length ? /* @__PURE__ */ jsx2("dl", { class: "scui-tool-fields", children: presentation.fields.map((field) => /* @__PURE__ */ jsxs("div", { children: [
507
603
  /* @__PURE__ */ jsx2("dt", { children: field.label }),
508
604
  /* @__PURE__ */ jsx2("dd", { children: field.value })
509
605
  ] }, field.label)) }) : null,
606
+ /* @__PURE__ */ jsx2(ToolActions, { presentation, adapter }),
510
607
  /* @__PURE__ */ jsx2(TechnicalDetails, { entry })
511
608
  ] })
512
609
  ] });
513
610
  }
514
- function ActivityGroup({ entries, state }) {
515
- if (entries.length === 1) return /* @__PURE__ */ jsx2("section", { class: "scui-activity", "data-single": "true", children: /* @__PURE__ */ jsx2(ToolRow, { entry: entries[0], workspace: state.workspace }) });
611
+ function ActivityGroup({ entries, state, adapter }) {
612
+ if (entries.length === 1) return /* @__PURE__ */ jsx2("section", { class: "scui-activity", "data-single": "true", children: /* @__PURE__ */ jsx2(ToolRow, { entry: entries[0], workspace: state.workspace, adapter }) });
516
613
  const active = entries.some((entry) => entry.status === "pending");
517
614
  const [open, setOpen] = useState(active);
518
615
  const id = useId();
@@ -530,7 +627,7 @@ function ActivityGroup({ entries, state }) {
530
627
  entries.length
531
628
  ] })
532
629
  ] }),
533
- open ? /* @__PURE__ */ jsx2("div", { id, children: entries.map((entry) => /* @__PURE__ */ jsx2(ToolRow, { entry, workspace: state.workspace }, entry.id)) }) : null
630
+ open ? /* @__PURE__ */ jsx2("div", { id, children: entries.map((entry) => /* @__PURE__ */ jsx2(ToolRow, { entry, workspace: state.workspace, adapter }, entry.id)) }) : null
534
631
  ] });
535
632
  }
536
633
  function TaskPlan({ plan }) {