@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/core.mjs CHANGED
@@ -90,15 +90,19 @@ function boundedString(value, max) {
90
90
  return value.length <= max ? value : `${value.slice(0, max)}…`;
91
91
  }
92
92
 
93
- function argumentObject(argumentsText) {
93
+ function argumentValue(argumentsText) {
94
94
  if (!argumentsText) return null;
95
95
  try {
96
- return record(JSON.parse(argumentsText));
96
+ return JSON.parse(argumentsText);
97
97
  } catch {
98
98
  return null;
99
99
  }
100
100
  }
101
101
 
102
+ function argumentObject(argumentsText) {
103
+ return record(argumentValue(argumentsText));
104
+ }
105
+
102
106
  function firstString(source, keys) {
103
107
  for (const key of keys) {
104
108
  if (typeof source?.[key] === 'string' && source[key].trim()) return source[key].trim();
@@ -106,6 +110,41 @@ function firstString(source, keys) {
106
110
  return '';
107
111
  }
108
112
 
113
+ function decodedLiteral(value) {
114
+ if (!value) return '';
115
+ if (value.startsWith('"')) {
116
+ try { return JSON.parse(value); } catch { return ''; }
117
+ }
118
+ return value.slice(1, -1)
119
+ .replaceAll('\\n', '\n')
120
+ .replaceAll('\\t', '\t')
121
+ .replaceAll('\\r', '\r')
122
+ .replaceAll('\\`', '`')
123
+ .replaceAll("\\'", "'")
124
+ .replaceAll('\\\\', '\\');
125
+ }
126
+
127
+ function sourceString(source, keys) {
128
+ if (!source) return '';
129
+ const names = keys.join('|');
130
+ const match = new RegExp(`\\b(?:${names})\\s*:\\s*(\"(?:\\\\.|[^\"\\\\])*\"|'(?:\\\\.|[^'\\\\])*'|\`(?:\\\\.|[^\`\\\\])*\`)`).exec(source);
131
+ return decodedLiteral(match?.[1]);
132
+ }
133
+
134
+ function toolEnvelope(entry) {
135
+ const value = argumentValue(entry.arguments);
136
+ const source = typeof value === 'string' ? value : '';
137
+ const tools = source
138
+ ? [...new Set([...source.matchAll(/\btools\.([A-Za-z0-9_]+)/g)].map((match) => match[1]))].slice(0, 8)
139
+ : [];
140
+ const name = tools.length === 1 ? tools[0] : entry.label ?? 'tool';
141
+ return { args: record(value), source, tools, name };
142
+ }
143
+
144
+ function patchPath(source) {
145
+ return /\*\*\* (?:Update|Add|Delete) File:\s*([^\r\n]+)/.exec(source)?.[1]?.trim() ?? '';
146
+ }
147
+
109
148
  function explicitNumber(sources, keys) {
110
149
  for (const source of sources) {
111
150
  for (const key of keys) {
@@ -143,7 +182,7 @@ function planItems(args) {
143
182
  }).slice(0, 12);
144
183
  }
145
184
 
146
- function editPreview(args, resultText) {
185
+ function editPreview(args, resultText, source) {
147
186
  const direct = firstString(args, ['patch', 'diff']);
148
187
  if (direct) return direct;
149
188
  const oldText = firstString(args, ['old_string']);
@@ -154,22 +193,66 @@ function editPreview(args, resultText) {
154
193
  ...newText.split('\n').map((line) => `+ ${line}`),
155
194
  ].join('\n');
156
195
  }
196
+ const patch = /\*\*\* Begin Patch[\s\S]*?\*\*\* End Patch/.exec(source ?? '')?.[0];
197
+ if (patch) return patch;
157
198
  return /^(?:diff --git|@@ |--- |\+\+\+ )/m.test(resultText ?? '') ? resultText : '';
158
199
  }
159
200
 
201
+ function classifyTool(name, command) {
202
+ const normalized = name.toLocaleLowerCase();
203
+ if (/write_stdin|^wait$/.test(normalized)) return 'command';
204
+ if (/update.?plan|todo|checklist|taskcreate|taskupdate/.test(normalized)) return 'plan';
205
+ if (/search.?replace|edit|write|patch|replace|create_file|apply_patch/.test(normalized)) return 'edit';
206
+ if (/read|view|open_file|list_dir/.test(normalized)) return 'read';
207
+ if (/search|find|grep|glob|toolsearch/.test(normalized)) return 'search';
208
+ if (/browser|web|fetch|url/.test(normalized)) return 'web';
209
+ if (/agent|subagent|sendmessage|delegate|^task$/.test(normalized)) return 'agent';
210
+ if (/test|typecheck|lint|build/.test(normalized)) return 'test';
211
+ if (/terminal|bash|shell|command|exec|write_stdin|^wait$/.test(normalized)) return /\b(test|typecheck|lint|build)\b/i.test(command) ? 'test' : 'command';
212
+ return 'other';
213
+ }
214
+
215
+ function toolAction(status, category, name, tools) {
216
+ const position = status === 'pending' ? 0 : status === 'error' ? 2 : 1;
217
+ if (tools.length > 1) return [`Running ${tools.length} actions`, `Ran ${tools.length} actions`, `${tools.length} actions failed`][position];
218
+ const normalized = name.toLocaleLowerCase();
219
+ if (/sendmessage/.test(normalized)) return ['Messaging agent', 'Messaged agent', 'Agent message failed'][position];
220
+ if (/kill_command_or_subagent/.test(normalized)) return ['Stopping', 'Stopped', 'Stop failed'][position];
221
+ if (/get_command_or_subagent_output|write_stdin|^wait$/.test(normalized)) return ['Waiting for', 'Checked', 'Check failed'][position];
222
+ const actions = {
223
+ read: ['Reading', 'Read', 'Read failed'],
224
+ search: ['Searching', 'Searched', 'Search failed'],
225
+ edit: ['Editing', 'Edited', 'Edit failed'],
226
+ command: ['Running command', 'Ran command', 'Command failed'],
227
+ test: ['Running tests', 'Ran tests', 'Tests failed'],
228
+ web: ['Browsing', 'Browsed', 'Browser action failed'],
229
+ agent: ['Starting agent', 'Started agent', 'Agent failed'],
230
+ plan: ['Updating plan', 'Updated plan', 'Plan update failed'],
231
+ };
232
+ return actions[category]?.[position] ?? (status === 'error' ? `${name} failed` : name.replaceAll(/[_-]+/g, ' '));
233
+ }
234
+
160
235
  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 ?? '');
236
+ const envelope = toolEnvelope(entry);
237
+ const args = envelope.args;
238
+ const command = firstString(args, ['command', 'cmd']) || sourceString(envelope.source, ['command', 'cmd']);
239
+ const category = classifyTool(envelope.name, command || envelope.source || entry.arguments || '');
240
+ const path = firstString(args, ['file_path', 'target_file', 'target_directory', 'path']) || sourceString(envelope.source, ['file_path', 'target_file', 'target_directory', 'path']) || patchPath(envelope.source);
241
+ const query = firstString(args, ['query', 'pattern']) || sourceString(envelope.source, ['query', 'pattern', 'q']);
242
+ const url = firstString(args, ['url']) || sourceString(envelope.source, ['url', 'ref_id']);
243
+ const subject = firstString(args, ['subject', 'description', 'summary', 'task', 'prompt']) || sourceString(envelope.source, ['subject', 'description', 'summary', 'task', 'prompt']);
244
+ 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' : '';
245
+ const items = planItems(args);
246
+ const taskId = firstString(args, ['taskId', 'task_id']);
247
+ const planTarget = category === 'plan' ? items.length ? `${items.length} ${items.length === 1 ? 'item' : 'items'}` : taskId ? `task ${taskId}` : '' : '';
248
+ const target = path || command || query || url || subject || background || planTarget || (envelope.tools.length > 1 ? `${envelope.tools.length} coordinated actions` : toolTarget(entry.arguments));
249
+ const previewSource = category === 'edit' ? editPreview(args, entry.resultText, envelope.source) : (entry.resultText ?? '');
170
250
  const result = record(entry.resultContent);
171
251
  const metadata = record(entry.metadata);
252
+ const resultMetadata = record(result?.metadata);
172
253
  return {
254
+ name: boundedString(envelope.name, 120),
255
+ action: boundedString(toolAction(entry.status ?? 'completed', category, envelope.name, envelope.tools), 120),
173
256
  category,
174
257
  detail: toolDetail(category),
175
258
  target: boundedString(target, 300),
@@ -180,12 +263,13 @@ export function createToolPresentation(entry) {
180
263
  subject: boundedString(subject, MAX_TOOL_FIELD_CHARS),
181
264
  preview: boundedString(previewSource, MAX_TOOL_PREVIEW_CHARS),
182
265
  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']),
266
+ items,
267
+ tools: envelope.tools,
268
+ exitCode: explicitNumber([result, resultMetadata, metadata], ['exit_code', 'exitCode', 'pi_bash_exit_code']),
269
+ durationMs: explicitNumber([result, resultMetadata, metadata], ['duration_ms', 'durationMs', 'elapsed_ms', 'elapsedMs', 'totalDurationMs']),
270
+ additions: explicitNumber([result, resultMetadata, metadata], ['additions', 'lines_added']),
271
+ deletions: explicitNumber([result, resultMetadata, metadata], ['deletions', 'lines_removed']),
272
+ matches: explicitNumber([result, resultMetadata, metadata], ['matches', 'match_count', 'result_count']),
189
273
  };
190
274
  }
191
275
 
@@ -205,7 +289,10 @@ function readToolPresentation(value, entry) {
205
289
  ? [{ label: boundedString(planItem.label, 300), status: boundedString(planItem.status, 40) }]
206
290
  : [];
207
291
  }).slice(0, 12) : generated.items;
292
+ const tools = Array.isArray(item.tools) ? item.tools.filter((tool) => typeof tool === 'string').map((tool) => boundedString(tool, 120)).slice(0, 8) : generated.tools;
208
293
  return {
294
+ name: boundedString(item.name, 120) || generated.name,
295
+ action: boundedString(item.action, 120) || generated.action,
209
296
  category: TOOL_CATEGORIES.has(item.category) ? item.category : generated.category,
210
297
  detail: TOOL_DETAILS.has(item.detail) ? item.detail : generated.detail,
211
298
  target: boundedString(item.target, 300) || generated.target,
@@ -217,6 +304,7 @@ function readToolPresentation(value, entry) {
217
304
  preview: boundedString(item.preview, MAX_TOOL_PREVIEW_CHARS) || generated.preview,
218
305
  fields,
219
306
  items,
307
+ tools,
220
308
  exitCode: nullableNumber(item.exitCode) ?? generated.exitCode,
221
309
  durationMs: nullableNumber(item.durationMs) ?? generated.durationMs,
222
310
  additions: nullableNumber(item.additions) ?? generated.additions,
@@ -505,16 +593,9 @@ export function groupConversation(entries) {
505
593
 
506
594
  export function toolCategory(entry) {
507
595
  if (TOOL_CATEGORIES.has(entry.presentation?.category)) return entry.presentation.category;
508
- const name = entry.label?.toLocaleLowerCase() ?? '';
509
- if (/read|view|open_file|list_dir/.test(name)) return 'read';
510
- if (/search|find|grep|glob/.test(name)) return 'search';
511
- if (/edit|write|patch|replace|create_file/.test(name)) return 'edit';
512
- if (/test|typecheck|lint|build/.test(name)) return 'test';
513
- if (/terminal|bash|shell|command|exec/.test(name)) return /\b(test|typecheck|lint|build)\b/i.test(entry.arguments ?? '') ? 'test' : 'command';
514
- if (/browser|web|fetch|url/.test(name)) return 'web';
515
- if (/update.?plan|todo|checklist/.test(name)) return 'plan';
516
- if (/subagent|spawn.?agent|delegate|^task$/.test(name)) return 'agent';
517
- return 'other';
596
+ const envelope = toolEnvelope(entry);
597
+ const command = firstString(envelope.args, ['command', 'cmd']) || sourceString(envelope.source, ['command', 'cmd']);
598
+ return classifyTool(envelope.name, command || envelope.source || entry.arguments || '');
518
599
  }
519
600
 
520
601
  export function toolTarget(argumentsText) {
@@ -543,7 +624,8 @@ export function activitySummary(entries) {
543
624
  if (pending) return `${entries.length} actions in progress`;
544
625
  if (failed) return `${entries.length} actions · ${failed} failed`;
545
626
  if ([...counts.keys()].every((key) => key === 'read' || key === 'search')) return `Explored ${entries.length} ${entries.length === 1 ? 'item' : 'items'}`;
546
- return `${entries.length} actions`;
627
+ 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'] };
628
+ return [...counts.entries()].sort((a, b) => b[1] - a[1]).slice(0, 3).map(([category, count]) => `${count} ${nouns[category][count === 1 ? 0 : 1]}`).join(' · ');
547
629
  }
548
630
 
549
631
  export function canContinueHere(state) {
package/embed.mjs CHANGED
@@ -88,10 +88,10 @@ function boundedString(value, max) {
88
88
  if (typeof value !== "string") return "";
89
89
  return value.length <= max ? value : `${value.slice(0, max)}\u2026`;
90
90
  }
91
- function argumentObject(argumentsText) {
91
+ function argumentValue(argumentsText) {
92
92
  if (!argumentsText) return null;
93
93
  try {
94
- return record(JSON.parse(argumentsText));
94
+ return JSON.parse(argumentsText);
95
95
  } catch {
96
96
  return null;
97
97
  }
@@ -102,6 +102,33 @@ function firstString(source, keys) {
102
102
  }
103
103
  return "";
104
104
  }
105
+ function decodedLiteral(value) {
106
+ if (!value) return "";
107
+ if (value.startsWith('"')) {
108
+ try {
109
+ return JSON.parse(value);
110
+ } catch {
111
+ return "";
112
+ }
113
+ }
114
+ return value.slice(1, -1).replaceAll("\\n", "\n").replaceAll("\\t", " ").replaceAll("\\r", "\r").replaceAll("\\`", "`").replaceAll("\\'", "'").replaceAll("\\\\", "\\");
115
+ }
116
+ function sourceString(source, keys) {
117
+ if (!source) return "";
118
+ const names = keys.join("|");
119
+ const match = new RegExp(`\\b(?:${names})\\s*:\\s*("(?:\\\\.|[^"\\\\])*"|'(?:\\\\.|[^'\\\\])*'|\`(?:\\\\.|[^\`\\\\])*\`)`).exec(source);
120
+ return decodedLiteral(match?.[1]);
121
+ }
122
+ function toolEnvelope(entry) {
123
+ const value = argumentValue(entry.arguments);
124
+ const source = typeof value === "string" ? value : "";
125
+ const tools = source ? [...new Set([...source.matchAll(/\btools\.([A-Za-z0-9_]+)/g)].map((match) => match[1]))].slice(0, 8) : [];
126
+ const name = tools.length === 1 ? tools[0] : entry.label ?? "tool";
127
+ return { args: record(value), source, tools, name };
128
+ }
129
+ function patchPath(source) {
130
+ return /\*\*\* (?:Update|Add|Delete) File:\s*([^\r\n]+)/.exec(source)?.[1]?.trim() ?? "";
131
+ }
105
132
  function explicitNumber(sources, keys) {
106
133
  for (const source of sources) {
107
134
  for (const key of keys) {
@@ -135,7 +162,7 @@ function planItems(args) {
135
162
  return [{ label: boundedString(label, 300), status: firstString(value, ["status"]) }];
136
163
  }).slice(0, 12);
137
164
  }
138
- function editPreview(args, resultText) {
165
+ function editPreview(args, resultText, source) {
139
166
  const direct = firstString(args, ["patch", "diff"]);
140
167
  if (direct) return direct;
141
168
  const oldText = firstString(args, ["old_string"]);
@@ -146,21 +173,63 @@ function editPreview(args, resultText) {
146
173
  ...newText.split("\n").map((line) => `+ ${line}`)
147
174
  ].join("\n");
148
175
  }
176
+ const patch = /\*\*\* Begin Patch[\s\S]*?\*\*\* End Patch/.exec(source ?? "")?.[0];
177
+ if (patch) return patch;
149
178
  return /^(?:diff --git|@@ |--- |\+\+\+ )/m.test(resultText ?? "") ? resultText : "";
150
179
  }
180
+ function classifyTool(name, command) {
181
+ const normalized = name.toLocaleLowerCase();
182
+ if (/write_stdin|^wait$/.test(normalized)) return "command";
183
+ if (/update.?plan|todo|checklist|taskcreate|taskupdate/.test(normalized)) return "plan";
184
+ if (/search.?replace|edit|write|patch|replace|create_file|apply_patch/.test(normalized)) return "edit";
185
+ if (/read|view|open_file|list_dir/.test(normalized)) return "read";
186
+ if (/search|find|grep|glob|toolsearch/.test(normalized)) return "search";
187
+ if (/browser|web|fetch|url/.test(normalized)) return "web";
188
+ if (/agent|subagent|sendmessage|delegate|^task$/.test(normalized)) return "agent";
189
+ if (/test|typecheck|lint|build/.test(normalized)) return "test";
190
+ if (/terminal|bash|shell|command|exec|write_stdin|^wait$/.test(normalized)) return /\b(test|typecheck|lint|build)\b/i.test(command) ? "test" : "command";
191
+ return "other";
192
+ }
193
+ function toolAction(status, category, name, tools) {
194
+ const position = status === "pending" ? 0 : status === "error" ? 2 : 1;
195
+ if (tools.length > 1) return [`Running ${tools.length} actions`, `Ran ${tools.length} actions`, `${tools.length} actions failed`][position];
196
+ const normalized = name.toLocaleLowerCase();
197
+ if (/sendmessage/.test(normalized)) return ["Messaging agent", "Messaged agent", "Agent message failed"][position];
198
+ if (/kill_command_or_subagent/.test(normalized)) return ["Stopping", "Stopped", "Stop failed"][position];
199
+ if (/get_command_or_subagent_output|write_stdin|^wait$/.test(normalized)) return ["Waiting for", "Checked", "Check failed"][position];
200
+ const actions = {
201
+ read: ["Reading", "Read", "Read failed"],
202
+ search: ["Searching", "Searched", "Search failed"],
203
+ edit: ["Editing", "Edited", "Edit failed"],
204
+ command: ["Running command", "Ran command", "Command failed"],
205
+ test: ["Running tests", "Ran tests", "Tests failed"],
206
+ web: ["Browsing", "Browsed", "Browser action failed"],
207
+ agent: ["Starting agent", "Started agent", "Agent failed"],
208
+ plan: ["Updating plan", "Updated plan", "Plan update failed"]
209
+ };
210
+ return actions[category]?.[position] ?? (status === "error" ? `${name} failed` : name.replaceAll(/[_-]+/g, " "));
211
+ }
151
212
  function createToolPresentation(entry) {
152
- const args = argumentObject(entry.arguments);
153
- const category = toolCategory({ label: entry.label, arguments: entry.arguments });
154
- const command = firstString(args, ["command", "cmd"]);
155
- const path = firstString(args, ["file_path", "target_file", "path"]);
156
- const query = firstString(args, ["query", "pattern"]);
157
- const url = firstString(args, ["url"]);
158
- const subject = firstString(args, ["description", "task", "prompt"]);
159
- const target = path || command || query || url || subject || toolTarget(entry.arguments);
160
- const previewSource = category === "edit" ? editPreview(args, entry.resultText) : entry.resultText ?? "";
213
+ const envelope = toolEnvelope(entry);
214
+ const args = envelope.args;
215
+ const command = firstString(args, ["command", "cmd"]) || sourceString(envelope.source, ["command", "cmd"]);
216
+ const category = classifyTool(envelope.name, command || envelope.source || entry.arguments || "");
217
+ const path = firstString(args, ["file_path", "target_file", "target_directory", "path"]) || sourceString(envelope.source, ["file_path", "target_file", "target_directory", "path"]) || patchPath(envelope.source);
218
+ const query = firstString(args, ["query", "pattern"]) || sourceString(envelope.source, ["query", "pattern", "q"]);
219
+ const url = firstString(args, ["url"]) || sourceString(envelope.source, ["url", "ref_id"]);
220
+ const subject = firstString(args, ["subject", "description", "summary", "task", "prompt"]) || sourceString(envelope.source, ["subject", "description", "summary", "task", "prompt"]);
221
+ 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" : "";
222
+ const items = planItems(args);
223
+ const taskId = firstString(args, ["taskId", "task_id"]);
224
+ const planTarget = category === "plan" ? items.length ? `${items.length} ${items.length === 1 ? "item" : "items"}` : taskId ? `task ${taskId}` : "" : "";
225
+ const target = path || command || query || url || subject || background || planTarget || (envelope.tools.length > 1 ? `${envelope.tools.length} coordinated actions` : toolTarget(entry.arguments));
226
+ const previewSource = category === "edit" ? editPreview(args, entry.resultText, envelope.source) : entry.resultText ?? "";
161
227
  const result = record(entry.resultContent);
162
228
  const metadata = record(entry.metadata);
229
+ const resultMetadata = record(result?.metadata);
163
230
  return {
231
+ name: boundedString(envelope.name, 120),
232
+ action: boundedString(toolAction(entry.status ?? "completed", category, envelope.name, envelope.tools), 120),
164
233
  category,
165
234
  detail: toolDetail(category),
166
235
  target: boundedString(target, 300),
@@ -171,12 +240,13 @@ function createToolPresentation(entry) {
171
240
  subject: boundedString(subject, MAX_TOOL_FIELD_CHARS),
172
241
  preview: boundedString(previewSource, MAX_TOOL_PREVIEW_CHARS),
173
242
  fields: usefulToolFields(args),
174
- items: planItems(args),
175
- exitCode: explicitNumber([result, metadata], ["exit_code", "exitCode"]),
176
- durationMs: explicitNumber([result, metadata], ["duration_ms", "durationMs", "elapsed_ms", "elapsedMs"]),
177
- additions: explicitNumber([result, metadata], ["additions", "lines_added"]),
178
- deletions: explicitNumber([result, metadata], ["deletions", "lines_removed"]),
179
- matches: explicitNumber([result, metadata], ["matches", "match_count", "result_count"])
243
+ items,
244
+ tools: envelope.tools,
245
+ exitCode: explicitNumber([result, resultMetadata, metadata], ["exit_code", "exitCode", "pi_bash_exit_code"]),
246
+ durationMs: explicitNumber([result, resultMetadata, metadata], ["duration_ms", "durationMs", "elapsed_ms", "elapsedMs", "totalDurationMs"]),
247
+ additions: explicitNumber([result, resultMetadata, metadata], ["additions", "lines_added"]),
248
+ deletions: explicitNumber([result, resultMetadata, metadata], ["deletions", "lines_removed"]),
249
+ matches: explicitNumber([result, resultMetadata, metadata], ["matches", "match_count", "result_count"])
180
250
  };
181
251
  }
182
252
  function readToolPresentation(value, entry) {
@@ -191,7 +261,10 @@ function readToolPresentation(value, entry) {
191
261
  const planItem = record(raw);
192
262
  return planItem && typeof planItem.label === "string" ? [{ label: boundedString(planItem.label, 300), status: boundedString(planItem.status, 40) }] : [];
193
263
  }).slice(0, 12) : generated.items;
264
+ const tools = Array.isArray(item.tools) ? item.tools.filter((tool) => typeof tool === "string").map((tool) => boundedString(tool, 120)).slice(0, 8) : generated.tools;
194
265
  return {
266
+ name: boundedString(item.name, 120) || generated.name,
267
+ action: boundedString(item.action, 120) || generated.action,
195
268
  category: TOOL_CATEGORIES.has(item.category) ? item.category : generated.category,
196
269
  detail: TOOL_DETAILS.has(item.detail) ? item.detail : generated.detail,
197
270
  target: boundedString(item.target, 300) || generated.target,
@@ -203,6 +276,7 @@ function readToolPresentation(value, entry) {
203
276
  preview: boundedString(item.preview, MAX_TOOL_PREVIEW_CHARS) || generated.preview,
204
277
  fields,
205
278
  items,
279
+ tools,
206
280
  exitCode: nullableNumber(item.exitCode) ?? generated.exitCode,
207
281
  durationMs: nullableNumber(item.durationMs) ?? generated.durationMs,
208
282
  additions: nullableNumber(item.additions) ?? generated.additions,
@@ -436,16 +510,9 @@ function groupConversation(entries) {
436
510
  }
437
511
  function toolCategory(entry) {
438
512
  if (TOOL_CATEGORIES.has(entry.presentation?.category)) return entry.presentation.category;
439
- const name = entry.label?.toLocaleLowerCase() ?? "";
440
- if (/read|view|open_file|list_dir/.test(name)) return "read";
441
- if (/search|find|grep|glob/.test(name)) return "search";
442
- if (/edit|write|patch|replace|create_file/.test(name)) return "edit";
443
- if (/test|typecheck|lint|build/.test(name)) return "test";
444
- if (/terminal|bash|shell|command|exec/.test(name)) return /\b(test|typecheck|lint|build)\b/i.test(entry.arguments ?? "") ? "test" : "command";
445
- if (/browser|web|fetch|url/.test(name)) return "web";
446
- if (/update.?plan|todo|checklist/.test(name)) return "plan";
447
- if (/subagent|spawn.?agent|delegate|^task$/.test(name)) return "agent";
448
- return "other";
513
+ const envelope = toolEnvelope(entry);
514
+ const command = firstString(envelope.args, ["command", "cmd"]) || sourceString(envelope.source, ["command", "cmd"]);
515
+ return classifyTool(envelope.name, command || envelope.source || entry.arguments || "");
449
516
  }
450
517
  function toolTarget(argumentsText) {
451
518
  if (!argumentsText) return "";
@@ -471,7 +538,8 @@ function activitySummary(entries) {
471
538
  if (pending) return `${entries.length} actions in progress`;
472
539
  if (failed) return `${entries.length} actions \xB7 ${failed} failed`;
473
540
  if ([...counts.keys()].every((key) => key === "read" || key === "search")) return `Explored ${entries.length} ${entries.length === 1 ? "item" : "items"}`;
474
- return `${entries.length} actions`;
541
+ 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"] };
542
+ 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 ");
475
543
  }
476
544
  function canContinueHere(state) {
477
545
  if (state.mode !== "mirror" || state.canSend) return false;
@@ -694,7 +762,8 @@ function ToolIcon({ category }) {
694
762
  const Glyph = TOOL_ICONS[category] ?? TOOL_ICONS.other;
695
763
  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, {}) });
696
764
  }
697
- function toolAction(entry, category) {
765
+ function toolAction2(entry, category, presentation) {
766
+ if (presentation?.action) return presentation.action;
698
767
  const status = entry.status ?? "completed";
699
768
  const actions = {
700
769
  read: ["Reading", "Read", "Read failed"],
@@ -748,6 +817,16 @@ function ToolMetrics({ presentation }) {
748
817
  ].filter(Boolean);
749
818
  return metrics.length ? /* @__PURE__ */ jsx3("div", { class: "scui-tool-metrics", children: metrics.map((metric) => /* @__PURE__ */ jsx3("span", { children: metric }, metric)) }) : null;
750
819
  }
820
+ function PendingElapsed({ now }) {
821
+ const clock = now ?? Date.now;
822
+ const started = useRef2(clock());
823
+ const [elapsed, setElapsed] = useState2(0);
824
+ useEffect2(() => {
825
+ const timer = setInterval(() => setElapsed(Math.max(0, clock() - started.current)), 1e3);
826
+ return () => clearInterval(timer);
827
+ }, [clock]);
828
+ return /* @__PURE__ */ jsx3("small", { class: "scui-tool-elapsed", children: elapsed < 1e3 ? "now" : formatDuration(elapsed) });
829
+ }
751
830
  function LinePreview({ value, kind }) {
752
831
  if (!value) return null;
753
832
  return /* @__PURE__ */ jsx3("ol", { class: "scui-code-preview", "data-kind": kind, children: stripAnsi(value).split("\n").map((line, index) => {
@@ -805,16 +884,31 @@ function ToolPreview({ presentation, entry }) {
805
884
  presentation.url ? /* @__PURE__ */ jsx3("code", { children: presentation.url }) : null,
806
885
  presentation.preview ? /* @__PURE__ */ jsx3("p", { children: stripAnsi(presentation.preview) }) : /* @__PURE__ */ jsx3("small", { children: pending ? "Waiting for the page" : "No page summary returned" })
807
886
  ] });
808
- if (presentation.detail === "agent") return /* @__PURE__ */ jsxs2("section", { class: "scui-agent-preview", children: [
809
- presentation.subject ? /* @__PURE__ */ jsx3("p", { children: presentation.subject }) : null,
810
- presentation.preview ? /* @__PURE__ */ jsx3("pre", { children: stripAnsi(presentation.preview) }) : /* @__PURE__ */ jsx3("small", { children: pending ? "Agent is working" : "No textual handoff returned" })
811
- ] });
887
+ 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" }) });
812
888
  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: [
813
889
  /* @__PURE__ */ jsx3("i", { "aria-hidden": "true" }),
814
890
  /* @__PURE__ */ jsx3("span", { children: item.label })
815
891
  ] }, `${item.label}:${index}`)) });
816
892
  return presentation.preview ? /* @__PURE__ */ jsx3("pre", { class: "scui-tool-output", "data-error": failed, children: stripAnsi(presentation.preview) }) : null;
817
893
  }
894
+ function ToolActions({ presentation, adapter }) {
895
+ const [copied, setCopied] = useState2(false);
896
+ const reset = useRef2(null);
897
+ useEffect2(() => () => clearTimeout(reset.current), []);
898
+ if (!adapter?.copyText) return null;
899
+ 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;
900
+ const copy = async () => {
901
+ await adapter.copyText(action[1]);
902
+ setCopied(true);
903
+ clearTimeout(reset.current);
904
+ reset.current = setTimeout(() => setCopied(false), 1500);
905
+ };
906
+ return action ? /* @__PURE__ */ jsx3("div", { class: "scui-tool-actions", children: /* @__PURE__ */ jsx3("button", { type: "button", onClick: copy, children: copied ? "Copied" : action[0] }) }) : null;
907
+ }
908
+ function ToolStack({ tools }) {
909
+ if (tools.length < 2) return null;
910
+ 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)) });
911
+ }
818
912
  function TechnicalDetails({ entry }) {
819
913
  if (!entry.arguments && !entry.resultText) return null;
820
914
  return /* @__PURE__ */ jsxs2("details", { class: "scui-tool-technical", children: [
@@ -852,35 +946,42 @@ function TranscriptEntry({ entry, state, adapter }) {
852
946
  entry.truncated ? /* @__PURE__ */ jsx3("small", { class: "scui-truncated", children: "Entry truncated by host \xB7 load native session for the complete value" }) : null
853
947
  ] });
854
948
  }
855
- function ToolRow({ entry, workspace, open = false }) {
949
+ function ToolRow({ entry, workspace, open = false, adapter }) {
856
950
  const presentation = entry.presentation ?? createToolPresentation(entry);
951
+ const [expanded, setExpanded] = useState2(open || entry.status === "pending");
952
+ useEffect2(() => {
953
+ if (entry.status === "pending") setExpanded(true);
954
+ }, [entry.status]);
857
955
  const target = compactToolTarget(presentation.target, workspace);
858
956
  const hasDetail = Boolean(entry.arguments || entry.resultText || presentation.preview || presentation.fields.length);
859
957
  const category = presentation.category ?? toolCategory(entry);
860
958
  const summary = /* @__PURE__ */ jsxs2(Fragment, { children: [
861
959
  /* @__PURE__ */ jsx3(ToolIcon, { category }),
862
- /* @__PURE__ */ jsx3("strong", { children: toolAction(entry, category) }),
960
+ /* @__PURE__ */ jsx3("strong", { children: toolAction2(entry, category, presentation) }),
863
961
  target ? /* @__PURE__ */ jsx3("code", { class: "scui-tool-target", title: presentation.target, children: target }) : null,
864
962
  /* @__PURE__ */ jsx3("span", { class: "scui-spacer" }),
963
+ entry.status === "pending" ? /* @__PURE__ */ jsx3(PendingElapsed, { now: adapter?.now }) : null,
865
964
  /* @__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" }),
866
965
  hasDetail ? /* @__PURE__ */ jsx3("span", { class: "scui-tool-chevron", "aria-hidden": "true", children: "\u203A" }) : null
867
966
  ] });
868
967
  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 }) });
869
- return /* @__PURE__ */ jsxs2("details", { class: "scui-tool", "data-status": entry.status ?? "completed", "data-category": category, open: open || entry.status === "pending", children: [
968
+ return /* @__PURE__ */ jsxs2("details", { class: "scui-tool", "data-status": entry.status ?? "completed", "data-category": category, open: expanded, onToggle: (event) => setExpanded(event.currentTarget.open), children: [
870
969
  /* @__PURE__ */ jsx3("summary", { class: "scui-tool-head", children: summary }),
871
970
  /* @__PURE__ */ jsxs2("div", { class: "scui-tool-detail", children: [
872
971
  /* @__PURE__ */ jsx3(ToolMetrics, { presentation }),
972
+ /* @__PURE__ */ jsx3(ToolStack, { tools: presentation.tools ?? [] }),
873
973
  /* @__PURE__ */ jsx3(ToolPreview, { presentation, entry }),
874
974
  presentation.fields.length ? /* @__PURE__ */ jsx3("dl", { class: "scui-tool-fields", children: presentation.fields.map((field) => /* @__PURE__ */ jsxs2("div", { children: [
875
975
  /* @__PURE__ */ jsx3("dt", { children: field.label }),
876
976
  /* @__PURE__ */ jsx3("dd", { children: field.value })
877
977
  ] }, field.label)) }) : null,
978
+ /* @__PURE__ */ jsx3(ToolActions, { presentation, adapter }),
878
979
  /* @__PURE__ */ jsx3(TechnicalDetails, { entry })
879
980
  ] })
880
981
  ] });
881
982
  }
882
- function ActivityGroup({ entries, state }) {
883
- if (entries.length === 1) return /* @__PURE__ */ jsx3("section", { class: "scui-activity", "data-single": "true", children: /* @__PURE__ */ jsx3(ToolRow, { entry: entries[0], workspace: state.workspace }) });
983
+ function ActivityGroup({ entries, state, adapter }) {
984
+ 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 }) });
884
985
  const active = entries.some((entry) => entry.status === "pending");
885
986
  const [open, setOpen] = useState2(active);
886
987
  const id = useId();
@@ -898,7 +999,7 @@ function ActivityGroup({ entries, state }) {
898
999
  entries.length
899
1000
  ] })
900
1001
  ] }),
901
- open ? /* @__PURE__ */ jsx3("div", { id, children: entries.map((entry) => /* @__PURE__ */ jsx3(ToolRow, { entry, workspace: state.workspace }, entry.id)) }) : null
1002
+ open ? /* @__PURE__ */ jsx3("div", { id, children: entries.map((entry) => /* @__PURE__ */ jsx3(ToolRow, { entry, workspace: state.workspace, adapter }, entry.id)) }) : null
902
1003
  ] });
903
1004
  }
904
1005
  function TaskPlan({ plan }) {
package/index.d.ts CHANGED
@@ -80,6 +80,8 @@ export interface TranscriptEntryModel {
80
80
  export type ToolCategory = 'read' | 'search' | 'edit' | 'command' | 'test' | 'web' | 'agent' | 'plan' | 'other';
81
81
 
82
82
  export interface ToolPresentationModel {
83
+ name: string;
84
+ action: string;
83
85
  category: ToolCategory;
84
86
  detail: 'file' | 'matches' | 'diff' | 'terminal' | 'web' | 'agent' | 'plan' | 'fields';
85
87
  target: string;
@@ -91,6 +93,7 @@ export interface ToolPresentationModel {
91
93
  preview: string;
92
94
  fields: Array<{ label: string; value: string }>;
93
95
  items: Array<{ label: string; status: string }>;
96
+ tools: string[];
94
97
  exitCode: number | null;
95
98
  durationMs: number | null;
96
99
  additions: number | null;
@@ -266,6 +269,7 @@ export interface ToolRowProps {
266
269
  entry: TranscriptEntryModel;
267
270
  workspace?: string;
268
271
  open?: boolean;
272
+ adapter?: Pick<UiAdapter, 'copyText' | 'now'>;
269
273
  }
270
274
 
271
275
  export interface TaskPlanProps {
@@ -314,7 +318,7 @@ export function groupConversation(entries: readonly TranscriptEntryModel[]): Arr
314
318
  | { kind: 'activity'; id: string; entries: TranscriptEntryModel[] }
315
319
  >;
316
320
  export function toolCategory(entry: Pick<TranscriptEntryModel, 'label' | 'arguments' | 'presentation'>): ToolCategory;
317
- export function createToolPresentation(entry: Pick<TranscriptEntryModel, 'label' | 'arguments' | 'resultText'> & { resultContent?: unknown; metadata?: Record<string, string> }): ToolPresentationModel;
321
+ export function createToolPresentation(entry: Pick<TranscriptEntryModel, 'label' | 'arguments' | 'resultText' | 'status'> & { resultContent?: unknown; metadata?: Record<string, string> }): ToolPresentationModel;
318
322
  export function toolTarget(argumentsText?: string): string;
319
323
  export function compactToolTarget(target: string, workspace: string): string;
320
324
  export function activitySummary(entries: readonly TranscriptEntryModel[]): string;