@bahulam/code 2.6.13 → 2.6.15

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