@bahulam/code 2.6.13 → 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.
@@ -71,8 +71,9 @@ let unsubResize = null;
71
71
  // the cursor at the correct input position. Without this, readline echoes
72
72
  // land on rows the dock briefly moved through mid-render and characters
73
73
  // appear above/below the input row.
74
- let lastFrame = { context: '', meta: '', tips: '', prefix: '', value: '', cursor: null };
74
+ let lastFrame = { context: '', meta: '', tips: '', prefix: '', value: '', cursor: null, overlayLines: null };
75
75
  let resetting = false;
76
+ let lastGeometry = null;
76
77
 
77
78
  function write(s) { try { OUT.write(s); } catch {} }
78
79
  function setScrollRegion(top, bottom) { write(`${ESC}${top};${bottom}r`); }
@@ -90,6 +91,13 @@ function cols() {
90
91
  return Math.max(40, term().columns || 80);
91
92
  }
92
93
 
94
+ function drawableColumns() {
95
+ // Avoid writing into the final terminal column. Many terminals enter
96
+ // autowrap state there; during rapid resizes that can push fixed dock
97
+ // frame lines into scrollback.
98
+ return Math.max(1, cols() - 1);
99
+ }
100
+
93
101
  function contentBottomRow() {
94
102
  return Math.max(1, rows() - reservedRows);
95
103
  }
@@ -137,8 +145,8 @@ function computeInputRowsForBuffer(prefix, value) {
137
145
  // meta line) which then leaks into the transcript as streamed content
138
146
  // scrolls past them. We clear the old dock region BEFORE moving the frame
139
147
  // so the freed rows are blank when they enter the scroll region.
140
- function setInputRowsTo(nextRows) {
141
- const clamped = Math.max(MIN_INPUT_ROWS, Math.min(inputRowsMax, Math.floor(nextRows)));
148
+ function setInputRowsTo(nextRows, { maxRows = inputRowsMax } = {}) {
149
+ const clamped = Math.max(MIN_INPUT_ROWS, Math.min(maxRows, Math.floor(nextRows)));
142
150
  if (clamped === inputRows) return false;
143
151
  const shrinking = clamped < inputRows;
144
152
  if (mounted && shrinking) {
@@ -162,7 +170,7 @@ function setInputRowsTo(nextRows) {
162
170
 
163
171
  function padLine(text) {
164
172
  const value = String(text || '');
165
- const pad = Math.max(0, cols() - visibleWidth(value));
173
+ const pad = Math.max(0, drawableColumns() - visibleWidth(value));
166
174
  return value + ' '.repeat(pad);
167
175
  }
168
176
 
@@ -183,7 +191,7 @@ function ruleChars(count) {
183
191
  // - Optional right-aligned context strip (session tokens/elapsed) with rule
184
192
  // fill between label and context
185
193
  function topRuleLine(context = '') {
186
- const w = Math.max(0, cols() - 1);
194
+ const w = Math.max(0, drawableColumns());
187
195
  const brand = paint.brand.primary;
188
196
  const bold = paint.bold;
189
197
 
@@ -192,14 +200,19 @@ function topRuleLine(context = '') {
192
200
  const label = bold(brand(BRAND_LABEL));
193
201
  const leftBlock = `${leadRule} ${accent}${label}`;
194
202
  const leftWidth = visibleWidth(leftBlock);
203
+ const rightPadRule = 2;
195
204
 
196
- const ctx = fitText(context, Math.max(0, Math.floor(w / 2)));
205
+ const maxCtxWidth = Math.max(0, Math.min(
206
+ Math.floor(w / 2),
207
+ w - leftWidth - 1 /* left gap */ - 1 /* minimum middle */ - 1 /* ctx gap */ - rightPadRule - 1 /* tail gap */,
208
+ ));
209
+ const ctx = fitText(context, maxCtxWidth);
197
210
  const ctxWidth = ctx ? visibleWidth(ctx) : 0;
198
211
 
199
212
  // Rule fill in the middle. Reserve 1 space around ctx when present.
200
- const gap = 1;
201
- const rightPadRule = 2;
202
- const middleWidth = Math.max(1, w - leftWidth - gap - ctxWidth - (ctx ? gap + rightPadRule : 0));
213
+ const middleWidth = ctx
214
+ ? Math.max(1, w - leftWidth - ctxWidth - rightPadRule - 3)
215
+ : Math.max(1, w - leftWidth - 1);
203
216
  const middleRule = brand(ruleChars(middleWidth));
204
217
 
205
218
  if (!ctx) {
@@ -210,7 +223,7 @@ function topRuleLine(context = '') {
210
223
  }
211
224
 
212
225
  function bottomRuleLine() {
213
- return paint.brand.primary(ruleChars(Math.max(0, cols() - 1)));
226
+ return paint.brand.primary(ruleChars(Math.max(0, drawableColumns())));
214
227
  }
215
228
 
216
229
  // Always park the cursor at the tracked (prefix, value) input position.
@@ -229,8 +242,11 @@ function parkCursorAtInput() {
229
242
  focusDockInput(prefix, value, lastFrame.cursor);
230
243
  }
231
244
 
232
- function applyLayout() {
245
+ function applyLayout({ clearPrevious = false } = {}) {
233
246
  if (!mounted) return;
247
+ if (clearPrevious && lastGeometry) {
248
+ clearDockArea({ restore: true, geometry: lastGeometry });
249
+ }
234
250
  const bottom = contentBottomRow();
235
251
  setScrollRegion(1, bottom);
236
252
  renderFrame(lastFrame);
@@ -257,6 +273,9 @@ function renderFrame(frame = {}) {
257
273
  clearLine();
258
274
 
259
275
  // (input rows are written by drawInputLines / clearInputRows)
276
+ if (Array.isArray(lastFrame.overlayLines)) {
277
+ drawInputLines(lastFrame.overlayLines);
278
+ }
260
279
 
261
280
  // Spacer below input.
262
281
  moveTo(spacerBelowRow(), 1);
@@ -290,6 +309,20 @@ function renderFrame(frame = {}) {
290
309
  // pinned-status writers clobber the outer save and restore to the wrong
291
310
  // place. Instead, callers that need the cursor parked at the input row
292
311
  // invoke parkCursorAtInput() after renderFrame returns.
312
+ lastGeometry = {
313
+ top: topRuleRow(),
314
+ bottom: rows(),
315
+ };
316
+ }
317
+
318
+ function clearDockRows(startRow, endRow) {
319
+ const maxRow = rows();
320
+ const start = Math.max(1, Math.min(maxRow, Math.floor(startRow || 1)));
321
+ const end = Math.max(start, Math.min(maxRow, Math.floor(endRow || maxRow)));
322
+ for (let row = start; row <= end; row++) {
323
+ moveTo(row, 1);
324
+ clearLine();
325
+ }
293
326
  }
294
327
 
295
328
  function clearInputRows() {
@@ -299,13 +332,10 @@ function clearInputRows() {
299
332
  }
300
333
  }
301
334
 
302
- export function clearDockArea({ restore = true } = {}) {
335
+ export function clearDockArea({ restore = true, geometry = null } = {}) {
303
336
  if (!mounted) return false;
304
337
  if (restore) saveCursor();
305
- for (let row = topRuleRow(); row <= rows(); row++) {
306
- moveTo(row, 1);
307
- clearLine();
308
- }
338
+ clearDockRows(geometry?.top || topRuleRow(), geometry?.bottom || rows());
309
339
  if (restore) restoreCursor();
310
340
  return true;
311
341
  }
@@ -329,6 +359,15 @@ function layoutInput(prefix, value) {
329
359
  };
330
360
  }
331
361
 
362
+ function layoutOverlayLines(lines) {
363
+ const budget = inputTextBudget();
364
+ const wrapped = [];
365
+ for (const line of lines) {
366
+ wrapped.push(...wrapToLines(line, budget));
367
+ }
368
+ return wrapped.length ? wrapped : [''];
369
+ }
370
+
332
371
  function drawInputLines(lines) {
333
372
  const indent = ' '.repeat(INPUT_INDENT);
334
373
  for (let i = 0; i < inputRows; i++) {
@@ -359,7 +398,7 @@ export function mountInputDock({ inputRowsMax: requestedMax } = {}) {
359
398
  mounted = true;
360
399
  applyLayout();
361
400
 
362
- unsubResize = onResize(() => applyLayout());
401
+ unsubResize = onResize(() => applyLayout({ clearPrevious: true }));
363
402
  process.once('exit', safeUnmount);
364
403
  process.once('SIGTERM', () => { safeUnmount(); process.exit(143); });
365
404
  return true;
@@ -377,6 +416,7 @@ export function unmountInputDock() {
377
416
  } finally {
378
417
  mounted = false;
379
418
  resetting = false;
419
+ lastGeometry = null;
380
420
  }
381
421
  }
382
422
 
@@ -435,7 +475,7 @@ export function prepareInputPrompt({ context = '', tips = '', meta = '' } = {})
435
475
  if (!mounted) return false;
436
476
  setInputRowsTo(MIN_INPUT_ROWS);
437
477
  clearInputRows();
438
- renderFrame({ context, tips, meta, prefix: '', value: '' });
478
+ renderFrame({ context, tips, meta, prefix: '', value: '', overlayLines: null });
439
479
  moveTo(inputRowStart(), INPUT_INDENT + 1);
440
480
  return true;
441
481
  }
@@ -444,6 +484,7 @@ export function clearInputPrompt() {
444
484
  if (!mounted) return false;
445
485
  clearInputRows();
446
486
  lastFrame.value = '';
487
+ lastFrame.overlayLines = null;
447
488
  renderFrame(lastFrame);
448
489
  parkCursorAtInput();
449
490
  return true;
@@ -452,13 +493,39 @@ export function clearInputPrompt() {
452
493
  export function renderDockInput(prefix, value, { context = '', tips = '', meta = '', cursor = null } = {}) {
453
494
  if (!mounted) return false;
454
495
  setInputRowsTo(computeInputRowsForBuffer(prefix, value));
455
- renderFrame({ context, tips, meta, prefix, value, cursor });
496
+ renderFrame({ context, tips, meta, prefix, value, cursor, overlayLines: null });
456
497
  const layout = layoutInput(prefix, value);
457
498
  drawInputLines(layout.lines);
458
499
  focusDockInput(prefix, value, cursor);
459
500
  return true;
460
501
  }
461
502
 
503
+ export function renderDockOverlay({
504
+ context = '',
505
+ lines = [],
506
+ meta = '',
507
+ tips = '',
508
+ maxRows = 8,
509
+ } = {}) {
510
+ if (!mounted) return false;
511
+ const sourceLines = Array.isArray(lines) ? lines : String(lines || '').split('\n');
512
+ const wrapped = layoutOverlayLines(sourceLines);
513
+ const rowCap = Math.max(MIN_INPUT_ROWS, Math.min(MAX_INPUT_ROWS_CAP, Math.max(inputRowsMax, maxRows)));
514
+ setInputRowsTo(Math.min(rowCap, Math.max(MIN_INPUT_ROWS, wrapped.length)), { maxRows: rowCap });
515
+ const tail = tailWithEllipsis(wrapped, inputRows);
516
+ renderFrame({
517
+ context,
518
+ meta,
519
+ tips,
520
+ prefix: '',
521
+ value: '',
522
+ cursor: null,
523
+ overlayLines: tail.visible,
524
+ });
525
+ moveTo(rows(), 1);
526
+ return true;
527
+ }
528
+
462
529
  /**
463
530
  * Move the terminal cursor to the position that corresponds to
464
531
  * `prefix + value[0..cursorInValue]` within the (possibly wrapped and
@@ -499,6 +566,10 @@ export function _internals() {
499
566
  reservedRows: () => reservedRows,
500
567
  layoutInput,
501
568
  inputTextBudget,
569
+ topRuleLine,
570
+ bottomRuleLine,
571
+ padLine,
572
+ drawableColumns,
502
573
  FIXED_ROWS,
503
574
  BRAND_LABEL,
504
575
  };
@@ -45,6 +45,7 @@ export const COMMANDS = {
45
45
  '/surgical': 'Verbosity: show everything (reasoning, expanded tools)',
46
46
  '/compact': 'Compact conversation context',
47
47
  '/agents': 'List available agents',
48
+ '/run': 'Run a sub-agent or synced workflow',
48
49
  '/explore': 'Code explorer agent',
49
50
  '/review': 'Code review agent',
50
51
  '/architect': 'Feature architect agent',
@@ -137,11 +138,20 @@ export const HELP_GROUPS = [
137
138
  ['/agents create <name>', 'Create .bahulam/agents/<name>.yaml'],
138
139
  ['/agents edit <name>', 'Open local agent YAML'],
139
140
  ['/agents sync [name]', 'Sync all or one local agent to cloud'],
141
+ ['/run <agent> [instruction]', 'Run a local or built-in agent'],
140
142
  ['/explore <instruction>', 'Explore code'],
141
143
  ['/review <instruction>', 'Review code'],
142
144
  ['/architect <instruction>', 'Design an approach'],
143
145
  ],
144
146
  },
147
+ {
148
+ key: 'workflows',
149
+ title: 'Workflows',
150
+ summary: 'saved automations',
151
+ commands: [
152
+ ['/run <workflow> [instruction]', 'Run a synced workflow if no agent matches'],
153
+ ],
154
+ },
145
155
  {
146
156
  key: 'session',
147
157
  title: 'Session',
@@ -29,6 +29,7 @@ import {
29
29
  toolDisplaySummary,
30
30
  formatShellCommand,
31
31
  shellCommandDisplay,
32
+ shellCommandProfile,
32
33
  } from '../terminal/tool-display.mjs';
33
34
 
34
35
  // ── Family → label colorizer ─────────────────────────────────────────────
@@ -50,8 +51,10 @@ function formatArgs(tool, args, cwd) {
50
51
  const summary = toolDisplaySummary(tool, args || {}, { cwd });
51
52
  if (!summary) return '';
52
53
  if (tool === 'shell') {
54
+ const profile = shellCommandProfile(summary, { cwd });
55
+ if (profile.compact) return compactShellProfile(profile);
53
56
  const display = shellCommandDisplay(summary, { cwd });
54
- const command = formatShellCommand(display.command, paintShellAdapter);
57
+ const command = `${paint.text.dim('$')} ${formatShellCommand(display.command, paintShellAdapter)}`;
55
58
  return display.cwdLabel
56
59
  ? `${command} ${paint.text.dim('in')} ${paint.brand.data(display.cwdLabel)}`
57
60
  : command;
@@ -150,6 +153,10 @@ export function summarizeResult(tool, data) {
150
153
  if (exit != null && exit !== 0) {
151
154
  return { text: `exit ${exit}`, tone: 'danger' };
152
155
  }
156
+ if (tool === 'shell') {
157
+ const structured = structuredOutputSummary(data.output_preview || data.output);
158
+ if (structured) return structured;
159
+ }
153
160
  const head = firstOutputLine(data).slice(0, 100);
154
161
  return { text: head || 'ok', tone: 'success' };
155
162
  }
@@ -179,6 +186,49 @@ export function summarizeResult(tool, data) {
179
186
  }
180
187
  }
181
188
 
189
+ function structuredOutputSummary(output) {
190
+ const raw = String(output || '').trim();
191
+ if (!raw || !/^[\[{]/.test(raw)) return null;
192
+ let value;
193
+ try {
194
+ value = JSON.parse(raw);
195
+ } catch {
196
+ const first = raw.split('\n').find(line => /^[\[{]/.test(line.trim()));
197
+ if (!first) return null;
198
+ try { value = JSON.parse(first.trim()); } catch { return null; }
199
+ }
200
+ return summarizeJsonOutput(value);
201
+ }
202
+
203
+ function summarizeJsonOutput(value) {
204
+ if (Array.isArray(value)) {
205
+ return { text: `json array · ${value.length} item${value.length === 1 ? '' : 's'}`, tone: 'success' };
206
+ }
207
+ if (!value || typeof value !== 'object') return null;
208
+
209
+ if ('service' in value && 'profile' in value && 'inSync' in value) {
210
+ const service = String(value.service || 'service');
211
+ const profile = value.profile ? ` · ${value.profile}` : '';
212
+ const status = value.inSync === true ? 'in sync'
213
+ : value.inSync === false ? 'out of sync'
214
+ : 'sync status unknown';
215
+ const diffs = Array.isArray(value.diff) ? value.diff
216
+ : Array.isArray(value.diffs) ? value.diffs
217
+ : [];
218
+ const diffText = diffs.length ? ` · ${diffs.length} diff${diffs.length === 1 ? '' : 's'}` : '';
219
+ return {
220
+ text: `${service} ${status}${profile}${diffText}`,
221
+ tone: value.inSync === false ? 'warn' : 'success',
222
+ };
223
+ }
224
+
225
+ const keys = Object.keys(value).slice(0, 4);
226
+ return {
227
+ text: keys.length ? `json · ${keys.join(', ')}` : 'json object',
228
+ tone: 'success',
229
+ };
230
+ }
231
+
182
232
  export function formatCompactFileDiff(result, {
183
233
  indent = ' ',
184
234
  maxLines = 14,
@@ -303,20 +353,30 @@ function tone(text, t) {
303
353
  export function formatCardHead(tool, args, opts = {}) {
304
354
  const cwd = opts.cwd || safeCwd();
305
355
  const cols = opts.columns || term().columns || 120;
306
- const indent = opts.indent || ' ';
356
+ const indent = opts.indent ?? (tool === 'shell' ? '' : ' ');
307
357
 
308
358
  const label = toolDisplayLabel(tool);
309
359
  const argsText = formatArgs(tool, args, cwd);
360
+ const leadText = formatHeadLead(tool, label);
310
361
 
311
- const leadVisible = visibleWidth(`${indent}${label}`);
362
+ const leadVisible = visibleWidth(`${indent}${leadText}`);
312
363
  const budget = Math.max(20, cols - leadVisible - 4);
313
364
 
365
+ if (tool === 'shell') {
366
+ const profile = shellCommandProfile(toolDisplaySummary(tool, args || {}, { cwd }), { cwd });
367
+ if (profile.compact) {
368
+ const argsTruncated = truncateMiddle(argsText, budget);
369
+ const head = `${indent}${leadText}`;
370
+ return argsTruncated ? `${head} ${argsTruncated}` : head;
371
+ }
372
+ }
373
+
314
374
  if (tool === 'shell' && visibleWidth(argsText) > budget) {
315
375
  const wrapWidth = Math.max(32, cols - visibleWidth(indent) - 4);
316
376
  const display = shellCommandDisplay(toolDisplaySummary(tool, args || {}, { cwd }), { cwd });
317
377
  const commandLines = wrapCommand(display.command, wrapWidth)
318
- .map(line => `${indent}${paint.text.dim(' ')}${formatShellCommand(line, paintShellAdapter)}`);
319
- const head = `${indent}${paintLabel(tool, label)}`;
378
+ .map((line, index) => `${indent}${paint.text.dim(index === 0 ? '$ ' : '> ')}${formatShellCommand(line, paintShellAdapter)}`);
379
+ const head = `${indent}${leadText}`;
320
380
  const cwdLine = display.cwdLabel
321
381
  ? `\n${indent}${paint.text.dim(' in ')}${paint.brand.data(display.cwdLabel)}`
322
382
  : '';
@@ -325,10 +385,22 @@ export function formatCardHead(tool, args, opts = {}) {
325
385
 
326
386
  const argsTruncated = truncateMiddle(argsText, budget);
327
387
 
328
- const head = `${indent}${paintLabel(tool, label)}`;
388
+ const head = `${indent}${leadText}`;
329
389
  return argsTruncated ? `${head} ${argsTruncated}` : head;
330
390
  }
331
391
 
392
+ function formatHeadLead(tool, label) {
393
+ if (tool !== 'shell') return paintLabel(tool, label);
394
+ return `${paint.text.dim('• shell ·')} ${paintLabel(tool, label)}`;
395
+ }
396
+
397
+ function compactShellProfile(profile) {
398
+ const parts = [`${paint.text.dim('$')} ${paint.text.primary(profile.summary)}`];
399
+ if (profile.cwdLabel) parts.push(`${paint.text.dim('in')} ${paint.brand.data(profile.cwdLabel)}`);
400
+ parts.push(paint.text.dim(profile.detailHint || 'details: F2 or /last'));
401
+ return parts.join(' · ');
402
+ }
403
+
332
404
  /**
333
405
  * Render a full card with outcome.
334
406
  *
@@ -346,7 +418,7 @@ export function formatCard({ tool, args, result, durationMs, indent, columns, cw
346
418
 
347
419
  if (!summary.text && !duration) return head;
348
420
 
349
- const arrow = paint.text.dim('—');
421
+ const arrow = outcomeLead(tool);
350
422
  const body = summary.text ? tone(summary.text, summary.tone) : '';
351
423
  // Hide the duration tail when the tool was effectively instant (<200ms).
352
424
  // For fast reads, "1ms" / "0ms" was noise that broke the prose feel.
@@ -374,6 +446,19 @@ export function formatCard({ tool, args, result, durationMs, indent, columns, cw
374
446
  return `${head}\n${gutterIndent}${arrow} ${body}${tail}`;
375
447
  }
376
448
 
449
+ function outcomeLead(tool) {
450
+ return isShellOutcomeTool(tool)
451
+ ? `${paint.text.dim('result')} ${paint.text.dim('—')}`
452
+ : paint.text.dim('—');
453
+ }
454
+
455
+ function isShellOutcomeTool(tool) {
456
+ return [
457
+ 'shell', 'run_tests', 'validate_build', 'lint_check',
458
+ 'validate_file', 'validate_structure',
459
+ ].includes(String(tool || '').toLowerCase());
460
+ }
461
+
377
462
  function isInlineOutcomeTool(tool) {
378
463
  return [
379
464
  'read_file', 'read_files', 'read_batch', 'get_file_info',
@@ -397,22 +482,25 @@ function wrapCommand(command, width) {
397
482
  const text = String(command || '');
398
483
  if (!text) return ['(empty command)'];
399
484
  const lines = [];
400
- let line = '';
401
- for (const token of text.match(/\S+\s*/g) || [text]) {
402
- const next = line + token;
403
- if (line && visibleWidth(next.trimEnd()) > width) {
404
- lines.push(line.trimEnd());
405
- line = token;
406
- continue;
407
- }
408
- if (!line && visibleWidth(token.trimEnd()) > width) {
409
- lines.push(...chunkLongToken(token.trimEnd(), width));
410
- line = '';
411
- continue;
485
+ for (const physicalLine of text.replace(/\r\n?/g, '\n').split('\n')) {
486
+ let line = '';
487
+ for (const token of physicalLine.match(/\S+\s*/g) || [physicalLine]) {
488
+ const next = line + token;
489
+ if (line && visibleWidth(next.trimEnd()) > width) {
490
+ lines.push(line.trimEnd());
491
+ line = token;
492
+ continue;
493
+ }
494
+ if (!line && visibleWidth(token.trimEnd()) > width) {
495
+ lines.push(...chunkLongToken(token.trimEnd(), width));
496
+ line = '';
497
+ continue;
498
+ }
499
+ line = next;
412
500
  }
413
- line = next;
501
+ if (line.trimEnd()) lines.push(line.trimEnd());
502
+ else if (!physicalLine.trim()) lines.push('');
414
503
  }
415
- if (line.trimEnd()) lines.push(line.trimEnd());
416
504
  return lines.length ? lines : ['(empty command)'];
417
505
  }
418
506
 
@@ -13,9 +13,10 @@
13
13
 
14
14
  import { paint } from './palette.mjs';
15
15
  import { icon, toolFamily } from './icons.mjs';
16
- import { toolDisplayLabel } from '../terminal/tool-display.mjs';
16
+ import { shellCommandProfile, toolDisplayLabel, toolDisplaySummary } from '../terminal/tool-display.mjs';
17
17
 
18
18
  const MAX_DETAIL_LINES = 60;
19
+ const MAX_SHELL_DETAIL_LINES = 220;
19
20
  const MAX_LINE_WIDTH = 220;
20
21
 
21
22
  // ── Dispatch ─────────────────────────────────────────────────────────────
@@ -49,6 +50,10 @@ function renderBody(card) {
49
50
  case 'validate_file':
50
51
  case 'validate_structure': return detailValidator(card);
51
52
  case 'plan': return detailPlan(card);
53
+ case 'Agent':
54
+ case 'agent':
55
+ case 'task': return detailAgent(card);
56
+ case 'sub_agent_tools': return detailSubAgentTools(card);
52
57
  case 'explore':
53
58
  case 'verify':
54
59
  case 'debug':
@@ -96,6 +101,13 @@ function oneLineArgs(tool, args) {
96
101
  }
97
102
 
98
103
  function safeDetailArgs(tool, args) {
104
+ if (tool === 'shell') {
105
+ const profile = shellCommandProfile(args?.command || args?.cmd || '');
106
+ return {
107
+ command: profile.summary,
108
+ ...(profile.cwdLabel ? { cwd: profile.cwdLabel } : {}),
109
+ };
110
+ }
99
111
  if (tool === 'write_file') {
100
112
  const { content, ...rest } = args || {};
101
113
  return {
@@ -112,6 +124,16 @@ function safeDetailArgs(tool, args) {
112
124
  })),
113
125
  };
114
126
  }
127
+ if (tool === 'Agent' || tool === 'agent' || tool === 'task') {
128
+ const prompt = args?.prompt || args?.task || args?.query || args?.description || args?.instruction || '';
129
+ return {
130
+ ...(args?.subagent_type ? { subagent_type: args.subagent_type } : {}),
131
+ ...(args?.agent ? { agent: args.agent } : {}),
132
+ ...(args?.name ? { name: args.name } : {}),
133
+ ...(Array.isArray(args?.allowed_tools) ? { allowed_tools: args.allowed_tools } : {}),
134
+ ...(prompt ? { prompt: `[${String(prompt).split('\n').length} lines] ${String(prompt).split('\n').find(Boolean)?.slice(0, 80) || ''}` } : {}),
135
+ };
136
+ }
115
137
  if (tool === 'edit_file') {
116
138
  const next = { ...args };
117
139
  for (const key of ['search', 'replace', 'old_string', 'new_string']) {
@@ -235,17 +257,37 @@ function detailDeleteFile(card) {
235
257
  // ── Shell / validators ──────────────────────────────────────────────────
236
258
 
237
259
  function detailShell(card) {
260
+ const command = String(card.args?.command || card.args?.cmd || '').trim();
238
261
  const stdout = String(card.result?.stdout ?? card.result?.output ?? '');
239
262
  const stderr = String(card.result?.stderr ?? '');
240
263
  const out = [];
264
+ if (command) {
265
+ const profile = shellCommandProfile(command);
266
+ if (profile.cwdLabel) {
267
+ out.push(paint.text.dim(' cwd'));
268
+ out.push(` ${paint.brand.data(profile.cwdLabel)}`);
269
+ out.push('');
270
+ }
271
+
272
+ out.push(paint.text.dim(' command'));
273
+ if (profile.script?.body) {
274
+ out.push(` ${paint.text.primary(profile.script.invocation || profile.command.split('\n')[0] || profile.command)}`);
275
+ out.push('');
276
+ out.push(paint.text.dim(' script'));
277
+ out.push(numbered(profile.script.body, 1, { maxLines: MAX_SHELL_DETAIL_LINES }));
278
+ } else {
279
+ out.push(clip(profile.command, paint.text.primary, { maxLines: MAX_SHELL_DETAIL_LINES }));
280
+ }
281
+ }
241
282
  if (stdout) {
283
+ if (out.length) out.push('');
242
284
  out.push(paint.text.dim(' stdout'));
243
- out.push(clip(stdout));
285
+ out.push(clip(stdout, paint.text.primary, { maxLines: MAX_DETAIL_LINES }));
244
286
  }
245
287
  if (stderr) {
246
288
  if (out.length) out.push('');
247
289
  out.push(paint.state.warn(' stderr'));
248
- out.push(clip(stderr, paint.state.danger));
290
+ out.push(clip(stderr, paint.state.danger, { maxLines: MAX_DETAIL_LINES }));
249
291
  }
250
292
  return out.length ? out.join('\n') : paint.text.dim(' (no output)');
251
293
  }
@@ -267,6 +309,56 @@ function detailPlan(card) {
267
309
  }).join('\n');
268
310
  }
269
311
 
312
+ function detailAgent(card) {
313
+ const args = card.args || {};
314
+ const prompt = args.prompt || args.task || args.query || args.description || args.instruction || '';
315
+ const out = [];
316
+ const meta = [
317
+ args.subagent_type ? `type ${args.subagent_type}` : '',
318
+ args.agent || args.name || '',
319
+ Array.isArray(args.allowed_tools) && args.allowed_tools.length
320
+ ? `${args.allowed_tools.length} allowed tools`
321
+ : '',
322
+ ].filter(Boolean).join(' · ');
323
+ if (meta) out.push(` ${paint.text.dim(meta)}`);
324
+ if (prompt) {
325
+ out.push(paint.text.dim(' prompt'));
326
+ out.push(clip(prompt, paint.text.primary, { maxLines: MAX_DETAIL_LINES }));
327
+ }
328
+ const result = String(card.result?.output ?? card.result?.output_preview ?? '');
329
+ if (result) {
330
+ if (out.length) out.push('');
331
+ out.push(paint.text.dim(' result'));
332
+ out.push(clip(result));
333
+ }
334
+ return out.length ? out.join('\n') : detailGenericOutput(card);
335
+ }
336
+
337
+ function detailSubAgentTools(card) {
338
+ const entries = Array.isArray(card.result?.tools) ? card.result.tools : [];
339
+ if (!entries.length) return paint.text.dim(' (no folded tool calls)');
340
+
341
+ const out = [];
342
+ entries.forEach((entry, index) => {
343
+ const tool = entry.tool || 'tool';
344
+ const args = entry.args || {};
345
+ const summary = entry.summary || toolDisplaySummary(tool, args);
346
+ const outcome = entry.outcome ? ` ${paint.text.dim('—')} ${paint.text.muted(entry.outcome)}` : '';
347
+ const duration = entry.durationMs != null ? ` ${paint.text.dim('· ' + formatDuration(entry.durationMs))}` : '';
348
+ out.push(` ${paint.text.dim(`${index + 1}.`)} ${icon(tool)} ${toolDisplayLabel(tool)}${summary ? ` ${paint.text.muted(summary)}` : ''}${outcome}${duration}`);
349
+
350
+ const child = {
351
+ tool,
352
+ args,
353
+ result: entry.result || null,
354
+ durationMs: entry.durationMs ?? null,
355
+ };
356
+ const detail = renderBody(child);
357
+ if (detail) out.push(indentBlock(detail, ' '));
358
+ });
359
+ return out.join('\n');
360
+ }
361
+
270
362
  function detailGenericOutput(card) {
271
363
  const out = String(card.result?.output ?? card.result?.output_preview ?? '');
272
364
  return clip(out);
@@ -274,28 +366,35 @@ function detailGenericOutput(card) {
274
366
 
275
367
  // ── Helpers ─────────────────────────────────────────────────────────────
276
368
 
277
- function numbered(text, start) {
369
+ function numbered(text, start, { maxLines = MAX_DETAIL_LINES } = {}) {
278
370
  const lines = String(text).split('\n');
279
371
  const total = lines.length;
280
372
  const width = String(start + total - 1).length;
281
- return lines.slice(0, MAX_DETAIL_LINES).map((line, i) => {
373
+ return lines.slice(0, maxLines).map((line, i) => {
282
374
  const n = String(start + i).padStart(width);
283
375
  return ` ${paint.text.dim(n)} ${paint.text.primary(line.slice(0, MAX_LINE_WIDTH))}`;
284
- }).join('\n') + (total > MAX_DETAIL_LINES
285
- ? `\n ${paint.text.dim(`… ${total - MAX_DETAIL_LINES} more line(s)`)}`
376
+ }).join('\n') + (total > maxLines
377
+ ? `\n ${paint.text.dim(`… ${total - maxLines} more line(s)`)}`
286
378
  : '');
287
379
  }
288
380
 
289
- function clip(text, painter = paint.text.primary) {
381
+ function clip(text, painter = paint.text.primary, { maxLines = MAX_DETAIL_LINES } = {}) {
290
382
  if (!text) return paint.text.dim(' (empty)');
291
383
  const lines = String(text).split('\n');
292
- const head = lines.slice(0, MAX_DETAIL_LINES).map(l => ` ${painter(l.slice(0, MAX_LINE_WIDTH))}`);
293
- if (lines.length > MAX_DETAIL_LINES) {
294
- head.push(` ${paint.text.dim(`… ${lines.length - MAX_DETAIL_LINES} more line(s)`)}`);
384
+ const head = lines.slice(0, maxLines).map(l => ` ${painter(l.slice(0, MAX_LINE_WIDTH))}`);
385
+ if (lines.length > maxLines) {
386
+ head.push(` ${paint.text.dim(`… ${lines.length - maxLines} more line(s)`)}`);
295
387
  }
296
388
  return head.join('\n');
297
389
  }
298
390
 
391
+ function indentBlock(text, prefix) {
392
+ return String(text || '')
393
+ .split('\n')
394
+ .map(line => `${prefix}${line.trimStart()}`)
395
+ .join('\n');
396
+ }
397
+
299
398
  function renderDiff(text) {
300
399
  return text.split('\n').slice(0, MAX_DETAIL_LINES).map(line => {
301
400
  if (line.startsWith('+++') || line.startsWith('---')) return ` ${paint.bold(paint.text.muted(line))}`;