@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.
- package/package.json +4 -4
- package/src/agents/scaffold.mjs +1 -0
- package/src/commands/agent.mjs +3 -2
- package/src/core/approval-log.mjs +22 -4
- package/src/core/approval.mjs +172 -24
- package/src/core/headless.mjs +14 -3
- package/src/core/local-agent.mjs +3 -2
- package/src/core/tool-executor.mjs +13 -3
- package/src/index.mjs +1 -1
- package/src/terminal/agents.mjs +194 -18
- package/src/terminal/repl-render.mjs +22 -4
- package/src/terminal/repl-state.mjs +1 -0
- package/src/terminal/repl.mjs +395 -21
- package/src/terminal/tool-display.mjs +135 -2
- package/src/ui/approval.mjs +200 -14
- package/src/ui/icons.mjs +11 -5
- package/src/ui/input-dock.mjs +90 -19
- package/src/ui/slash-commands.mjs +10 -0
- package/src/ui/tool-card.mjs +109 -21
- package/src/ui/tool-details.mjs +110 -11
- package/src/ui/transcript-block.mjs +2 -3
package/src/ui/input-dock.mjs
CHANGED
|
@@ -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(
|
|
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,
|
|
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,
|
|
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
|
|
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
|
|
201
|
-
|
|
202
|
-
|
|
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,
|
|
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
|
-
|
|
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',
|
package/src/ui/tool-card.mjs
CHANGED
|
@@ -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}${
|
|
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('
|
|
319
|
-
const head = `${indent}${
|
|
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}${
|
|
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 =
|
|
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
|
-
|
|
401
|
-
|
|
402
|
-
const
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
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
|
|
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
|
|
package/src/ui/tool-details.mjs
CHANGED
|
@@ -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,
|
|
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 >
|
|
285
|
-
? `\n ${paint.text.dim(`… ${total -
|
|
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,
|
|
293
|
-
if (lines.length >
|
|
294
|
-
head.push(` ${paint.text.dim(`… ${lines.length -
|
|
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))}`;
|