@bahulam/code 2.6.12 → 2.6.14

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.
@@ -35,6 +35,10 @@ const TOOL_LABELS = Object.freeze({
35
35
  workflow_create_multi: 'Creating workflow',
36
36
  workflow_sync_multi: 'Syncing workflows',
37
37
  workflow_run_multi: 'Running workflow',
38
+ Agent: 'Delegating',
39
+ agent: 'Delegating',
40
+ task: 'Delegating',
41
+ sub_agent_tools: 'Sub-agent tools',
38
42
  explore: 'Exploring',
39
43
  plan: 'Planning',
40
44
  verify: 'Verifying',
@@ -43,9 +47,15 @@ const TOOL_LABELS = Object.freeze({
43
47
  ask_user: 'Asking',
44
48
  });
45
49
 
50
+ function labelKey(tool) {
51
+ const raw = String(tool || '');
52
+ return TOOL_LABELS[raw] ? raw : raw.toLowerCase();
53
+ }
54
+
46
55
  export function toolDisplayLabel(tool) {
47
56
  if (!tool) return 'Use tool';
48
- if (TOOL_LABELS[tool]) return TOOL_LABELS[tool];
57
+ const key = labelKey(tool);
58
+ if (TOOL_LABELS[key]) return TOOL_LABELS[key];
49
59
  return tool
50
60
  .replace(/^mcp[_-]?/i, '')
51
61
  .split(/[_-]+/)
@@ -85,7 +95,8 @@ function firstParagraph(text) {
85
95
  }
86
96
 
87
97
  export function toolDisplaySummary(tool, args = {}, { cwd } = {}) {
88
- switch (tool) {
98
+ const key = String(tool || '').toLowerCase();
99
+ switch (key) {
89
100
  case 'shell':
90
101
  return args.command || '(empty command)';
91
102
  case 'read_file': {
@@ -159,6 +170,17 @@ export function toolDisplaySummary(tool, args = {}, { cwd } = {}) {
159
170
  return args.name || args.slug || 'all local workflows';
160
171
  case 'workflow_run_multi':
161
172
  return [args.workflow_id || args.workflowId || args.name || '', args.pattern || 'sequential'].filter(Boolean).join(' · ');
173
+ case 'agent':
174
+ case 'task': {
175
+ const agentName = args.subagent_type || args.agent || args.name || args.type || '';
176
+ const task = firstParagraph(args.prompt || args.task || args.query || args.description || args.instruction || '');
177
+ return [agentName, task].filter(Boolean).join(' · ');
178
+ }
179
+ case 'sub_agent_tools': {
180
+ const total = Number(args.total || args.count || 0);
181
+ const agent = args.agent || args.type || '';
182
+ return [agent, total > 0 ? `${total} tool use${total === 1 ? '' : 's'}` : ''].filter(Boolean).join(' · ');
183
+ }
162
184
  case 'explore':
163
185
  case 'plan':
164
186
  case 'verify':
@@ -185,6 +207,117 @@ export function shellCommandDisplay(command, { cwd = currentWorkingDirectory() }
185
207
  };
186
208
  }
187
209
 
210
+ const COMPACT_SHELL_CHARS = 320;
211
+ const COMPACT_SHELL_LINES = 2;
212
+
213
+ export function shellCommandProfile(command, {
214
+ cwd = currentWorkingDirectory(),
215
+ compactChars = COMPACT_SHELL_CHARS,
216
+ compactLines = COMPACT_SHELL_LINES,
217
+ } = {}) {
218
+ const original = String(command || '');
219
+ const display = shellCommandDisplay(original, { cwd });
220
+ const normalized = String(display.command || '').replace(/\r\n?/g, '\n');
221
+ const body = normalized || '(empty command)';
222
+ const lines = normalized ? normalized.split('\n') : [];
223
+ const commandLineCount = Math.max(1, lines.filter(line => line.trim()).length || lines.length);
224
+ const commandByteCount = byteLength(normalized || body);
225
+ const script = detectShellScript(normalized);
226
+ const lineCount = script?.body ? physicalLineCount(script.body) : commandLineCount;
227
+ const byteCount = script?.body ? byteLength(script.body) : commandByteCount;
228
+ const compact = Boolean(script)
229
+ || commandLineCount >= compactLines
230
+ || commandByteCount > compactChars;
231
+ const kind = script?.kind || (lineCount > 1 ? 'shell script' : 'shell command');
232
+ const summary = compact
233
+ ? `${kind} · ${lineCount} line${lineCount === 1 ? '' : 's'} · ${formatBytes(byteCount)}`
234
+ : body;
235
+
236
+ return {
237
+ original,
238
+ command: body,
239
+ cwdLabel: display.cwdLabel,
240
+ lineCount,
241
+ byteCount,
242
+ commandLineCount,
243
+ commandByteCount,
244
+ compact,
245
+ kind,
246
+ summary,
247
+ script,
248
+ detailHint: compact ? 'details: F2 or /last' : '',
249
+ };
250
+ }
251
+
252
+ function detectShellScript(command) {
253
+ const text = String(command || '').replace(/\r\n?/g, '\n');
254
+ if (!text) return null;
255
+
256
+ const heredoc = text.match(/\b(python3?|node|ruby|perl|bash|sh)\b[^\n]*<<-?\s*(['"]?)([A-Za-z_][A-Za-z0-9_]*)\2[^\n]*\n([\s\S]*?)\n\3(?:\s*$|\s)/);
257
+ if (heredoc) {
258
+ const invocation = text.slice(0, text.indexOf('\n')).trim();
259
+ return {
260
+ kind: interpreterKind(heredoc[1]),
261
+ interpreter: heredoc[1],
262
+ marker: heredoc[3],
263
+ invocation,
264
+ body: heredoc[4],
265
+ };
266
+ }
267
+
268
+ const inline = text.match(/\b(python3?|node|ruby|perl)\b\s+(?:-[A-Za-z]*[ce][A-Za-z]*|--command|--eval)\s+(['"])([\s\S]{120,})\2/);
269
+ if (inline) {
270
+ return {
271
+ kind: interpreterKind(inline[1]),
272
+ interpreter: inline[1],
273
+ invocation: text.slice(0, inline.index + inline[0].indexOf(inline[2])).trim(),
274
+ body: inline[3],
275
+ };
276
+ }
277
+
278
+ const tempScript = text.match(/\b(python3?|node|ruby|perl|bash|sh)\b\s+((?:\/(?:private\/)?tmp|\/private\/var\/folders|\/var\/folders)[^\s;&|]+\.(?:py|mjs|js|rb|pl|sh))\b/);
279
+ if (tempScript) {
280
+ return {
281
+ kind: interpreterKind(tempScript[1]),
282
+ interpreter: tempScript[1],
283
+ invocation: `${tempScript[1]} ${tempScript[2]}`,
284
+ path: tempScript[2],
285
+ };
286
+ }
287
+
288
+ return null;
289
+ }
290
+
291
+ function interpreterKind(value) {
292
+ const name = String(value || '').toLowerCase();
293
+ if (name.startsWith('python')) return 'python script';
294
+ if (name === 'node') return 'node script';
295
+ if (name === 'ruby') return 'ruby script';
296
+ if (name === 'perl') return 'perl script';
297
+ return 'shell script';
298
+ }
299
+
300
+ function byteLength(value) {
301
+ try {
302
+ return Buffer.byteLength(String(value || ''), 'utf8');
303
+ } catch {
304
+ return String(value || '').length;
305
+ }
306
+ }
307
+
308
+ function physicalLineCount(value) {
309
+ const text = String(value || '');
310
+ if (!text) return 0;
311
+ return text.split('\n').length;
312
+ }
313
+
314
+ function formatBytes(bytes) {
315
+ const n = Number(bytes) || 0;
316
+ if (n < 1024) return `${n} B`;
317
+ if (n < 1024 * 1024) return `${(n / 1024).toFixed(n < 10 * 1024 ? 1 : 0)} KB`;
318
+ return `${(n / (1024 * 1024)).toFixed(1)} MB`;
319
+ }
320
+
188
321
  export function formatShellCommand(command, colors) {
189
322
  const tokens = String(command || '').match(/"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|&&|\|\||[|;<>]|[^\s]+|\s+/g) || [];
190
323
  let expectsCommand = true;
@@ -17,7 +17,7 @@
17
17
 
18
18
  import { paint, width as visibleWidth } from './palette.mjs';
19
19
  import { icon } from './icons.mjs';
20
- import { shellCommandDisplay, toolDisplayLabel, toolDisplaySummary } from '../terminal/tool-display.mjs';
20
+ import { shellCommandDisplay, shellCommandProfile, toolDisplayLabel, toolDisplaySummary } from '../terminal/tool-display.mjs';
21
21
  import { label as tierLabel, requiresExplicitApproval, TIERS } from '../core/risk-tier.mjs';
22
22
 
23
23
  /**
@@ -29,12 +29,19 @@ import { label as tierLabel, requiresExplicitApproval, TIERS } from '../core/ris
29
29
  * value — return value from the menu loop
30
30
  * hint — secondary description shown to the right of the label
31
31
  */
32
- export function defaultOptions(tier) {
32
+ export function defaultOptions(tier, { tool = '', args = {} } = {}) {
33
33
  const approve = { key: 'y', label: 'approve once', value: 'approve', hint: 'run this call' };
34
34
  const cancel = { key: 'n', label: 'cancel', value: 'reject', hint: 'do not run' };
35
35
  if (requiresExplicitApproval(tier)) {
36
36
  return [approve, cancel];
37
37
  }
38
+ if (tool === 'shell') {
39
+ return [
40
+ approve,
41
+ { key: 't', label: 'allow similar', value: 'allow-session', hint: `auto-approve ${shellTrustHint(args)} this session` },
42
+ cancel,
43
+ ];
44
+ }
38
45
  return [
39
46
  approve,
40
47
  { key: 't', label: 'always allow', value: 'allow-type', hint: 'auto-approve future calls to this tool' },
@@ -65,27 +72,63 @@ export function defaultOptions(tier) {
65
72
  */
66
73
  export function renderApprovalPrompt({
67
74
  tool, args = {}, tier, why = '', width,
68
- options, selected = 0,
75
+ options, selected = 0, showDetails = false,
69
76
  } = {}) {
70
77
  const cols = Math.max(60, Math.min(width || process.stderr.columns || 96, 120));
71
78
  const explicit = requiresExplicitApproval(tier);
72
79
  const accent = explicit ? paint.brand.accent : paint.brand.data;
73
- const opts = options || defaultOptions(tier);
74
- const title = `${approvalTitle(tier).toLowerCase()} · ${tierLabel(tier)} · ${tool || 'tool'}`;
80
+ const opts = options || defaultOptions(tier, { tool, args });
81
+ const title = `⚠ ${approvalTitle(tier)} · ${tierLabel(tier)} · ${tool || 'tool'}`;
75
82
 
76
83
  const lines = [
77
84
  blockHeader(title, accent),
78
85
  ...subjectRows(tool, args, cols, accent),
86
+ ...detailRows(tool, args, cols, accent, showDetails),
79
87
  ...riskRows(tool, args, tier, accent),
80
- ...reasonRows(why, cols, accent),
88
+ ...reasonRows(tool, args, why, cols, accent),
81
89
  blockLine(accent),
82
90
  ...decisionRows(opts, selected, accent),
83
- blockLine(accent, paint.text.dim('↑↓ move · Enter pick · letter shortcut · Esc cancel')),
91
+ blockLine(accent, paint.text.dim(approvalFooter(tool, showDetails))),
84
92
  ];
85
93
 
86
94
  return '\n' + lines.join('\n');
87
95
  }
88
96
 
97
+ export function renderApprovalDockPrompt({
98
+ tool, args = {}, tier, why = '', width,
99
+ options, selected = 0, showDetails = false,
100
+ } = {}) {
101
+ const cols = Math.max(60, Math.min(width || process.stderr.columns || 96, 120));
102
+ const opts = options || defaultOptions(tier, { tool, args });
103
+ const subject = approvalDockSubject(tool, args, cols, showDetails);
104
+ const risks = riskTerms(tool, args, tier);
105
+ const reason = compactReason(tool, args, why);
106
+ const lines = [
107
+ ...approvalDockSubjectRows(subject),
108
+ ...(risks.length ? [`${paint.text.dim('risk ')}${paint.state.warn(risks.join(', '))}`] : []),
109
+ ...(reason ? [`${paint.text.dim('reason ')}${paint.text.primary(truncate(reason, 120))}`] : []),
110
+ paint.text.dim('Decision'),
111
+ ...opts.map((option, index) => optionToken(option, index === selected, explicitAccent(tier))),
112
+ ];
113
+
114
+ return {
115
+ prefix: '? approve › ',
116
+ value: truncateForDock(subject, showDetails ? 1200 : 220),
117
+ context: `${approvalTitle(tier)} · ${tierLabel(tier)} · ${tool || 'tool'}`,
118
+ meta: '',
119
+ tips: approvalFooter(tool, showDetails),
120
+ lines,
121
+ maxRows: showDetails ? 12 : 8,
122
+ };
123
+ }
124
+
125
+ function shellTrustHint(args = {}) {
126
+ const display = shellCommandDisplay(args.command || args.cmd || '');
127
+ const parts = String(display.command || '').trim().split(/\s+/).filter(Boolean);
128
+ const shape = parts.slice(0, 2).join(' ');
129
+ return shape ? `${shape}*` : 'similar shell commands';
130
+ }
131
+
89
132
  export function renderTrustedApproval({ tool, args = {}, scope = 'session', ruleId = '', delaySeconds = 0 } = {}) {
90
133
  const summary = approvalSubjectSummary(tool, args);
91
134
  const subject = `${tool || 'tool'}${summary ? ` "${truncate(summary, 80)}"` : ''}`;
@@ -138,18 +181,22 @@ function approvalTitle(tier) {
138
181
  }
139
182
 
140
183
  function blockHeader(title, accent) {
141
- return ` ${accent('╭─')} ${paint.bold(accent(title))}`;
184
+ return ` ${paint.bold(accent(title))}`;
142
185
  }
143
186
 
144
187
  function blockLine(accent, text = '') {
145
- return ` ${accent('│')}${text ? ` ${text}` : ''}`;
188
+ return text ? ` ${text}` : ' ';
189
+ }
190
+
191
+ function explicitAccent(tier) {
192
+ return requiresExplicitApproval(tier) ? paint.brand.accent : paint.brand.data;
146
193
  }
147
194
 
148
195
  function subjectRows(tool, args, cols, accent) {
149
196
  const rows = [];
150
197
  const available = Math.max(24, cols - 5);
151
198
  const summary = toolDisplaySummary(tool, args, {});
152
- const label = `${icon(tool)} ${paint.text.primary(toolDisplayLabel(tool))}`;
199
+ const label = subjectLabel(tool);
153
200
  const details = subjectDetails(tool, args, summary, Math.max(24, available - visibleWidth(label) - 1));
154
201
 
155
202
  if (details.length === 1 && visibleWidth(`${label} ${details[0]}`) <= available) {
@@ -164,6 +211,14 @@ function subjectRows(tool, args, cols, accent) {
164
211
  return rows;
165
212
  }
166
213
 
214
+ function subjectLabel(tool) {
215
+ const label = toolDisplayLabel(tool);
216
+ if (tool === 'shell') {
217
+ return `${paint.text.dim('• shell ·')} ${paint.text.primary(label)}`;
218
+ }
219
+ return `${icon(tool)} ${paint.text.primary(label)}`;
220
+ }
221
+
167
222
  function decisionRows(opts, selected, accent) {
168
223
  const rows = [blockLine(accent, paint.text.dim('Decision'))];
169
224
  for (let i = 0; i < opts.length; i++) {
@@ -172,8 +227,8 @@ function decisionRows(opts, selected, accent) {
172
227
  return rows;
173
228
  }
174
229
 
175
- function reasonRows(why, cols, accent) {
176
- const reason = String(why || '').replace(/\s+/g, ' ').trim();
230
+ function reasonRows(tool, args, why, cols, accent) {
231
+ const reason = compactReason(tool, args, why);
177
232
  if (!reason) return [];
178
233
  const label = paint.text.dim('reason ');
179
234
  const firstWidth = Math.max(20, cols - 5 - visibleWidth(label));
@@ -192,6 +247,21 @@ function reasonRows(why, cols, accent) {
192
247
  return rows;
193
248
  }
194
249
 
250
+ function compactReason(tool, args = {}, why = '') {
251
+ const reason = String(why || '').replace(/\s+/g, ' ').trim();
252
+ if (!reason || tool !== 'shell') return reason;
253
+
254
+ const redundantShellPrefix = reason.match(/^Shell command requires approval:\s*(.+)$/i);
255
+ if (!redundantShellPrefix) return reason;
256
+
257
+ const command = String(args.command || args.cmd || '').replace(/\s+/g, ' ').trim();
258
+ const repeated = redundantShellPrefix[1].replace(/\s+/g, ' ').trim();
259
+ if (!command || repeated === command || command.startsWith(repeated) || repeated.startsWith(command)) {
260
+ return 'Shell command requires approval.';
261
+ }
262
+ return reason;
263
+ }
264
+
195
265
  function riskRows(tool, args = {}, tier, accent) {
196
266
  const terms = riskTerms(tool, args, tier);
197
267
  if (!terms.length) return [];
@@ -230,8 +300,16 @@ function optionToken(option, selected, accent) {
230
300
 
231
301
  function subjectDetails(tool, args = {}, summary = '', available = 72) {
232
302
  if (tool === 'shell') {
233
- const display = shellCommandDisplay(args.command || args.cmd || summary || '');
234
- const lines = wrapText(display.command, available);
303
+ const command = args.command || args.cmd || summary || '';
304
+ const profile = shellCommandProfile(command);
305
+ if (profile.compact) {
306
+ const lines = [`$ ${profile.summary}`];
307
+ if (profile.cwdLabel) lines.push(`in ${profile.cwdLabel}`);
308
+ return lines;
309
+ }
310
+ const display = shellCommandDisplay(command);
311
+ const lines = wrapText(display.command, Math.max(20, available - 2))
312
+ .map((line, index) => `${index === 0 ? '$' : '>'} ${line}`);
235
313
  if (display.cwdLabel) lines.push(`in ${display.cwdLabel}`);
236
314
  return lines.length ? lines : ['(empty command)'];
237
315
  }
@@ -259,6 +337,72 @@ function subjectDetails(tool, args = {}, summary = '', available = 72) {
259
337
  return wrapText(summary || JSON.stringify(args || {}), available).slice(0, 4);
260
338
  }
261
339
 
340
+ function detailRows(tool, args = {}, cols, accent, showDetails) {
341
+ if (!showDetails || tool !== 'shell') return [];
342
+ const profile = shellCommandProfile(args.command || args.cmd || '');
343
+ const rows = [];
344
+ const labelWidth = 9;
345
+ const textWidth = Math.max(28, cols - 5 - labelWidth);
346
+
347
+ if (profile.cwdLabel) {
348
+ rows.push(blockLine(accent, `${paint.text.dim('cwd ')} ${paint.brand.data(profile.cwdLabel)}`));
349
+ }
350
+
351
+ rows.push(blockLine(accent, paint.text.dim('details')));
352
+ if (profile.script?.body) {
353
+ const invocation = profile.script.invocation || profile.command.split('\n')[0] || profile.command;
354
+ for (const line of wrapText(invocation, textWidth)) {
355
+ rows.push(blockLine(accent, `${paint.text.dim('cmd ')} ${paint.text.primary(line)}`));
356
+ }
357
+ rows.push(blockLine(accent, paint.text.dim('script ')));
358
+ rows.push(...numberedRows(profile.script.body, cols, accent, 120));
359
+ } else {
360
+ rows.push(...wrappedLabeledRows('cmd ', profile.command, cols, accent, 120));
361
+ }
362
+
363
+ return rows;
364
+ }
365
+
366
+ function approvalFooter(tool, showDetails) {
367
+ const details = tool === 'shell'
368
+ ? ` · d ${showDetails ? 'hide details' : 'details'}`
369
+ : '';
370
+ return `↑↓ move · Enter pick · letter shortcut${details} · Esc cancel`;
371
+ }
372
+
373
+ function approvalDockDetails(tool, args = {}, cols = 96) {
374
+ if (tool !== 'shell') return subjectDetails(tool, args, toolDisplaySummary(tool, args, {}), cols).join(' · ');
375
+ const profile = shellCommandProfile(args.command || args.cmd || '');
376
+ if (profile.script?.body) {
377
+ const invocation = profile.script.invocation || profile.command.split('\n')[0] || profile.command;
378
+ return [
379
+ `$ ${invocation}`,
380
+ ...profile.script.body.split(/\r?\n/).slice(0, 12).map((line, index) => `${index + 1} ${line}`),
381
+ ...(profile.script.body.split(/\r?\n/).length > 12 ? ['...'] : []),
382
+ ].join('\n');
383
+ }
384
+ return profile.command;
385
+ }
386
+
387
+ function approvalDockSubject(tool, args = {}, cols = 96, showDetails = false) {
388
+ if (showDetails && tool === 'shell') return approvalDockDetails(tool, args, cols);
389
+ return subjectDetails(
390
+ tool,
391
+ args,
392
+ toolDisplaySummary(tool, args, {}),
393
+ Math.max(24, cols - 20),
394
+ ).join(' · ');
395
+ }
396
+
397
+ function approvalDockSubjectRows(subject) {
398
+ const lines = String(subject || '').split('\n');
399
+ const first = lines.shift() || '';
400
+ return [
401
+ `${paint.text.dim('? approve ›')} ${paint.text.primary(truncate(first, 160))}`,
402
+ ...lines.slice(0, 6).map(line => `${paint.text.dim(' ')}${paint.text.primary(truncate(line, 160))}`),
403
+ ];
404
+ }
405
+
262
406
  function approvalSubjectSummary(tool, args = {}) {
263
407
  const summary = toolDisplaySummary(tool, args, {});
264
408
  if (tool !== 'shell') return summary;
@@ -274,6 +418,48 @@ function hostFromUrl(url) {
274
418
  }
275
419
  }
276
420
 
421
+ function wrappedLabeledRows(label, text, cols, accent, maxLines = 120) {
422
+ const rows = [];
423
+ const labelText = paint.text.dim(label);
424
+ const firstWidth = Math.max(28, cols - 5 - visibleWidth(labelText) - 1);
425
+ const restWidth = Math.max(28, cols - 5 - visibleWidth(labelText) - 1);
426
+ const sourceLines = String(text || '').replace(/\r\n?/g, '\n').split('\n');
427
+ let emitted = 0;
428
+ for (const source of sourceLines) {
429
+ const wrapped = wrapText(source || ' ', emitted === 0 ? firstWidth : restWidth);
430
+ for (const line of wrapped) {
431
+ if (emitted >= maxLines) {
432
+ rows.push(blockLine(accent, `${paint.text.dim(' ')}${paint.text.dim('... detail truncated')}`));
433
+ return rows;
434
+ }
435
+ rows.push(blockLine(accent, `${emitted === 0 ? labelText : paint.text.dim(' ')} ${paint.text.primary(line)}`));
436
+ emitted++;
437
+ }
438
+ }
439
+ return rows;
440
+ }
441
+
442
+ function numberedRows(text, cols, accent, maxLines = 120) {
443
+ const rows = [];
444
+ const lines = String(text || '').replace(/\r\n?/g, '\n').split('\n');
445
+ const width = String(Math.min(lines.length, maxLines)).length;
446
+ const textWidth = Math.max(28, cols - 5 - width - 2);
447
+ for (let i = 0; i < Math.min(lines.length, maxLines); i++) {
448
+ const n = String(i + 1).padStart(width);
449
+ rows.push(blockLine(accent, `${paint.text.dim(`${n} `)}${paint.text.primary(truncate(lines[i], textWidth))}`));
450
+ }
451
+ if (lines.length > maxLines) {
452
+ rows.push(blockLine(accent, `${paint.text.dim(`... ${lines.length - maxLines} more line(s)`)}`));
453
+ }
454
+ return rows;
455
+ }
456
+
457
+ function truncateForDock(text, maxChars) {
458
+ const value = String(text || '').trim();
459
+ if (value.length <= maxChars) return value;
460
+ return `${value.slice(0, Math.max(0, maxChars - 1))}…`;
461
+ }
462
+
277
463
  function wrapText(text, width) {
278
464
  const words = String(text || '').split(/\s+/).filter(Boolean);
279
465
  if (!words.length) return [''];
package/src/ui/icons.mjs CHANGED
@@ -61,6 +61,10 @@ const TOOL_ICON = Object.freeze({
61
61
  verify: 'subAgent',
62
62
  debug: 'subAgent',
63
63
  refactor: 'subAgent',
64
+ Agent: 'subAgent',
65
+ agent: 'subAgent',
66
+ task: 'subAgent',
67
+ sub_agent_tools: 'subAgent',
64
68
 
65
69
  // Read / search
66
70
  read_file: 'search',
@@ -133,14 +137,15 @@ export const icons = new Proxy({}, {
133
137
  */
134
138
  export function icon(toolName) {
135
139
  if (!toolName) return '';
136
- const key = TOOL_ICON[toolName];
140
+ const raw = String(toolName);
141
+ const key = TOOL_ICON[raw] || TOOL_ICON[raw.toLowerCase()];
137
142
  if (key) return render(ICON_RECORDS[key]);
138
143
 
139
144
  // MCP tools often arrive as "mcp__server__tool" — strip the prefix and
140
145
  // try again before falling back to the generic glyph.
141
- if (toolName.startsWith('mcp')) {
142
- const cleaned = toolName.replace(/^mcp[_-]+/, '').split(/[_-]+/)[0];
143
- const fallback = TOOL_ICON[cleaned];
146
+ if (raw.startsWith('mcp')) {
147
+ const cleaned = raw.replace(/^mcp[_-]+/, '').split(/[_-]+/)[0];
148
+ const fallback = TOOL_ICON[cleaned] || TOOL_ICON[cleaned.toLowerCase()];
144
149
  if (fallback) return render(ICON_RECORDS[fallback]);
145
150
  }
146
151
 
@@ -152,7 +157,8 @@ export function icon(toolName) {
152
157
  * Used by tier classification and color choice in the tool card renderer.
153
158
  */
154
159
  export function toolFamily(toolName) {
155
- return TOOL_ICON[toolName] || 'other';
160
+ const raw = String(toolName || '');
161
+ return TOOL_ICON[raw] || TOOL_ICON[raw.toLowerCase()] || 'other';
156
162
  }
157
163
 
158
164
  /**