@bahulam/code 2.6.14 → 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.
- package/package.json +4 -4
- package/src/core/approval-log.mjs +23 -0
- package/src/core/approval.mjs +93 -17
- package/src/core/file-diff.mjs +1 -1
- package/src/core/risk-tier.mjs +53 -2
- package/src/core/safety.mjs +61 -4
- package/src/core/tool-executor.mjs +25 -13
- package/src/core/trust.mjs +5 -3
- package/src/index.mjs +1 -1
- package/src/terminal/repl-render.mjs +104 -7
- package/src/terminal/repl-state.mjs +1 -0
- package/src/terminal/repl.mjs +158 -67
- package/src/terminal/tool-display.mjs +20 -1
- package/src/ui/approval.mjs +11 -0
- package/src/ui/input-dock.mjs +127 -13
- package/src/ui/tool-card.mjs +156 -13
- package/src/ui/tool-details.mjs +96 -3
package/src/ui/input-dock.mjs
CHANGED
|
@@ -52,6 +52,7 @@ const INPUT_RIGHT_PAD = 2;
|
|
|
52
52
|
const META_INDENT = 4;
|
|
53
53
|
|
|
54
54
|
const DEFAULT_MAX_INPUT_ROWS = 6;
|
|
55
|
+
const DEFAULT_OVERLAY_MAX_ROWS = 8;
|
|
55
56
|
const MIN_INPUT_ROWS = 1;
|
|
56
57
|
const MAX_INPUT_ROWS_CAP = 12;
|
|
57
58
|
|
|
@@ -74,8 +75,22 @@ let unsubResize = null;
|
|
|
74
75
|
let lastFrame = { context: '', meta: '', tips: '', prefix: '', value: '', cursor: null, overlayLines: null };
|
|
75
76
|
let resetting = false;
|
|
76
77
|
let lastGeometry = null;
|
|
77
|
-
|
|
78
|
-
|
|
78
|
+
let contentCursorRow = 1;
|
|
79
|
+
let contentCursorCol = 1;
|
|
80
|
+
let contentTrackingActive = false;
|
|
81
|
+
let suppressWriteTracking = 0;
|
|
82
|
+
let originalStdoutWrite = null;
|
|
83
|
+
let originalStderrWrite = null;
|
|
84
|
+
|
|
85
|
+
function write(s) {
|
|
86
|
+
try {
|
|
87
|
+
suppressWriteTracking++;
|
|
88
|
+
OUT.write(s);
|
|
89
|
+
} catch {
|
|
90
|
+
} finally {
|
|
91
|
+
suppressWriteTracking = Math.max(0, suppressWriteTracking - 1);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
79
94
|
function setScrollRegion(top, bottom) { write(`${ESC}${top};${bottom}r`); }
|
|
80
95
|
function clearScrollRegion() { write(`${ESC}r`); }
|
|
81
96
|
function saveCursor() { write(`${ESC}s`); }
|
|
@@ -102,6 +117,69 @@ function contentBottomRow() {
|
|
|
102
117
|
return Math.max(1, rows() - reservedRows);
|
|
103
118
|
}
|
|
104
119
|
|
|
120
|
+
function clampContentCursor() {
|
|
121
|
+
const bottom = contentBottomRow();
|
|
122
|
+
contentCursorRow = Math.max(1, Math.min(bottom, contentCursorRow || 1));
|
|
123
|
+
contentCursorCol = Math.max(1, Math.min(cols(), contentCursorCol || 1));
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function resetContentCursor(row = 1, col = 1) {
|
|
127
|
+
contentCursorRow = Math.max(1, Math.min(contentBottomRow(), Math.floor(row || 1)));
|
|
128
|
+
contentCursorCol = Math.max(1, Math.min(cols(), Math.floor(col || 1)));
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function trackContentWrite(chunk) {
|
|
132
|
+
if (!mounted || !contentTrackingActive || suppressWriteTracking > 0) return;
|
|
133
|
+
const text = Buffer.isBuffer(chunk) ? chunk.toString('utf8') : String(chunk ?? '');
|
|
134
|
+
if (!text) return;
|
|
135
|
+
const clean = text
|
|
136
|
+
.replace(/\x1b\[[0-9;?]*[ -/]*[@-~]/g, '')
|
|
137
|
+
.replace(/\x1b[()][A-Za-z0-9]/g, '');
|
|
138
|
+
const bottom = contentBottomRow();
|
|
139
|
+
const width = Math.max(1, drawableColumns());
|
|
140
|
+
for (const ch of clean) {
|
|
141
|
+
if (ch === '\r') {
|
|
142
|
+
contentCursorCol = 1;
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
if (ch === '\n') {
|
|
146
|
+
contentCursorRow = Math.min(bottom, contentCursorRow + 1);
|
|
147
|
+
contentCursorCol = 1;
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
contentCursorCol++;
|
|
151
|
+
if (contentCursorCol > width) {
|
|
152
|
+
contentCursorRow = Math.min(bottom, contentCursorRow + 1);
|
|
153
|
+
contentCursorCol = 1;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function patchOutputTracking() {
|
|
159
|
+
if (originalStdoutWrite || originalStderrWrite) return;
|
|
160
|
+
originalStdoutWrite = process.stdout.write.bind(process.stdout);
|
|
161
|
+
originalStderrWrite = process.stderr.write.bind(process.stderr);
|
|
162
|
+
process.stdout.write = function trackedStdoutWrite(chunk, ...args) {
|
|
163
|
+
trackContentWrite(chunk);
|
|
164
|
+
return originalStdoutWrite(chunk, ...args);
|
|
165
|
+
};
|
|
166
|
+
process.stderr.write = function trackedStderrWrite(chunk, ...args) {
|
|
167
|
+
trackContentWrite(chunk);
|
|
168
|
+
return originalStderrWrite(chunk, ...args);
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function unpatchOutputTracking() {
|
|
173
|
+
if (originalStdoutWrite) {
|
|
174
|
+
process.stdout.write = originalStdoutWrite;
|
|
175
|
+
originalStdoutWrite = null;
|
|
176
|
+
}
|
|
177
|
+
if (originalStderrWrite) {
|
|
178
|
+
process.stderr.write = originalStderrWrite;
|
|
179
|
+
originalStderrWrite = null;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
105
183
|
// Row map (top → bottom of the reserved region).
|
|
106
184
|
function topRuleRow() { return contentBottomRow() + 1; }
|
|
107
185
|
function spacerAboveRow() { return topRuleRow() + 1; }
|
|
@@ -125,6 +203,18 @@ function resolveMaxInputRows(requested) {
|
|
|
125
203
|
return Math.max(MIN_INPUT_ROWS, Math.min(MAX_INPUT_ROWS_CAP, n));
|
|
126
204
|
}
|
|
127
205
|
|
|
206
|
+
function resolveOverlayRowCap(requested = DEFAULT_OVERLAY_MAX_ROWS) {
|
|
207
|
+
const n = Number.parseInt(String(requested), 10);
|
|
208
|
+
if (!Number.isFinite(n)) return DEFAULT_OVERLAY_MAX_ROWS;
|
|
209
|
+
return Math.max(MIN_INPUT_ROWS, Math.min(MAX_INPUT_ROWS_CAP, n));
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function overlayRowsForWrapped(wrappedLength, requestedMaxRows = DEFAULT_OVERLAY_MAX_ROWS) {
|
|
213
|
+
const rowCap = resolveOverlayRowCap(requestedMaxRows);
|
|
214
|
+
const wanted = Math.max(MIN_INPUT_ROWS, Math.floor(Number(wrappedLength) || 0));
|
|
215
|
+
return Math.min(rowCap, wanted);
|
|
216
|
+
}
|
|
217
|
+
|
|
128
218
|
// How many input rows does this (prefix + value) buffer need? Wrapped line
|
|
129
219
|
// count clamped to [1, inputRowsMax]. Beyond the cap, tail-with-ellipsis
|
|
130
220
|
// takes over inside drawInputLines so at most inputRowsMax rows render.
|
|
@@ -250,6 +340,7 @@ function applyLayout({ clearPrevious = false } = {}) {
|
|
|
250
340
|
const bottom = contentBottomRow();
|
|
251
341
|
setScrollRegion(1, bottom);
|
|
252
342
|
renderFrame(lastFrame);
|
|
343
|
+
clampContentCursor();
|
|
253
344
|
// On (re)mount and resize, park at input if we have one; otherwise sit at
|
|
254
345
|
// the bottom of the content region so any pending content writes flush
|
|
255
346
|
// above the dock rather than into a stale mid-frame position.
|
|
@@ -385,7 +476,11 @@ export function isInputDockMounted() {
|
|
|
385
476
|
return mounted;
|
|
386
477
|
}
|
|
387
478
|
|
|
388
|
-
export function mountInputDock({
|
|
479
|
+
export function mountInputDock({
|
|
480
|
+
inputRowsMax: requestedMax,
|
|
481
|
+
initialContentRow = 1,
|
|
482
|
+
initialContentCol = 1,
|
|
483
|
+
} = {}) {
|
|
389
484
|
const t = term();
|
|
390
485
|
if (!t.isTTY || t.plain) return false;
|
|
391
486
|
if (t.ttyMode !== 'rich' || t.fixedInput === false) return false;
|
|
@@ -395,7 +490,10 @@ export function mountInputDock({ inputRowsMax: requestedMax } = {}) {
|
|
|
395
490
|
inputRowsMax = resolveMaxInputRows(requestedMax);
|
|
396
491
|
inputRows = MIN_INPUT_ROWS;
|
|
397
492
|
reservedRows = FIXED_ROWS + inputRows;
|
|
493
|
+
resetContentCursor(initialContentRow, initialContentCol);
|
|
494
|
+
contentTrackingActive = false;
|
|
398
495
|
mounted = true;
|
|
496
|
+
patchOutputTracking();
|
|
399
497
|
applyLayout();
|
|
400
498
|
|
|
401
499
|
unsubResize = onResize(() => applyLayout({ clearPrevious: true }));
|
|
@@ -415,6 +513,8 @@ export function unmountInputDock() {
|
|
|
415
513
|
if (unsubResize) { unsubResize(); unsubResize = null; }
|
|
416
514
|
} finally {
|
|
417
515
|
mounted = false;
|
|
516
|
+
contentTrackingActive = false;
|
|
517
|
+
unpatchOutputTracking();
|
|
418
518
|
resetting = false;
|
|
419
519
|
lastGeometry = null;
|
|
420
520
|
}
|
|
@@ -424,17 +524,19 @@ function safeUnmount() { try { unmountInputDock(); } catch {} }
|
|
|
424
524
|
|
|
425
525
|
export function moveToContent() {
|
|
426
526
|
if (!mounted) return false;
|
|
427
|
-
|
|
527
|
+
clampContentCursor();
|
|
528
|
+
contentTrackingActive = true;
|
|
529
|
+
moveTo(contentCursorRow, contentCursorCol);
|
|
428
530
|
return true;
|
|
429
531
|
}
|
|
430
532
|
|
|
431
|
-
//
|
|
432
|
-
//
|
|
433
|
-
//
|
|
434
|
-
// without piling copies of itself into scrollback.
|
|
533
|
+
// The next transcript row. Spinner/status overlays live next to the latest
|
|
534
|
+
// content instead of near the scroll-region bottom; otherwise sparse agent
|
|
535
|
+
// events leave large blank holes between visible lines.
|
|
435
536
|
export function pinnedStatusRow() {
|
|
436
537
|
if (!mounted) return null;
|
|
437
|
-
|
|
538
|
+
clampContentCursor();
|
|
539
|
+
return Math.max(1, Math.min(contentBottomRow(), contentCursorRow));
|
|
438
540
|
}
|
|
439
541
|
|
|
440
542
|
export function drawPinnedStatus(line) {
|
|
@@ -466,6 +568,7 @@ export function clearPinnedStatus() {
|
|
|
466
568
|
// clearing their overlay.
|
|
467
569
|
export function redrawDockFrame() {
|
|
468
570
|
if (!mounted) return false;
|
|
571
|
+
contentTrackingActive = false;
|
|
469
572
|
renderFrame(lastFrame);
|
|
470
573
|
parkCursorAtInput();
|
|
471
574
|
return true;
|
|
@@ -473,6 +576,7 @@ export function redrawDockFrame() {
|
|
|
473
576
|
|
|
474
577
|
export function prepareInputPrompt({ context = '', tips = '', meta = '' } = {}) {
|
|
475
578
|
if (!mounted) return false;
|
|
579
|
+
contentTrackingActive = false;
|
|
476
580
|
setInputRowsTo(MIN_INPUT_ROWS);
|
|
477
581
|
clearInputRows();
|
|
478
582
|
renderFrame({ context, tips, meta, prefix: '', value: '', overlayLines: null });
|
|
@@ -482,9 +586,11 @@ export function prepareInputPrompt({ context = '', tips = '', meta = '' } = {})
|
|
|
482
586
|
|
|
483
587
|
export function clearInputPrompt() {
|
|
484
588
|
if (!mounted) return false;
|
|
485
|
-
|
|
589
|
+
contentTrackingActive = false;
|
|
486
590
|
lastFrame.value = '';
|
|
487
591
|
lastFrame.overlayLines = null;
|
|
592
|
+
setInputRowsTo(MIN_INPUT_ROWS);
|
|
593
|
+
clearInputRows();
|
|
488
594
|
renderFrame(lastFrame);
|
|
489
595
|
parkCursorAtInput();
|
|
490
596
|
return true;
|
|
@@ -492,6 +598,7 @@ export function clearInputPrompt() {
|
|
|
492
598
|
|
|
493
599
|
export function renderDockInput(prefix, value, { context = '', tips = '', meta = '', cursor = null } = {}) {
|
|
494
600
|
if (!mounted) return false;
|
|
601
|
+
contentTrackingActive = false;
|
|
495
602
|
setInputRowsTo(computeInputRowsForBuffer(prefix, value));
|
|
496
603
|
renderFrame({ context, tips, meta, prefix, value, cursor, overlayLines: null });
|
|
497
604
|
const layout = layoutInput(prefix, value);
|
|
@@ -505,13 +612,14 @@ export function renderDockOverlay({
|
|
|
505
612
|
lines = [],
|
|
506
613
|
meta = '',
|
|
507
614
|
tips = '',
|
|
508
|
-
maxRows =
|
|
615
|
+
maxRows = DEFAULT_OVERLAY_MAX_ROWS,
|
|
509
616
|
} = {}) {
|
|
510
617
|
if (!mounted) return false;
|
|
618
|
+
contentTrackingActive = false;
|
|
511
619
|
const sourceLines = Array.isArray(lines) ? lines : String(lines || '').split('\n');
|
|
512
620
|
const wrapped = layoutOverlayLines(sourceLines);
|
|
513
|
-
const rowCap =
|
|
514
|
-
setInputRowsTo(
|
|
621
|
+
const rowCap = resolveOverlayRowCap(maxRows);
|
|
622
|
+
setInputRowsTo(overlayRowsForWrapped(wrapped.length, rowCap), { maxRows: rowCap });
|
|
515
623
|
const tail = tailWithEllipsis(wrapped, inputRows);
|
|
516
624
|
renderFrame({
|
|
517
625
|
context,
|
|
@@ -537,6 +645,7 @@ export function renderDockOverlay({
|
|
|
537
645
|
*/
|
|
538
646
|
export function focusDockInput(prefix, value = '', cursorInValue = null) {
|
|
539
647
|
if (!mounted) return false;
|
|
648
|
+
contentTrackingActive = false;
|
|
540
649
|
const layout = layoutInput(prefix, value);
|
|
541
650
|
const valueStr = String(value || '');
|
|
542
651
|
const rawCursor = cursorInValue == null
|
|
@@ -570,7 +679,12 @@ export function _internals() {
|
|
|
570
679
|
bottomRuleLine,
|
|
571
680
|
padLine,
|
|
572
681
|
drawableColumns,
|
|
682
|
+
resetContentCursor,
|
|
683
|
+
contentCursor: () => ({ row: contentCursorRow, col: contentCursorCol, active: contentTrackingActive }),
|
|
684
|
+
overlayRowsForWrapped,
|
|
573
685
|
FIXED_ROWS,
|
|
686
|
+
MAX_INPUT_ROWS_CAP,
|
|
687
|
+
DEFAULT_OVERLAY_MAX_ROWS,
|
|
574
688
|
BRAND_LABEL,
|
|
575
689
|
};
|
|
576
690
|
}
|
package/src/ui/tool-card.mjs
CHANGED
|
@@ -231,32 +231,47 @@ function summarizeJsonOutput(value) {
|
|
|
231
231
|
|
|
232
232
|
export function formatCompactFileDiff(result, {
|
|
233
233
|
indent = ' ',
|
|
234
|
-
maxLines =
|
|
235
|
-
maxFiles =
|
|
234
|
+
maxLines = Infinity,
|
|
235
|
+
maxFiles = Infinity,
|
|
236
236
|
columns = term().columns || 120,
|
|
237
|
+
showFileHeader = false,
|
|
237
238
|
} = {}) {
|
|
238
|
-
const diffs = fileDiffs(result)
|
|
239
|
+
const diffs = fileDiffs(result)
|
|
240
|
+
.map(normalizeFileDiff)
|
|
241
|
+
.filter(diff => diff && (diff.redacted || diff?.hunks?.length));
|
|
239
242
|
if (!diffs.length) return '';
|
|
240
243
|
|
|
241
244
|
const out = [];
|
|
242
245
|
let shown = 0;
|
|
243
246
|
let truncated = false;
|
|
247
|
+
const lineLimit = Number.isFinite(maxLines) ? Math.max(0, Math.floor(maxLines)) : Infinity;
|
|
248
|
+
const fileLimit = Number.isFinite(maxFiles) ? Math.max(0, Math.floor(maxFiles)) : diffs.length;
|
|
244
249
|
const lineBudget = Math.max(40, columns - visibleWidth(indent) - 4);
|
|
245
250
|
|
|
246
|
-
for (const diff of diffs.slice(0,
|
|
247
|
-
if (diffs.length > 1) {
|
|
248
|
-
if (shown >=
|
|
251
|
+
for (const diff of diffs.slice(0, fileLimit)) {
|
|
252
|
+
if (showFileHeader || diffs.length > 1) {
|
|
253
|
+
if (shown >= lineLimit) { truncated = true; break; }
|
|
249
254
|
out.push(`${indent}${paint.brand.primary(diff.relative_path || diff.path || 'file')} ${paint.text.dim(diffDelta(diff))}`);
|
|
250
255
|
shown++;
|
|
251
256
|
}
|
|
252
257
|
|
|
258
|
+
if (diff.redacted) {
|
|
259
|
+
if (shown >= lineLimit) { truncated = true; break; }
|
|
260
|
+
const subject = (showFileHeader || diffs.length > 1)
|
|
261
|
+
? ''
|
|
262
|
+
: `${[diff.relative_path || diff.path || 'file', diffDelta(diff)].filter(Boolean).join(' ')} · `;
|
|
263
|
+
out.push(`${indent}${paint.text.dim(`${subject}diff redacted for sensitive config`)}`);
|
|
264
|
+
shown++;
|
|
265
|
+
continue;
|
|
266
|
+
}
|
|
267
|
+
|
|
253
268
|
for (const hunk of diff.hunks || []) {
|
|
254
|
-
if (shown >=
|
|
269
|
+
if (shown >= lineLimit) { truncated = true; break; }
|
|
255
270
|
out.push(`${indent}${paint.text.dim(`@@ -${hunk.old_start},${hunk.old_count} +${hunk.new_start},${hunk.new_count} @@`)}`);
|
|
256
271
|
shown++;
|
|
257
272
|
|
|
258
273
|
for (const line of hunk.lines || []) {
|
|
259
|
-
if (shown >=
|
|
274
|
+
if (shown >= lineLimit) { truncated = true; break; }
|
|
260
275
|
out.push(`${indent}${paintDiffLine(line, lineBudget)}`);
|
|
261
276
|
shown++;
|
|
262
277
|
}
|
|
@@ -265,7 +280,7 @@ export function formatCompactFileDiff(result, {
|
|
|
265
280
|
if (truncated) break;
|
|
266
281
|
}
|
|
267
282
|
|
|
268
|
-
if (diffs.length >
|
|
283
|
+
if (diffs.length > fileLimit) truncated = true;
|
|
269
284
|
if (truncated) out.push(`${indent}${paint.text.dim('… diff preview truncated; use /last to expand')}`);
|
|
270
285
|
return out.join('\n');
|
|
271
286
|
}
|
|
@@ -274,9 +289,112 @@ function fileDiffs(result) {
|
|
|
274
289
|
if (!result) return [];
|
|
275
290
|
if (Array.isArray(result.file_diffs)) return result.file_diffs;
|
|
276
291
|
if (result.file_diff) return [result.file_diff];
|
|
292
|
+
if (result.type === 'file_diff' || result.hunks || result.unified) return [result];
|
|
277
293
|
return [];
|
|
278
294
|
}
|
|
279
295
|
|
|
296
|
+
function normalizeFileDiff(diff) {
|
|
297
|
+
if (!diff) return null;
|
|
298
|
+
return {
|
|
299
|
+
...diff,
|
|
300
|
+
hunks: normalizeHunks(diff),
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function normalizeHunks(diff) {
|
|
305
|
+
if (Array.isArray(diff?.hunks) && diff.hunks.length) {
|
|
306
|
+
return diff.hunks.map(normalizeHunk).filter(hunk => hunk.lines.length);
|
|
307
|
+
}
|
|
308
|
+
if (diff?.unified) return parseUnifiedHunks(diff.unified);
|
|
309
|
+
return [];
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function normalizeHunk(hunk = {}) {
|
|
313
|
+
const lines = Array.isArray(hunk.lines)
|
|
314
|
+
? hunk.lines.map(normalizeDiffLine).filter(Boolean)
|
|
315
|
+
: typeof hunk.body === 'string'
|
|
316
|
+
? parseDiffBody(hunk.body)
|
|
317
|
+
: [];
|
|
318
|
+
const oldCount = hunk.old_count ?? hunk.old_lines ?? hunk.oldCount ?? countDiffLines(lines, 'old');
|
|
319
|
+
const newCount = hunk.new_count ?? hunk.new_lines ?? hunk.newCount ?? countDiffLines(lines, 'new');
|
|
320
|
+
return {
|
|
321
|
+
...hunk,
|
|
322
|
+
old_start: hunk.old_start ?? hunk.oldStart ?? 1,
|
|
323
|
+
old_count: oldCount,
|
|
324
|
+
new_start: hunk.new_start ?? hunk.newStart ?? 1,
|
|
325
|
+
new_count: newCount,
|
|
326
|
+
lines,
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function normalizeDiffLine(line) {
|
|
331
|
+
if (typeof line === 'string') return parseUnifiedLine(line);
|
|
332
|
+
if (!line || typeof line !== 'object') return null;
|
|
333
|
+
const rawType = String(line.type || line.kind || '').toLowerCase();
|
|
334
|
+
const text = String(line.text ?? line.content ?? line.value ?? '');
|
|
335
|
+
if (rawType === 'add' || rawType === 'added' || rawType === '+') return { ...line, type: 'add', text };
|
|
336
|
+
if (rawType === 'remove' || rawType === 'removed' || rawType === 'delete' || rawType === '-') return { ...line, type: 'remove', text };
|
|
337
|
+
if (rawType === 'context' || rawType === 'same' || rawType === ' ') return { ...line, type: 'context', text };
|
|
338
|
+
return parseUnifiedLine(text);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function parseDiffBody(body) {
|
|
342
|
+
return String(body || '')
|
|
343
|
+
.replace(/\r\n?/g, '\n')
|
|
344
|
+
.split('\n')
|
|
345
|
+
.filter(line => line && !line.startsWith('@@'))
|
|
346
|
+
.map(parseUnifiedLine)
|
|
347
|
+
.filter(Boolean);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
function parseUnifiedHunks(unified) {
|
|
351
|
+
const hunks = [];
|
|
352
|
+
let current = null;
|
|
353
|
+
for (const raw of String(unified || '').replace(/\r\n?/g, '\n').split('\n')) {
|
|
354
|
+
if (raw.startsWith('--- ') || raw.startsWith('+++ ')) continue;
|
|
355
|
+
const header = raw.match(/^@@\s+-(\d+)(?:,(\d+))?\s+\+(\d+)(?:,(\d+))?\s+@@/);
|
|
356
|
+
if (header) {
|
|
357
|
+
current = {
|
|
358
|
+
old_start: Number(header[1]) || 1,
|
|
359
|
+
old_count: Number(header[2] || 1),
|
|
360
|
+
new_start: Number(header[3]) || 1,
|
|
361
|
+
new_count: Number(header[4] || 1),
|
|
362
|
+
lines: [],
|
|
363
|
+
};
|
|
364
|
+
hunks.push(current);
|
|
365
|
+
continue;
|
|
366
|
+
}
|
|
367
|
+
if (!current) {
|
|
368
|
+
if (!raw || (!raw.startsWith('+') && !raw.startsWith('-') && !raw.startsWith(' '))) continue;
|
|
369
|
+
current = { old_start: 1, old_count: 0, new_start: 1, new_count: 0, lines: [] };
|
|
370
|
+
hunks.push(current);
|
|
371
|
+
}
|
|
372
|
+
const line = parseUnifiedLine(raw);
|
|
373
|
+
if (line) current.lines.push(line);
|
|
374
|
+
}
|
|
375
|
+
for (const hunk of hunks) {
|
|
376
|
+
if (!hunk.old_count) hunk.old_count = countDiffLines(hunk.lines, 'old');
|
|
377
|
+
if (!hunk.new_count) hunk.new_count = countDiffLines(hunk.lines, 'new');
|
|
378
|
+
}
|
|
379
|
+
return hunks.filter(hunk => hunk.lines.length);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
function parseUnifiedLine(raw) {
|
|
383
|
+
const line = String(raw ?? '');
|
|
384
|
+
if (!line && raw !== '') return null;
|
|
385
|
+
if (line.startsWith('+') && !line.startsWith('+++')) return { type: 'add', text: line.slice(1) };
|
|
386
|
+
if (line.startsWith('-') && !line.startsWith('---')) return { type: 'remove', text: line.slice(1) };
|
|
387
|
+
if (line.startsWith(' ')) return { type: 'context', text: line.slice(1) };
|
|
388
|
+
return { type: 'context', text: line };
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
function countDiffLines(lines, side) {
|
|
392
|
+
return lines.filter(line => {
|
|
393
|
+
if (side === 'old') return line.type !== 'add';
|
|
394
|
+
return line.type !== 'remove';
|
|
395
|
+
}).length;
|
|
396
|
+
}
|
|
397
|
+
|
|
280
398
|
function paintDiffLine(line, maxWidth) {
|
|
281
399
|
const text = truncatePlain(String(line?.text ?? ''), Math.max(20, maxWidth - 2));
|
|
282
400
|
if (line?.type === 'add') return paint.state.success(`+ ${text}`);
|
|
@@ -365,8 +483,19 @@ export function formatCardHead(tool, args, opts = {}) {
|
|
|
365
483
|
if (tool === 'shell') {
|
|
366
484
|
const profile = shellCommandProfile(toolDisplaySummary(tool, args || {}, { cwd }), { cwd });
|
|
367
485
|
if (profile.compact) {
|
|
368
|
-
const argsTruncated = truncateMiddle(argsText, budget);
|
|
369
486
|
const head = `${indent}${leadText}`;
|
|
487
|
+
if (profile.preview) {
|
|
488
|
+
const fullArgs = compactShellProfile(profile);
|
|
489
|
+
if (visibleWidth(fullArgs) <= budget) return `${head} ${fullArgs}`;
|
|
490
|
+
const previewTail = `${paint.text.dim(' · preview:')} ${paint.text.primary(profile.preview)}`;
|
|
491
|
+
const baseArgs = compactShellProfile(profile, { includePreview: false, includeDetails: false });
|
|
492
|
+
const baseBudget = budget - visibleWidth(previewTail);
|
|
493
|
+
if (baseBudget >= 12) {
|
|
494
|
+
const baseTruncated = truncateEndVisible(baseArgs, baseBudget);
|
|
495
|
+
return `${head} ${baseTruncated}${previewTail}`;
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
const argsTruncated = truncateMiddle(argsText, budget);
|
|
370
499
|
return argsTruncated ? `${head} ${argsTruncated}` : head;
|
|
371
500
|
}
|
|
372
501
|
}
|
|
@@ -394,10 +523,15 @@ function formatHeadLead(tool, label) {
|
|
|
394
523
|
return `${paint.text.dim('• shell ·')} ${paintLabel(tool, label)}`;
|
|
395
524
|
}
|
|
396
525
|
|
|
397
|
-
function compactShellProfile(profile) {
|
|
398
|
-
const
|
|
526
|
+
function compactShellProfile(profile, { includePreview = true, includeDetails = true } = {}) {
|
|
527
|
+
const previewSuffix = profile.preview ? ` · preview: ${profile.preview}` : '';
|
|
528
|
+
const summary = previewSuffix && profile.summary.endsWith(previewSuffix)
|
|
529
|
+
? profile.summary.slice(0, -previewSuffix.length)
|
|
530
|
+
: profile.summary;
|
|
531
|
+
const parts = [`${paint.text.dim('$')} ${paint.text.primary(summary)}`];
|
|
399
532
|
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'));
|
|
533
|
+
if (includeDetails) parts.push(paint.text.dim(profile.detailHint || 'details: F2 or /last'));
|
|
534
|
+
if (includePreview && profile.preview) parts.push(`${paint.text.dim('preview:')} ${paint.text.primary(profile.preview)}`);
|
|
401
535
|
return parts.join(' · ');
|
|
402
536
|
}
|
|
403
537
|
|
|
@@ -478,6 +612,15 @@ function truncateMiddle(text, max) {
|
|
|
478
612
|
return paint.text.muted(`${head}…${tail}`);
|
|
479
613
|
}
|
|
480
614
|
|
|
615
|
+
function truncateEndVisible(text, max) {
|
|
616
|
+
if (!text) return '';
|
|
617
|
+
if (visibleWidth(text) <= max) return text;
|
|
618
|
+
const plain = text.replace(/\x1b\[[0-9;]*m/g, '');
|
|
619
|
+
const limit = Math.max(1, Math.floor(max));
|
|
620
|
+
if (limit <= 1) return '';
|
|
621
|
+
return paint.text.muted(`${plain.slice(0, limit - 1)}…`);
|
|
622
|
+
}
|
|
623
|
+
|
|
481
624
|
function wrapCommand(command, width) {
|
|
482
625
|
const text = String(command || '');
|
|
483
626
|
if (!text) return ['(empty command)'];
|
package/src/ui/tool-details.mjs
CHANGED
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
import { paint } from './palette.mjs';
|
|
15
15
|
import { icon, toolFamily } from './icons.mjs';
|
|
16
16
|
import { shellCommandProfile, toolDisplayLabel, toolDisplaySummary } from '../terminal/tool-display.mjs';
|
|
17
|
+
import { isSensitiveConfigPath } from '../core/safety.mjs';
|
|
17
18
|
|
|
18
19
|
const MAX_DETAIL_LINES = 60;
|
|
19
20
|
const MAX_SHELL_DETAIL_LINES = 220;
|
|
@@ -136,6 +137,12 @@ function safeDetailArgs(tool, args) {
|
|
|
136
137
|
}
|
|
137
138
|
if (tool === 'edit_file') {
|
|
138
139
|
const next = { ...args };
|
|
140
|
+
if (isSensitiveConfigPath(next.file_path || next.path)) {
|
|
141
|
+
for (const key of ['search', 'replace', 'old_string', 'new_string']) {
|
|
142
|
+
if (typeof next[key] === 'string') next[key] = '[redacted]';
|
|
143
|
+
}
|
|
144
|
+
return next;
|
|
145
|
+
}
|
|
139
146
|
for (const key of ['search', 'replace', 'old_string', 'new_string']) {
|
|
140
147
|
if (typeof next[key] === 'string' && next[key].length > 80) {
|
|
141
148
|
next[key] = `[${next[key].split('\n').length} lines omitted]`;
|
|
@@ -213,7 +220,16 @@ function detailListFiles(card) {
|
|
|
213
220
|
// ── Write / edit ────────────────────────────────────────────────────────
|
|
214
221
|
|
|
215
222
|
function detailEditFile(card) {
|
|
216
|
-
const
|
|
223
|
+
const redacted = redactedFileDiff(card.result?.file_diff)
|
|
224
|
+
|| (isSensitiveConfigPath(card.args?.file_path || card.args?.path)
|
|
225
|
+
? sensitiveFallbackDiff(card)
|
|
226
|
+
: null);
|
|
227
|
+
if (redacted) return renderRedactedDiff(redacted);
|
|
228
|
+
|
|
229
|
+
const diff = unifiedForFileDiff(card.result?.file_diff)
|
|
230
|
+
|| card.result?.diff
|
|
231
|
+
|| card.result?.patch
|
|
232
|
+
|| card.result?.output;
|
|
217
233
|
if (diff) return renderDiff(String(diff));
|
|
218
234
|
|
|
219
235
|
const before = card.args?.search;
|
|
@@ -228,7 +244,13 @@ function detailEditFile(card) {
|
|
|
228
244
|
}
|
|
229
245
|
|
|
230
246
|
function detailWriteFile(card) {
|
|
231
|
-
const
|
|
247
|
+
const redacted = redactedFileDiff(card.result?.file_diff)
|
|
248
|
+
|| (isSensitiveConfigPath(card.args?.file_path || card.args?.path)
|
|
249
|
+
? sensitiveFallbackDiff(card)
|
|
250
|
+
: null);
|
|
251
|
+
if (redacted) return renderRedactedDiff(redacted);
|
|
252
|
+
|
|
253
|
+
const diff = unifiedForFileDiff(card.result?.file_diff) || card.result?.diff;
|
|
232
254
|
if (diff) return renderDiff(String(diff));
|
|
233
255
|
const content = card.args?.content;
|
|
234
256
|
if (!content) return paint.text.dim(' (no content)');
|
|
@@ -238,7 +260,12 @@ function detailWriteFile(card) {
|
|
|
238
260
|
function detailWriteProject(card) {
|
|
239
261
|
const diffs = card.result?.file_diffs || [];
|
|
240
262
|
if (diffs.length) {
|
|
241
|
-
return
|
|
263
|
+
return diffs.map(diff => {
|
|
264
|
+
const redacted = redactedFileDiff(diff);
|
|
265
|
+
if (redacted) return renderRedactedDiff(redacted);
|
|
266
|
+
const unified = unifiedForFileDiff(diff);
|
|
267
|
+
return unified ? renderDiff(unified) : '';
|
|
268
|
+
}).filter(Boolean).join('\n');
|
|
242
269
|
}
|
|
243
270
|
const files = card.args?.files || [];
|
|
244
271
|
if (!files.length) return paint.text.dim(' (no files)');
|
|
@@ -249,6 +276,26 @@ function detailWriteProject(card) {
|
|
|
249
276
|
}).join('\n');
|
|
250
277
|
}
|
|
251
278
|
|
|
279
|
+
function redactedFileDiff(diff) {
|
|
280
|
+
return diff?.redacted ? diff : null;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function sensitiveFallbackDiff(card) {
|
|
284
|
+
return {
|
|
285
|
+
relative_path: card.args?.file_path || card.args?.path || 'sensitive config',
|
|
286
|
+
lines_added: card.result?.lines_added,
|
|
287
|
+
lines_removed: card.result?.lines_removed,
|
|
288
|
+
redacted: true,
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function renderRedactedDiff(diff = {}) {
|
|
293
|
+
const file = diff.relative_path || diff.path || 'sensitive config';
|
|
294
|
+
const add = diff.lines_added ?? 0;
|
|
295
|
+
const rem = diff.lines_removed ?? 0;
|
|
296
|
+
return ` ${paint.brand.primary(file)} ${paint.text.dim(`+${add} −${rem}`)}\n ${paint.text.dim('diff redacted for sensitive config')}`;
|
|
297
|
+
}
|
|
298
|
+
|
|
252
299
|
function detailDeleteFile(card) {
|
|
253
300
|
const p = card.args?.file_path || card.args?.path || '';
|
|
254
301
|
return ` ${paint.state.danger('✗')} ${paint.text.primary(p)}`;
|
|
@@ -405,6 +452,52 @@ function renderDiff(text) {
|
|
|
405
452
|
}).join('\n');
|
|
406
453
|
}
|
|
407
454
|
|
|
455
|
+
function unifiedForFileDiff(diff) {
|
|
456
|
+
if (!diff) return '';
|
|
457
|
+
if (diff.unified) return String(diff.unified);
|
|
458
|
+
const hunks = Array.isArray(diff.hunks) ? diff.hunks : [];
|
|
459
|
+
if (!hunks.length) return '';
|
|
460
|
+
const file = diff.relative_path || diff.path || 'file';
|
|
461
|
+
const out = [`--- a/${file}`, `+++ b/${file}`];
|
|
462
|
+
for (const hunk of hunks) {
|
|
463
|
+
const lines = hunkLines(hunk);
|
|
464
|
+
if (!lines.length) continue;
|
|
465
|
+
const oldCount = hunk.old_count ?? hunk.old_lines ?? countLinesForSide(lines, 'old');
|
|
466
|
+
const newCount = hunk.new_count ?? hunk.new_lines ?? countLinesForSide(lines, 'new');
|
|
467
|
+
out.push(`@@ -${hunk.old_start ?? 1},${oldCount} +${hunk.new_start ?? 1},${newCount} @@`);
|
|
468
|
+
for (const line of lines) out.push(toUnifiedLine(line));
|
|
469
|
+
}
|
|
470
|
+
return out.length > 2 ? out.join('\n') : '';
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
function hunkLines(hunk = {}) {
|
|
474
|
+
if (Array.isArray(hunk.lines)) return hunk.lines;
|
|
475
|
+
if (typeof hunk.body !== 'string') return [];
|
|
476
|
+
return hunk.body.replace(/\r\n?/g, '\n').split('\n').filter(line => line && !line.startsWith('@@'));
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
function toUnifiedLine(line) {
|
|
480
|
+
if (typeof line === 'string') {
|
|
481
|
+
if (line.startsWith('+') || line.startsWith('-') || line.startsWith(' ')) return line;
|
|
482
|
+
return ` ${line}`;
|
|
483
|
+
}
|
|
484
|
+
const type = String(line?.type || '').toLowerCase();
|
|
485
|
+
const text = String(line?.text ?? line?.content ?? '');
|
|
486
|
+
if (type === 'add' || type === 'added') return `+${text}`;
|
|
487
|
+
if (type === 'remove' || type === 'removed' || type === 'delete') return `-${text}`;
|
|
488
|
+
return ` ${text}`;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
function countLinesForSide(lines, side) {
|
|
492
|
+
return lines.filter(line => {
|
|
493
|
+
const type = typeof line === 'string'
|
|
494
|
+
? (line.startsWith('+') ? 'add' : line.startsWith('-') ? 'remove' : 'context')
|
|
495
|
+
: String(line?.type || 'context').toLowerCase();
|
|
496
|
+
if (side === 'old') return type !== 'add' && type !== 'added';
|
|
497
|
+
return type !== 'remove' && type !== 'removed' && type !== 'delete';
|
|
498
|
+
}).length;
|
|
499
|
+
}
|
|
500
|
+
|
|
408
501
|
function formatDuration(ms) {
|
|
409
502
|
if (ms < 1000) return `${Math.round(ms)}ms`;
|
|
410
503
|
return `${(ms / 1000).toFixed(1)}s`;
|