@bahulam/code 2.6.14 → 2.6.16

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.
@@ -231,32 +231,47 @@ function summarizeJsonOutput(value) {
231
231
 
232
232
  export function formatCompactFileDiff(result, {
233
233
  indent = ' ',
234
- maxLines = 14,
235
- maxFiles = 2,
234
+ maxLines = Infinity,
235
+ maxFiles = Infinity,
236
236
  columns = term().columns || 120,
237
+ showFileHeader = false,
237
238
  } = {}) {
238
- const diffs = fileDiffs(result).filter(diff => diff?.hunks?.length);
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, maxFiles)) {
247
- if (diffs.length > 1) {
248
- if (shown >= maxLines) { truncated = true; break; }
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 >= maxLines) { truncated = true; break; }
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 >= maxLines) { truncated = true; break; }
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 > maxFiles) truncated = true;
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 parts = [`${paint.text.dim('$')} ${paint.text.primary(profile.summary)}`];
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)'];
@@ -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 diff = card.result?.file_diff?.unified || card.result?.diff || card.result?.patch || card.result?.output;
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 diff = card.result?.file_diff?.unified || card.result?.diff;
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 renderDiff(diffs.map(diff => diff.unified).filter(Boolean).join('\n'));
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`;