@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.
- package/package.json +5 -5
- package/src/auth/tarang-auth.mjs +6 -0
- package/src/config/cli-args.mjs +7 -1
- package/src/config/model-catalog.mjs +57 -0
- package/src/core/approval-log.mjs +23 -0
- package/src/core/approval.mjs +93 -17
- package/src/core/bundled-runtime.mjs +12 -0
- package/src/core/error-guidance.mjs +8 -4
- package/src/core/file-diff.mjs +1 -1
- package/src/core/local-agent.mjs +3 -3
- 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 +7 -3
- package/src/permissions/command-classifier.mjs +50 -2
- package/src/telemetry/index.mjs +97 -71
- package/src/terminal/main.mjs +40 -0
- package/src/terminal/repl-format.mjs +64 -4
- package/src/terminal/repl-model-form.mjs +132 -0
- package/src/terminal/repl-render.mjs +256 -33
- package/src/terminal/repl-resume.mjs +27 -12
- package/src/terminal/repl-state.mjs +16 -0
- package/src/terminal/repl.mjs +644 -85
- package/src/terminal/tool-display.mjs +20 -1
- package/src/ui/approval.mjs +33 -5
- package/src/ui/input-dock.mjs +192 -16
- package/src/ui/render-queue.mjs +500 -0
- package/src/ui/slash-commands.mjs +2 -0
- package/src/ui/sub-agent.mjs +17 -2
- package/src/ui/tool-card.mjs +156 -13
- package/src/ui/tool-details.mjs +96 -3
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`;
|