@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.
@@ -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) process.stderr.write(`${diffPreview}\n`);
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) process.stderr.write(`${diffPreview}\n`);
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) process.stderr.write(`${diffPreview}\n`);
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 rendered = renderMarkdown(runtime.streamBuffer);
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 },
@@ -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
- for (const line of rendered.split('\n')) {
1109
- process.stdout.write(`${transcriptLine(line, { tone: 'assistant' })}\n`);
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
- for (const line of rendered.split('\n')) {
1484
- process.stdout.write(`${transcriptLine(line, { tone: 'assistant' })}\n`);
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
- printBanner(auth);
3107
-
3108
- // Preflight diagnostic (PRD-055 §9). Non-blocking; opt-out via
3109
- // KEPLER_NO_PREFLIGHT=1 (used by tests / scripted runs).
3110
- if (process.env.KEPLER_NO_PREFLIGHT !== '1' && !cliArgs.freeswim) {
3111
- try { await runPreflight({ auth, cwd: safeCwd(), version: VERSION }); }
3112
- catch { /* preflight is best-effort */ }
3113
- }
3114
-
3115
- // ── Initialization ──
3116
- process.stderr.write(` ${c.brand('⠋')} ${c.dim('Initializing...')}\r`);
3117
- await fetchUser(ctx);
3118
-
3119
- // Clear the spinner line
3120
- process.stderr.write(`\r${' '.repeat(60)}\r`);
3121
- process.stderr.write(` ${c.green('✓')} ${c.dim('Ready; projects will be indexed on demand')}\n`);
3122
- if (session.user) {
3123
- process.stderr.write(` ${c.green('')} ${c.dim(`Logged in as ${session.user.github_username || session.user.email || 'user'}`)}\n`);
3124
- }
3125
- // ── Resume previous session ──
3126
- if (cliArgs.resume) {
3127
- const lastSession = cliArgs.resumeSessionId
3128
- ? { sessionId: cliArgs.resumeSessionId }
3129
- : sessionMgr.getLastSession();
3130
-
3131
- if (lastSession) {
3132
- const resumed = await activateResumedSession(lastSession.sessionId, 'startup');
3133
- if (resumed.ok) {
3134
- process.stderr.write(` ${c.green('↺')} ${c.dim(`Resumed session: ${messageCountLabel(resumed.messages)}`)}`);
3135
- process.stderr.write(` ${c.dim('· project')} ${c.brand(path.basename(safeCwd()))}`);
3136
- process.stderr.write(` ${c.dim(`· agent ${resumed.historyMode}`)}`);
3137
- if (resumed.switchedProject) process.stderr.write(` ${c.dim('(cwd restored)')}`);
3138
- if (resumed.projectMissing) process.stderr.write(` ${c.yellow('(saved project path unavailable; using current cwd)')}`);
3139
- if (resumed.instruction) process.stderr.write(` ${c.dim('—')} ${c.dim(resumed.instruction.slice(0, 50))}`);
3140
- process.stderr.write('\n');
3141
- renderResumePreview(resumed, { renderEvent });
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(resumed.reason || 'No conversation found for session ' + lastSession.sessionId)}\n`);
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
- process.stderr.write(`\n ${c.dim('Press')} ${c.brand('Enter')} ${c.dim('to start, or type a prompt below.')}\n`);
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
- ? `${kind} · ${lineCount} line${lineCount === 1 ? '' : 's'} · ${formatBytes(byteCount)}`
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');
@@ -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}`];