@volter-ai-dev/supercode-ui 0.1.8 → 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
@@ -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,234 @@ 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 argumentValue(argumentsText) {
94
+ if (!argumentsText) return null;
95
+ try {
96
+ return JSON.parse(argumentsText);
97
+ } catch {
98
+ return null;
99
+ }
100
+ }
101
+
102
+ function argumentObject(argumentsText) {
103
+ return record(argumentValue(argumentsText));
104
+ }
105
+
106
+ function firstString(source, keys) {
107
+ for (const key of keys) {
108
+ if (typeof source?.[key] === 'string' && source[key].trim()) return source[key].trim();
109
+ }
110
+ return '';
111
+ }
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
+
148
+ function explicitNumber(sources, keys) {
149
+ for (const source of sources) {
150
+ for (const key of keys) {
151
+ const value = source?.[key];
152
+ const parsed = typeof value === 'string' && value.trim() !== '' ? Number(value) : value;
153
+ if (typeof parsed === 'number' && Number.isFinite(parsed)) return parsed;
154
+ }
155
+ }
156
+ return null;
157
+ }
158
+
159
+ function toolDetail(category) {
160
+ return ({ read: 'file', search: 'matches', edit: 'diff', command: 'terminal', test: 'terminal', web: 'web', agent: 'agent', plan: 'plan' })[category] ?? 'fields';
161
+ }
162
+
163
+ function usefulToolFields(args) {
164
+ if (!args) return [];
165
+ 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']);
166
+ return Object.entries(args).flatMap(([key, value]) => {
167
+ if (hidden.has(key) || value === null || value === undefined) return [];
168
+ const rendered = typeof value === 'string' ? value : JSON.stringify(value);
169
+ if (!rendered) return [];
170
+ return [{ label: key.replaceAll(/[_-]+/g, ' ').replace(/^./, (letter) => letter.toLocaleUpperCase()), value: boundedString(rendered, MAX_TOOL_FIELD_CHARS) }];
171
+ }).slice(0, MAX_TOOL_FIELDS);
172
+ }
173
+
174
+ function planItems(args) {
175
+ const source = Array.isArray(args?.plan) ? args.plan : Array.isArray(args?.todos) ? args.todos : [];
176
+ return source.flatMap((item) => {
177
+ if (typeof item === 'string' && item.trim()) return [{ label: boundedString(item.trim(), 300), status: '' }];
178
+ const value = record(item);
179
+ const label = firstString(value, ['step', 'title', 'content', 'text']);
180
+ if (!label) return [];
181
+ return [{ label: boundedString(label, 300), status: firstString(value, ['status']) }];
182
+ }).slice(0, 12);
183
+ }
184
+
185
+ function editPreview(args, resultText, source) {
186
+ const direct = firstString(args, ['patch', 'diff']);
187
+ if (direct) return direct;
188
+ const oldText = firstString(args, ['old_string']);
189
+ const newText = firstString(args, ['new_string']);
190
+ if (oldText || newText) {
191
+ return [
192
+ ...oldText.split('\n').map((line) => `- ${line}`),
193
+ ...newText.split('\n').map((line) => `+ ${line}`),
194
+ ].join('\n');
195
+ }
196
+ const patch = /\*\*\* Begin Patch[\s\S]*?\*\*\* End Patch/.exec(source ?? '')?.[0];
197
+ if (patch) return patch;
198
+ return /^(?:diff --git|@@ |--- |\+\+\+ )/m.test(resultText ?? '') ? resultText : '';
199
+ }
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
+
235
+ export function createToolPresentation(entry) {
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 ?? '');
250
+ const result = record(entry.resultContent);
251
+ const metadata = record(entry.metadata);
252
+ const resultMetadata = record(result?.metadata);
253
+ return {
254
+ name: boundedString(envelope.name, 120),
255
+ action: boundedString(toolAction(entry.status ?? 'completed', category, envelope.name, envelope.tools), 120),
256
+ category,
257
+ detail: toolDetail(category),
258
+ target: boundedString(target, 300),
259
+ command: boundedString(command, MAX_TOOL_FIELD_CHARS),
260
+ path: boundedString(path, MAX_TOOL_FIELD_CHARS),
261
+ query: boundedString(query, MAX_TOOL_FIELD_CHARS),
262
+ url: boundedString(url, MAX_TOOL_FIELD_CHARS),
263
+ subject: boundedString(subject, MAX_TOOL_FIELD_CHARS),
264
+ preview: boundedString(previewSource, MAX_TOOL_PREVIEW_CHARS),
265
+ fields: usefulToolFields(args),
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']),
273
+ };
274
+ }
275
+
276
+ function readToolPresentation(value, entry) {
277
+ const generated = createToolPresentation(entry);
278
+ const item = record(value);
279
+ if (!item) return generated;
280
+ const fields = Array.isArray(item.fields) ? item.fields.flatMap((raw) => {
281
+ const field = record(raw);
282
+ return field && typeof field.label === 'string' && typeof field.value === 'string'
283
+ ? [{ label: boundedString(field.label, 80), value: boundedString(field.value, MAX_TOOL_FIELD_CHARS) }]
284
+ : [];
285
+ }).slice(0, MAX_TOOL_FIELDS) : generated.fields;
286
+ const items = Array.isArray(item.items) ? item.items.flatMap((raw) => {
287
+ const planItem = record(raw);
288
+ return planItem && typeof planItem.label === 'string'
289
+ ? [{ label: boundedString(planItem.label, 300), status: boundedString(planItem.status, 40) }]
290
+ : [];
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;
293
+ return {
294
+ name: boundedString(item.name, 120) || generated.name,
295
+ action: boundedString(item.action, 120) || generated.action,
296
+ category: TOOL_CATEGORIES.has(item.category) ? item.category : generated.category,
297
+ detail: TOOL_DETAILS.has(item.detail) ? item.detail : generated.detail,
298
+ target: boundedString(item.target, 300) || generated.target,
299
+ command: boundedString(item.command, MAX_TOOL_FIELD_CHARS) || generated.command,
300
+ path: boundedString(item.path, MAX_TOOL_FIELD_CHARS) || generated.path,
301
+ query: boundedString(item.query, MAX_TOOL_FIELD_CHARS) || generated.query,
302
+ url: boundedString(item.url, MAX_TOOL_FIELD_CHARS) || generated.url,
303
+ subject: boundedString(item.subject, MAX_TOOL_FIELD_CHARS) || generated.subject,
304
+ preview: boundedString(item.preview, MAX_TOOL_PREVIEW_CHARS) || generated.preview,
305
+ fields,
306
+ items,
307
+ tools,
308
+ exitCode: nullableNumber(item.exitCode) ?? generated.exitCode,
309
+ durationMs: nullableNumber(item.durationMs) ?? generated.durationMs,
310
+ additions: nullableNumber(item.additions) ?? generated.additions,
311
+ deletions: nullableNumber(item.deletions) ?? generated.deletions,
312
+ matches: nullableNumber(item.matches) ?? generated.matches,
313
+ };
314
+ }
315
+
83
316
  function readTranscript(value) {
84
317
  if (!Array.isArray(value)) return [];
85
318
  const result = [];
@@ -97,6 +330,7 @@ function readTranscript(value) {
97
330
  if (typeof item[key] === 'string') entry[key] = item[key];
98
331
  }
99
332
  if (['pending', 'completed', 'error'].includes(item.status)) entry.status = item.status;
333
+ if (item.role === 'tool') entry.presentation = readToolPresentation(item.presentation, entry);
100
334
  if (typeof item.streaming === 'boolean') entry.streaming = item.streaming;
101
335
  if (Array.isArray(item.context)) {
102
336
  entry.context = item.context.flatMap((raw) => {
@@ -358,15 +592,10 @@ export function groupConversation(entries) {
358
592
  }
359
593
 
360
594
  export function toolCategory(entry) {
361
- const name = entry.label?.toLocaleLowerCase() ?? '';
362
- if (/read|view|open_file|list_dir/.test(name)) return 'read';
363
- if (/search|find|grep|glob/.test(name)) return 'search';
364
- if (/edit|write|patch|replace|create_file/.test(name)) return 'edit';
365
- if (/test|typecheck|lint|build/.test(name)) return 'test';
366
- if (/terminal|bash|shell|command|exec/.test(name)) return /\b(test|typecheck|lint|build)\b/i.test(entry.arguments ?? '') ? 'test' : 'command';
367
- if (/browser|web|fetch|url/.test(name)) return 'web';
368
- if (/subagent|spawn|task/.test(name)) return 'agent';
369
- return 'other';
595
+ if (TOOL_CATEGORIES.has(entry.presentation?.category)) return entry.presentation.category;
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 || '');
370
599
  }
371
600
 
372
601
  export function toolTarget(argumentsText) {
@@ -395,7 +624,8 @@ export function activitySummary(entries) {
395
624
  if (pending) return `${entries.length} actions in progress`;
396
625
  if (failed) return `${entries.length} actions · ${failed} failed`;
397
626
  if ([...counts.keys()].every((key) => key === 'read' || key === 'search')) return `Explored ${entries.length} ${entries.length === 1 ? 'item' : 'items'}`;
398
- 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(' · ');
399
629
  }
400
630
 
401
631
  export function canContinueHere(state) {