@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
|
@@ -279,6 +279,7 @@ export function renderToolResult(data, eventType = 'tool_result') {
|
|
|
279
279
|
const tool = data.tool || data._tool || '';
|
|
280
280
|
const durationMs = data?.duration_ms ?? (data?.duration_s != null ? data.duration_s * 1000 : null);
|
|
281
281
|
recordReadActivity(tool, data.args || {});
|
|
282
|
+
recordWriteActivity(tool, data.args || {}, data);
|
|
282
283
|
|
|
283
284
|
// Update the card buffer so /last and `d` can find it.
|
|
284
285
|
if (callId) recordCard({ id: callId, tool, args: data.args, result: data, durationMs });
|
|
@@ -324,7 +325,10 @@ export function renderToolResult(data, eventType = 'tool_result') {
|
|
|
324
325
|
const combined = `${runtime.pendingHead.head} ${outcome}`;
|
|
325
326
|
if (stripAnsi(combined).length <= cols) {
|
|
326
327
|
process.stderr.write(`${combined}\n`);
|
|
327
|
-
if (diffPreview)
|
|
328
|
+
if (diffPreview) {
|
|
329
|
+
process.stderr.write(`${diffPreview}\n`);
|
|
330
|
+
rememberFileDiffPreview(data);
|
|
331
|
+
}
|
|
328
332
|
runtime.lastRenderedBlock = 'tool';
|
|
329
333
|
runtime.pendingHead = null;
|
|
330
334
|
return;
|
|
@@ -332,7 +336,10 @@ export function renderToolResult(data, eventType = 'tool_result') {
|
|
|
332
336
|
if (isInlineOutcomeTool(tool)) {
|
|
333
337
|
const compactHead = compactHeadForOutcome(runtime.pendingHead.head, outcome, cols);
|
|
334
338
|
process.stderr.write(`${compactHead} ${outcome}\n`);
|
|
335
|
-
if (diffPreview)
|
|
339
|
+
if (diffPreview) {
|
|
340
|
+
process.stderr.write(`${diffPreview}\n`);
|
|
341
|
+
rememberFileDiffPreview(data);
|
|
342
|
+
}
|
|
336
343
|
runtime.lastRenderedBlock = 'tool';
|
|
337
344
|
runtime.pendingHead = null;
|
|
338
345
|
return;
|
|
@@ -347,7 +354,10 @@ export function renderToolResult(data, eventType = 'tool_result') {
|
|
|
347
354
|
|
|
348
355
|
// Two-line shape: gutter under the (already-printed or just-flushed) head.
|
|
349
356
|
process.stderr.write(`${gutter}${outcome}\n`);
|
|
350
|
-
if (diffPreview)
|
|
357
|
+
if (diffPreview) {
|
|
358
|
+
process.stderr.write(`${diffPreview}\n`);
|
|
359
|
+
rememberFileDiffPreview(data);
|
|
360
|
+
}
|
|
351
361
|
runtime.lastRenderedBlock = 'tool';
|
|
352
362
|
|
|
353
363
|
// Lint warnings stay visible alongside writes.
|
|
@@ -356,6 +366,58 @@ export function renderToolResult(data, eventType = 'tool_result') {
|
|
|
356
366
|
}
|
|
357
367
|
}
|
|
358
368
|
|
|
369
|
+
function fileDiffKey(data = {}) {
|
|
370
|
+
return fileDiffKeys(data)[0] || '';
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
function fileDiffKeys(data = {}) {
|
|
374
|
+
const keys = [];
|
|
375
|
+
const callId = data.call_id || data._callId || data.request_id || data.id;
|
|
376
|
+
if (callId) keys.push(`call:${callId}`);
|
|
377
|
+
const diff = Array.isArray(data.file_diffs) ? data.file_diffs[0]
|
|
378
|
+
: data.file_diff ? data.file_diff
|
|
379
|
+
: data.type === 'file_diff' ? data
|
|
380
|
+
: null;
|
|
381
|
+
const file = diff?.relative_path || diff?.path || data.relative_path || data.path || '';
|
|
382
|
+
if (file) {
|
|
383
|
+
const added = diff?.lines_added ?? data.lines_added ?? '';
|
|
384
|
+
const removed = diff?.lines_removed ?? data.lines_removed ?? '';
|
|
385
|
+
keys.push(`file:${file}:${added}:${removed}`);
|
|
386
|
+
}
|
|
387
|
+
return keys;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function rememberFileDiffPreview(data = {}) {
|
|
391
|
+
for (const key of fileDiffKeys(data)) {
|
|
392
|
+
runtime.renderedFileDiffPreviews.add(key);
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
export function renderFileDiffEvent(data = {}) {
|
|
397
|
+
const keys = fileDiffKeys(data);
|
|
398
|
+
if (keys.some(key => runtime.renderedFileDiffPreviews.has(key))) return false;
|
|
399
|
+
|
|
400
|
+
const indent = subAgentIndent();
|
|
401
|
+
const gutter = `${indent}${paint.text.dim('⎿')} `;
|
|
402
|
+
const diffPreview = formatCompactFileDiff({
|
|
403
|
+
file_diff: data,
|
|
404
|
+
lines_added: data.lines_added,
|
|
405
|
+
lines_removed: data.lines_removed,
|
|
406
|
+
}, {
|
|
407
|
+
indent: gutter,
|
|
408
|
+
columns: process.stderr.columns || 120,
|
|
409
|
+
showFileHeader: true,
|
|
410
|
+
});
|
|
411
|
+
if (!diffPreview) return false;
|
|
412
|
+
|
|
413
|
+
renderBlockBoundary('tool', { compactSame: true });
|
|
414
|
+
process.stderr.write(`${diffPreview}\n`);
|
|
415
|
+
for (const key of keys) runtime.renderedFileDiffPreviews.add(key);
|
|
416
|
+
rememberChangedFile(data.relative_path || data.path);
|
|
417
|
+
runtime.lastRenderedBlock = 'tool';
|
|
418
|
+
return true;
|
|
419
|
+
}
|
|
420
|
+
|
|
359
421
|
function shellResultTool(tool) {
|
|
360
422
|
return [
|
|
361
423
|
'shell', 'run_tests', 'validate_build', 'lint_check',
|
|
@@ -415,6 +477,11 @@ export function rememberReadFile(filePath) {
|
|
|
415
477
|
if (file && !session.filesRead.includes(file)) session.filesRead.push(file);
|
|
416
478
|
}
|
|
417
479
|
|
|
480
|
+
export function rememberChangedFile(filePath) {
|
|
481
|
+
const file = shortPath(String(filePath || '').trim());
|
|
482
|
+
if (file && !session.filesChanged.includes(file)) session.filesChanged.push(file);
|
|
483
|
+
}
|
|
484
|
+
|
|
418
485
|
export function recordReadActivity(tool, args = {}) {
|
|
419
486
|
const normalized = String(tool || '').toLowerCase();
|
|
420
487
|
if (normalized === 'read_file' || normalized === 'read') {
|
|
@@ -429,6 +496,26 @@ export function recordReadActivity(tool, args = {}) {
|
|
|
429
496
|
}
|
|
430
497
|
}
|
|
431
498
|
|
|
499
|
+
export function recordWriteActivity(tool, args = {}, result = {}) {
|
|
500
|
+
const normalized = String(tool || '').toLowerCase();
|
|
501
|
+
if (!['write_file', 'edit_file', 'delete_file', 'write_project'].includes(normalized)) return;
|
|
502
|
+
|
|
503
|
+
if (normalized === 'write_project') {
|
|
504
|
+
const files = Array.isArray(args.files) ? args.files : [];
|
|
505
|
+
for (const file of files) {
|
|
506
|
+
rememberChangedFile(file?.file_path || file?.path);
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
rememberChangedFile(args.file_path || args.path || result.file_path || result.path);
|
|
511
|
+
const diffs = Array.isArray(result.file_diffs)
|
|
512
|
+
? result.file_diffs
|
|
513
|
+
: result.file_diff ? [result.file_diff] : [];
|
|
514
|
+
for (const diff of diffs) {
|
|
515
|
+
rememberChangedFile(diff.relative_path || diff.path);
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
|
|
432
519
|
export function thinkingKind(text) {
|
|
433
520
|
return /\b(read|reading|inspect|scan|search|open|trace|look(?:ing)?\s+at)\b/i.test(text)
|
|
434
521
|
? 'Reading'
|
|
@@ -511,6 +598,7 @@ export function startContentStream() {
|
|
|
511
598
|
runtime.streamBuffer = '';
|
|
512
599
|
runtime.streamedPartialText = '';
|
|
513
600
|
runtime.renderedToolResults.clear();
|
|
601
|
+
runtime.renderedFileDiffPreviews.clear();
|
|
514
602
|
runtime.exploreRun = { counts: {}, recent: [], lineActive: false, lastPrintedSummary: '', lastPrintedTotal: 0, lastPrintedAt: 0 };
|
|
515
603
|
runtime.renderedContentThisTurn = false;
|
|
516
604
|
runtime.contentHeaderPrinted = false;
|
|
@@ -535,27 +623,36 @@ export function flushContent() {
|
|
|
535
623
|
if (runtime.streamTimer) { clearTimeout(runtime.streamTimer); runtime.streamTimer = null; }
|
|
536
624
|
if (!runtime.streamBuffer) return;
|
|
537
625
|
|
|
626
|
+
const rendered = renderMarkdown(runtime.streamBuffer);
|
|
627
|
+
const lines = transcriptRenderableLines(rendered);
|
|
628
|
+
runtime.streamBuffer = '';
|
|
629
|
+
if (!lines.length) return;
|
|
630
|
+
|
|
538
631
|
if (isInputDockMounted()) moveToContent();
|
|
539
632
|
stopSpinner();
|
|
540
633
|
// Any buffered tool head needs to land BEFORE this content so the order
|
|
541
634
|
// is preserved on screen.
|
|
542
635
|
flushPendingHead();
|
|
543
636
|
flushCompactReadRun();
|
|
544
|
-
renderBlockBoundary('content');
|
|
637
|
+
renderBlockBoundary('content', { compactSame: true });
|
|
545
638
|
if (!runtime.contentHeaderPrinted) {
|
|
546
639
|
process.stdout.write(`${transcriptHeader('bahulam', { tone: 'assistant' })}\n`);
|
|
547
640
|
runtime.contentHeaderPrinted = true;
|
|
548
641
|
}
|
|
549
|
-
const
|
|
550
|
-
for (const line of rendered.split('\n')) {
|
|
642
|
+
for (const line of lines) {
|
|
551
643
|
process.stdout.write(`${transcriptLine(line, { tone: 'assistant' })}\n`);
|
|
552
644
|
}
|
|
553
|
-
runtime.streamBuffer = '';
|
|
554
645
|
runtime.renderedContentThisTurn = true;
|
|
555
646
|
runtime.lastRenderedBlock = 'content';
|
|
556
647
|
if (typeof runtime.afterContentFlush === 'function') runtime.afterContentFlush();
|
|
557
648
|
}
|
|
558
649
|
|
|
650
|
+
export function transcriptRenderableLines(rendered) {
|
|
651
|
+
const lines = String(rendered ?? '').replace(/\r\n?/g, '\n').split('\n');
|
|
652
|
+
while (lines.length && lines[lines.length - 1] === '') lines.pop();
|
|
653
|
+
return lines;
|
|
654
|
+
}
|
|
655
|
+
|
|
559
656
|
export function renderStagnation(data = {}) {
|
|
560
657
|
const rawMessage = data?.message || '';
|
|
561
658
|
const reason = data?.reason || rawMessage.replace(/^Stagnation:\s*/i, '').trim();
|
|
@@ -39,6 +39,7 @@ export const runtime = {
|
|
|
39
39
|
pendingHead: null, // { callId, head, indent } buffered until result arrives
|
|
40
40
|
lastRenderedBlock: null, // 'tool' | 'content' | 'thinking' | 'status' | 'plan' | null
|
|
41
41
|
renderedToolResults: new Set(),
|
|
42
|
+
renderedFileDiffPreviews: new Set(),
|
|
42
43
|
|
|
43
44
|
// Explore-run collapse (read/list/search/index bursts as concise progress).
|
|
44
45
|
exploreRun: { counts: {}, recent: [], lineActive: false, lastPrintedSummary: '', lastPrintedTotal: 0, lastPrintedAt: 0 },
|
package/src/terminal/repl.mjs
CHANGED
|
@@ -84,12 +84,14 @@ import {
|
|
|
84
84
|
isInlineOutcomeTool,
|
|
85
85
|
renderBlockBoundary,
|
|
86
86
|
renderExploreRun,
|
|
87
|
+
renderFileDiffEvent,
|
|
87
88
|
renderStagnation,
|
|
88
89
|
renderToolCall,
|
|
89
90
|
renderToolResult,
|
|
90
91
|
startContentStream,
|
|
91
92
|
startSpinner,
|
|
92
93
|
stopSpinner,
|
|
94
|
+
transcriptRenderableLines,
|
|
93
95
|
thinkingPrefix,
|
|
94
96
|
updateSpinner,
|
|
95
97
|
} from './repl-render.mjs';
|
|
@@ -1099,17 +1101,20 @@ function renderEvent(event) {
|
|
|
1099
1101
|
}
|
|
1100
1102
|
}
|
|
1101
1103
|
if (text) {
|
|
1102
|
-
renderBlockBoundary('content');
|
|
1103
|
-
if (!runtime.contentHeaderPrinted) {
|
|
1104
|
-
process.stdout.write(`${transcriptHeader('bahulam', { tone: 'assistant' })}\n`);
|
|
1105
|
-
runtime.contentHeaderPrinted = true;
|
|
1106
|
-
}
|
|
1107
1104
|
const rendered = renderMarkdown(text);
|
|
1108
|
-
|
|
1109
|
-
|
|
1105
|
+
const lines = transcriptRenderableLines(rendered);
|
|
1106
|
+
if (lines.length) {
|
|
1107
|
+
renderBlockBoundary('content', { compactSame: true });
|
|
1108
|
+
if (!runtime.contentHeaderPrinted) {
|
|
1109
|
+
process.stdout.write(`${transcriptHeader('bahulam', { tone: 'assistant' })}\n`);
|
|
1110
|
+
runtime.contentHeaderPrinted = true;
|
|
1111
|
+
}
|
|
1112
|
+
for (const line of lines) {
|
|
1113
|
+
process.stdout.write(`${transcriptLine(line, { tone: 'assistant' })}\n`);
|
|
1114
|
+
}
|
|
1115
|
+
runtime.renderedContentThisTurn = true;
|
|
1116
|
+
runtime.lastRenderedBlock = 'content';
|
|
1110
1117
|
}
|
|
1111
|
-
runtime.renderedContentThisTurn = true;
|
|
1112
|
-
runtime.lastRenderedBlock = 'content';
|
|
1113
1118
|
}
|
|
1114
1119
|
break;
|
|
1115
1120
|
}
|
|
@@ -1229,6 +1234,14 @@ function renderEvent(event) {
|
|
|
1229
1234
|
break;
|
|
1230
1235
|
}
|
|
1231
1236
|
|
|
1237
|
+
case 'file_diff': {
|
|
1238
|
+
stopSpinner();
|
|
1239
|
+
flushContent();
|
|
1240
|
+
flushPendingHead();
|
|
1241
|
+
renderFileDiffEvent(data);
|
|
1242
|
+
break;
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1232
1245
|
case 'plan': {
|
|
1233
1246
|
stopSpinner();
|
|
1234
1247
|
flushContent();
|
|
@@ -1474,17 +1487,20 @@ function renderEvent(event) {
|
|
|
1474
1487
|
|
|
1475
1488
|
const summary = data?.summary || '';
|
|
1476
1489
|
if (summary && !runtime.renderedContentThisTurn) {
|
|
1477
|
-
renderBlockBoundary('content');
|
|
1478
|
-
if (!runtime.contentHeaderPrinted) {
|
|
1479
|
-
process.stdout.write(`${transcriptHeader('bahulam', { tone: 'assistant' })}\n`);
|
|
1480
|
-
runtime.contentHeaderPrinted = true;
|
|
1481
|
-
}
|
|
1482
1490
|
const rendered = renderMarkdown(summary);
|
|
1483
|
-
|
|
1484
|
-
|
|
1491
|
+
const lines = transcriptRenderableLines(rendered);
|
|
1492
|
+
if (lines.length) {
|
|
1493
|
+
renderBlockBoundary('content', { compactSame: true });
|
|
1494
|
+
if (!runtime.contentHeaderPrinted) {
|
|
1495
|
+
process.stdout.write(`${transcriptHeader('bahulam', { tone: 'assistant' })}\n`);
|
|
1496
|
+
runtime.contentHeaderPrinted = true;
|
|
1497
|
+
}
|
|
1498
|
+
for (const line of lines) {
|
|
1499
|
+
process.stdout.write(`${transcriptLine(line, { tone: 'assistant' })}\n`);
|
|
1500
|
+
}
|
|
1501
|
+
runtime.renderedContentThisTurn = true;
|
|
1502
|
+
runtime.lastRenderedBlock = 'content';
|
|
1485
1503
|
}
|
|
1486
|
-
runtime.renderedContentThisTurn = true;
|
|
1487
|
-
runtime.lastRenderedBlock = 'content';
|
|
1488
1504
|
}
|
|
1489
1505
|
|
|
1490
1506
|
// Update session token counts
|
|
@@ -2784,6 +2800,54 @@ export async function startTerminalRepl() {
|
|
|
2784
2800
|
|
|
2785
2801
|
const ctx = { auth, toolExecutor, approval, jsonlWriter, sessionMgr, checkpoints, effectivePolicy, latestProjectContext, latestEnvelope, pendingVisionPaths: [] };
|
|
2786
2802
|
|
|
2803
|
+
let startupOutputRow = 1;
|
|
2804
|
+
let startupOutputCol = 1;
|
|
2805
|
+
|
|
2806
|
+
function trackStartupOutput(chunk) {
|
|
2807
|
+
if (!process.stderr.isTTY || term().plain) return;
|
|
2808
|
+
const text = Buffer.isBuffer(chunk) ? chunk.toString('utf8') : String(chunk ?? '');
|
|
2809
|
+
if (!text) return;
|
|
2810
|
+
const clean = text
|
|
2811
|
+
.replace(/\x1b\[[0-9;?]*[ -/]*[@-~]/g, '')
|
|
2812
|
+
.replace(/\x1b[()][A-Za-z0-9]/g, '');
|
|
2813
|
+
const width = Math.max(1, process.stderr.columns || process.stdout.columns || 80);
|
|
2814
|
+
for (const ch of clean) {
|
|
2815
|
+
if (ch === '\r') {
|
|
2816
|
+
startupOutputCol = 1;
|
|
2817
|
+
continue;
|
|
2818
|
+
}
|
|
2819
|
+
if (ch === '\n') {
|
|
2820
|
+
startupOutputRow++;
|
|
2821
|
+
startupOutputCol = 1;
|
|
2822
|
+
continue;
|
|
2823
|
+
}
|
|
2824
|
+
startupOutputCol++;
|
|
2825
|
+
if (startupOutputCol > width) {
|
|
2826
|
+
startupOutputRow++;
|
|
2827
|
+
startupOutputCol = 1;
|
|
2828
|
+
}
|
|
2829
|
+
}
|
|
2830
|
+
}
|
|
2831
|
+
|
|
2832
|
+
function startStartupOutputTracking() {
|
|
2833
|
+
if (!process.stderr.isTTY || term().plain) return () => {};
|
|
2834
|
+
const originalWrite = process.stderr.write;
|
|
2835
|
+
function trackedStartupWrite(chunk, ...args) {
|
|
2836
|
+
trackStartupOutput(chunk);
|
|
2837
|
+
return originalWrite.call(this, chunk, ...args);
|
|
2838
|
+
}
|
|
2839
|
+
process.stderr.write = trackedStartupWrite;
|
|
2840
|
+
return () => {
|
|
2841
|
+
if (process.stderr.write === trackedStartupWrite) {
|
|
2842
|
+
process.stderr.write = originalWrite;
|
|
2843
|
+
}
|
|
2844
|
+
};
|
|
2845
|
+
}
|
|
2846
|
+
|
|
2847
|
+
function startupCursorSeed() {
|
|
2848
|
+
return { row: startupOutputRow, col: startupOutputCol };
|
|
2849
|
+
}
|
|
2850
|
+
|
|
2787
2851
|
async function startNewSession({ announce = true } = {}) {
|
|
2788
2852
|
stopSpinner();
|
|
2789
2853
|
flushContent();
|
|
@@ -3103,57 +3167,67 @@ export async function startTerminalRepl() {
|
|
|
3103
3167
|
// ── Print banner + preflight + init BEFORE mounting the status bar ──
|
|
3104
3168
|
// The status bar shrinks the scroll region; if it mounts first, the
|
|
3105
3169
|
// banner scrolls off-screen before the user ever sees it.
|
|
3106
|
-
|
|
3107
|
-
|
|
3108
|
-
|
|
3109
|
-
|
|
3110
|
-
|
|
3111
|
-
|
|
3112
|
-
|
|
3113
|
-
|
|
3114
|
-
|
|
3115
|
-
|
|
3116
|
-
|
|
3117
|
-
|
|
3118
|
-
|
|
3119
|
-
|
|
3120
|
-
|
|
3121
|
-
|
|
3122
|
-
|
|
3123
|
-
process.stderr.write(
|
|
3124
|
-
}
|
|
3125
|
-
|
|
3126
|
-
|
|
3127
|
-
|
|
3128
|
-
|
|
3129
|
-
|
|
3130
|
-
|
|
3131
|
-
|
|
3132
|
-
|
|
3133
|
-
|
|
3134
|
-
|
|
3135
|
-
|
|
3136
|
-
|
|
3137
|
-
|
|
3138
|
-
|
|
3139
|
-
|
|
3140
|
-
|
|
3141
|
-
|
|
3170
|
+
const stopStartupOutputTracking = startStartupOutputTracking();
|
|
3171
|
+
let dockCursor = startupCursorSeed();
|
|
3172
|
+
try {
|
|
3173
|
+
printBanner(auth);
|
|
3174
|
+
|
|
3175
|
+
// Preflight diagnostic (PRD-055 §9). Non-blocking; opt-out via
|
|
3176
|
+
// KEPLER_NO_PREFLIGHT=1 (used by tests / scripted runs).
|
|
3177
|
+
if (process.env.KEPLER_NO_PREFLIGHT !== '1' && !cliArgs.freeswim) {
|
|
3178
|
+
try { await runPreflight({ auth, cwd: safeCwd(), version: VERSION }); }
|
|
3179
|
+
catch { /* preflight is best-effort */ }
|
|
3180
|
+
}
|
|
3181
|
+
|
|
3182
|
+
// ── Initialization ──
|
|
3183
|
+
process.stderr.write(` ${c.brand('⠋')} ${c.dim('Initializing...')}\r`);
|
|
3184
|
+
await fetchUser(ctx);
|
|
3185
|
+
|
|
3186
|
+
// Clear the spinner line
|
|
3187
|
+
process.stderr.write(`\r${' '.repeat(60)}\r`);
|
|
3188
|
+
process.stderr.write(` ${c.green('✓')} ${c.dim('Ready; projects will be indexed on demand')}\n`);
|
|
3189
|
+
if (session.user) {
|
|
3190
|
+
process.stderr.write(` ${c.green('✓')} ${c.dim(`Logged in as ${session.user.github_username || session.user.email || 'user'}`)}\n`);
|
|
3191
|
+
}
|
|
3192
|
+
// ── Resume previous session ──
|
|
3193
|
+
if (cliArgs.resume) {
|
|
3194
|
+
const lastSession = cliArgs.resumeSessionId
|
|
3195
|
+
? { sessionId: cliArgs.resumeSessionId }
|
|
3196
|
+
: sessionMgr.getLastSession();
|
|
3197
|
+
|
|
3198
|
+
if (lastSession) {
|
|
3199
|
+
const resumed = await activateResumedSession(lastSession.sessionId, 'startup');
|
|
3200
|
+
if (resumed.ok) {
|
|
3201
|
+
process.stderr.write(` ${c.green('↺')} ${c.dim(`Resumed session: ${messageCountLabel(resumed.messages)}`)}`);
|
|
3202
|
+
process.stderr.write(` ${c.dim('· project')} ${c.brand(path.basename(safeCwd()))}`);
|
|
3203
|
+
process.stderr.write(` ${c.dim(`· agent ${resumed.historyMode}`)}`);
|
|
3204
|
+
if (resumed.switchedProject) process.stderr.write(` ${c.dim('(cwd restored)')}`);
|
|
3205
|
+
if (resumed.projectMissing) process.stderr.write(` ${c.yellow('(saved project path unavailable; using current cwd)')}`);
|
|
3206
|
+
if (resumed.instruction) process.stderr.write(` ${c.dim('—')} ${c.dim(resumed.instruction.slice(0, 50))}`);
|
|
3207
|
+
process.stderr.write('\n');
|
|
3208
|
+
renderResumePreview(resumed, { renderEvent });
|
|
3209
|
+
} else {
|
|
3210
|
+
process.stderr.write(` ${c.yellow('!')} ${c.dim(resumed.reason || 'No conversation found for session ' + lastSession.sessionId)}\n`);
|
|
3211
|
+
}
|
|
3142
3212
|
} else {
|
|
3143
|
-
process.stderr.write(` ${c.yellow('!')} ${c.dim(
|
|
3213
|
+
process.stderr.write(` ${c.yellow('!')} ${c.dim('No previous session to resume')}\n`);
|
|
3144
3214
|
}
|
|
3145
|
-
} else {
|
|
3146
|
-
process.stderr.write(` ${c.yellow('!')} ${c.dim('No previous session to resume')}\n`);
|
|
3147
3215
|
}
|
|
3148
|
-
}
|
|
3149
3216
|
|
|
3150
|
-
|
|
3217
|
+
process.stderr.write(`\n ${c.dim('Press')} ${c.brand('Enter')} ${c.dim('to start, or type a prompt below.')}\n`);
|
|
3218
|
+
} finally {
|
|
3219
|
+
dockCursor = startupCursorSeed();
|
|
3220
|
+
stopStartupOutputTracking();
|
|
3221
|
+
}
|
|
3151
3222
|
|
|
3152
3223
|
// Keep one bottom-reserved UI surface: the fixed input dock. The older
|
|
3153
3224
|
// status bar used the same terminal scroll-region primitive, so mounting
|
|
3154
3225
|
// both would make prompt placement unpredictable.
|
|
3155
3226
|
orbitRef.current = createOrbit();
|
|
3156
|
-
const inputDockActive = mountInputDock(
|
|
3227
|
+
const inputDockActive = mountInputDock({
|
|
3228
|
+
initialContentRow: dockCursor.row,
|
|
3229
|
+
initialContentCol: dockCursor.col,
|
|
3230
|
+
});
|
|
3157
3231
|
if (inputDockActive) {
|
|
3158
3232
|
process.on('beforeExit', unmountInputDock);
|
|
3159
3233
|
process.on('exit', unmountInputDock);
|
|
@@ -3851,6 +3925,21 @@ export async function startTerminalRepl() {
|
|
|
3851
3925
|
}
|
|
3852
3926
|
runtime.afterContentFlush = focusExecutionInput;
|
|
3853
3927
|
|
|
3928
|
+
function printExecutionInstruction(instruction) {
|
|
3929
|
+
if (isInputDockMounted()) {
|
|
3930
|
+
clearInputPrompt();
|
|
3931
|
+
moveToContent();
|
|
3932
|
+
} else if (executionInputVisible) {
|
|
3933
|
+
process.stderr.write('\n');
|
|
3934
|
+
}
|
|
3935
|
+
renderBlockBoundary('user', { compactSame: true });
|
|
3936
|
+
process.stderr.write(`${transcriptHeader('you', { tone: 'user' })} ${paint.text.dim('follow-up')}\n`);
|
|
3937
|
+
for (const line of String(instruction || '').split('\n')) {
|
|
3938
|
+
process.stderr.write(`${transcriptLine(line, { tone: 'user' })}\n`);
|
|
3939
|
+
}
|
|
3940
|
+
runtime.lastRenderedBlock = 'user';
|
|
3941
|
+
}
|
|
3942
|
+
|
|
3854
3943
|
async function submitExecutionInstruction() {
|
|
3855
3944
|
const instruction = executionInputBuffer.trim();
|
|
3856
3945
|
executionInputBuffer = '';
|
|
@@ -3869,18 +3958,14 @@ export async function startTerminalRepl() {
|
|
|
3869
3958
|
executionInputVisible = false;
|
|
3870
3959
|
return;
|
|
3871
3960
|
}
|
|
3961
|
+
printExecutionInstruction(instruction);
|
|
3872
3962
|
if (isInputDockMounted()) {
|
|
3873
|
-
clearInputPrompt();
|
|
3874
|
-
moveToContent();
|
|
3875
|
-
process.stderr.write(`${executionInputPrefix()}${instruction}\n`);
|
|
3876
3963
|
renderDockInput(executionInputPrefix(), '', {
|
|
3877
3964
|
context: buildContextStrip(),
|
|
3878
3965
|
meta: buildDockMeta(),
|
|
3879
3966
|
tips: executionInputTips(),
|
|
3880
3967
|
});
|
|
3881
3968
|
moveToContent();
|
|
3882
|
-
} else if (executionInputVisible) {
|
|
3883
|
-
process.stderr.write('\n');
|
|
3884
3969
|
}
|
|
3885
3970
|
executionInputVisible = false;
|
|
3886
3971
|
// Live steering (PRD-081 §5.2): submit through the dedicated
|
|
@@ -3906,18 +3991,26 @@ export async function startTerminalRepl() {
|
|
|
3906
3991
|
});
|
|
3907
3992
|
|
|
3908
3993
|
if (status === 'accepted') {
|
|
3994
|
+
renderBlockBoundary('status', { compactSame: true });
|
|
3909
3995
|
process.stderr.write(` ${c.green('↳')} ${c.dim('sent to running agent')}\n`);
|
|
3996
|
+
runtime.lastRenderedBlock = 'status';
|
|
3910
3997
|
} else if (status === 'duplicate') {
|
|
3998
|
+
renderBlockBoundary('status', { compactSame: true });
|
|
3911
3999
|
process.stderr.write(` ${c.dim('↳ already sent (idempotent)')}\n`);
|
|
4000
|
+
runtime.lastRenderedBlock = 'status';
|
|
3912
4001
|
} else if (status === 'queued_next_turn') {
|
|
3913
4002
|
_queuedLines.push(instruction);
|
|
4003
|
+
renderBlockBoundary('status', { compactSame: true });
|
|
3914
4004
|
process.stderr.write(` ${c.yellow('↳')} ${c.dim('task ended — queued for next turn')}\n`);
|
|
4005
|
+
runtime.lastRenderedBlock = 'status';
|
|
3915
4006
|
} else {
|
|
3916
4007
|
// no_task, error, or unknown — fall back to next-turn queue so the
|
|
3917
4008
|
// user's text is never silently lost.
|
|
3918
4009
|
_queuedLines.push(instruction);
|
|
3919
4010
|
const errBits = result && result.error ? ` ${c.dim(`(${String(result.error).slice(0, 80)})`)}` : '';
|
|
4011
|
+
renderBlockBoundary('status', { compactSame: true });
|
|
3920
4012
|
process.stderr.write(` ${c.yellow('↳')} ${c.dim('queued for next turn')}${errBits}\n`);
|
|
4013
|
+
runtime.lastRenderedBlock = 'status';
|
|
3921
4014
|
}
|
|
3922
4015
|
}
|
|
3923
4016
|
|
|
@@ -4147,8 +4240,6 @@ export async function startTerminalRepl() {
|
|
|
4147
4240
|
moveToContent();
|
|
4148
4241
|
}
|
|
4149
4242
|
startContentStream();
|
|
4150
|
-
process.stderr.write(`\n${transcriptHeader('bahulam', { tone: 'assistant' })}\n`);
|
|
4151
|
-
runtime.contentHeaderPrinted = true;
|
|
4152
4243
|
|
|
4153
4244
|
// Immediate feedback so the screen isn't blank between submit and the
|
|
4154
4245
|
// first backend event. The first `status`, `thinking`, or `content_*`
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
// Present-progressive verbs — read more conversationally than "Read file":
|
|
2
2
|
// "Reading auth.py — 47 lines" reads like the agent narrating, not a log.
|
|
3
|
+
import { isSensitiveConfigPath } from '../core/safety.mjs';
|
|
4
|
+
|
|
3
5
|
const TOOL_LABELS = Object.freeze({
|
|
4
6
|
shell: 'Running',
|
|
5
7
|
read_file: 'Reading',
|
|
@@ -129,6 +131,7 @@ export function toolDisplaySummary(tool, args = {}, { cwd } = {}) {
|
|
|
129
131
|
.join(', ') || 'Project files';
|
|
130
132
|
case 'edit_file': {
|
|
131
133
|
const filePath = shortPath(args.file_path || args.path, cwd);
|
|
134
|
+
if (isSensitiveConfigPath(filePath)) return `${filePath} · match [redacted]`;
|
|
132
135
|
const search = String(args.search || '').trim();
|
|
133
136
|
return search ? `${filePath} · match "${search.slice(0, 40)}${search.length > 40 ? '...' : ''}"` : filePath;
|
|
134
137
|
}
|
|
@@ -229,8 +232,12 @@ export function shellCommandProfile(command, {
|
|
|
229
232
|
|| commandLineCount >= compactLines
|
|
230
233
|
|| commandByteCount > compactChars;
|
|
231
234
|
const kind = script?.kind || (lineCount > 1 ? 'shell script' : 'shell command');
|
|
235
|
+
const preview = script?.body ? scriptBodyPreview(script.body) : '';
|
|
232
236
|
const summary = compact
|
|
233
|
-
?
|
|
237
|
+
? [
|
|
238
|
+
`${kind} · ${lineCount} line${lineCount === 1 ? '' : 's'} · ${formatBytes(byteCount)}`,
|
|
239
|
+
preview ? `preview: ${preview}` : '',
|
|
240
|
+
].filter(Boolean).join(' · ')
|
|
234
241
|
: body;
|
|
235
242
|
|
|
236
243
|
return {
|
|
@@ -244,6 +251,7 @@ export function shellCommandProfile(command, {
|
|
|
244
251
|
compact,
|
|
245
252
|
kind,
|
|
246
253
|
summary,
|
|
254
|
+
preview,
|
|
247
255
|
script,
|
|
248
256
|
detailHint: compact ? 'details: F2 or /last' : '',
|
|
249
257
|
};
|
|
@@ -297,6 +305,17 @@ function interpreterKind(value) {
|
|
|
297
305
|
return 'shell script';
|
|
298
306
|
}
|
|
299
307
|
|
|
308
|
+
function scriptBodyPreview(body, maxChars = 20) {
|
|
309
|
+
const line = String(body || '')
|
|
310
|
+
.replace(/\r\n?/g, '\n')
|
|
311
|
+
.split('\n')
|
|
312
|
+
.map(value => value.trim())
|
|
313
|
+
.find(Boolean) || '';
|
|
314
|
+
const compact = line.replace(/\s+/g, ' ');
|
|
315
|
+
if (compact.length <= maxChars) return compact;
|
|
316
|
+
return `${compact.slice(0, Math.max(0, maxChars - 1))}…`;
|
|
317
|
+
}
|
|
318
|
+
|
|
300
319
|
function byteLength(value) {
|
|
301
320
|
try {
|
|
302
321
|
return Buffer.byteLength(String(value || ''), 'utf8');
|
package/src/ui/approval.mjs
CHANGED
|
@@ -19,6 +19,7 @@ import { paint, width as visibleWidth } from './palette.mjs';
|
|
|
19
19
|
import { icon } from './icons.mjs';
|
|
20
20
|
import { shellCommandDisplay, shellCommandProfile, toolDisplayLabel, toolDisplaySummary } from '../terminal/tool-display.mjs';
|
|
21
21
|
import { label as tierLabel, requiresExplicitApproval, TIERS } from '../core/risk-tier.mjs';
|
|
22
|
+
import { isSensitiveConfigPath } from '../core/safety.mjs';
|
|
22
23
|
|
|
23
24
|
/**
|
|
24
25
|
* Default option set per tier. Caller can override via `opts.options`.
|
|
@@ -158,6 +159,7 @@ export { TIERS };
|
|
|
158
159
|
function tierTitle(tier) {
|
|
159
160
|
switch (tier) {
|
|
160
161
|
case TIERS.SENSITIVE_READ: return 'SENSITIVE';
|
|
162
|
+
case TIERS.PROTECTED_EDIT: return 'PROTECTED';
|
|
161
163
|
case TIERS.SHELL_DANGEROUS: return 'DANGEROUS';
|
|
162
164
|
case TIERS.DESTRUCTIVE: return 'DESTRUCTIVE';
|
|
163
165
|
case TIERS.SHELL_MEDIUM: return 'MEDIUM';
|
|
@@ -172,6 +174,7 @@ function tierTitle(tier) {
|
|
|
172
174
|
function approvalTitle(tier) {
|
|
173
175
|
switch (tier) {
|
|
174
176
|
case TIERS.SENSITIVE_READ:
|
|
177
|
+
case TIERS.PROTECTED_EDIT:
|
|
175
178
|
case TIERS.SHELL_DANGEROUS:
|
|
176
179
|
case TIERS.DESTRUCTIVE:
|
|
177
180
|
return tierTitle(tier);
|
|
@@ -286,6 +289,7 @@ function riskTerms(tool, args = {}, tier) {
|
|
|
286
289
|
}
|
|
287
290
|
}
|
|
288
291
|
if (tier === TIERS.SENSITIVE_READ) terms.push('sensitive read');
|
|
292
|
+
if (tier === TIERS.PROTECTED_EDIT) terms.push('protected edit');
|
|
289
293
|
if (tier === TIERS.DESTRUCTIVE) terms.push('destructive');
|
|
290
294
|
return [...new Set(terms)].slice(0, 3);
|
|
291
295
|
}
|
|
@@ -315,11 +319,18 @@ function subjectDetails(tool, args = {}, summary = '', available = 72) {
|
|
|
315
319
|
}
|
|
316
320
|
if (tool === 'write_file') {
|
|
317
321
|
const file = args.file_path || args.path || summary || '';
|
|
322
|
+
if (isSensitiveConfigPath(file)) return [`${file} · content redacted`];
|
|
318
323
|
const lineCount = typeof args.content === 'string' ? args.content.split('\n').length : null;
|
|
319
324
|
return [`${file}${lineCount ? ` · ${lineCount} lines` : ''}`];
|
|
320
325
|
}
|
|
321
326
|
if (tool === 'edit_file') {
|
|
322
327
|
const file = args.file_path || args.path || '';
|
|
328
|
+
if (isSensitiveConfigPath(file)) {
|
|
329
|
+
const details = [`${file || summary}`];
|
|
330
|
+
if (args.search || args.old_string) details.push('match: [redacted]');
|
|
331
|
+
if (args.replace || args.new_string) details.push('replace: [redacted]');
|
|
332
|
+
return details;
|
|
333
|
+
}
|
|
323
334
|
const search = String(args.search || args.old_string || '').trim();
|
|
324
335
|
const replacement = String(args.replace || args.new_string || '').trim();
|
|
325
336
|
const details = [`${file || summary}`];
|