Verified all implementation gaps from the genesis bead checklist are complete: - memoryProfiler.ts: Fully implemented with snapshot tracking and V8 heap dumps - FileHeatmap treemap + timelapse: Both views fully implemented with playback controls - SpanDag zoom/pan: Fully implemented with wheel zoom and drag-to-pan All 2399 tests pass (4 skipped). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
334 lines
7.4 KiB
TypeScript
334 lines
7.4 KiB
TypeScript
/**
|
|
* DiffView Component
|
|
*
|
|
* Renders unified diffs from Edit tool calls.
|
|
* Shows additions in green, deletions in red, with line numbers.
|
|
*/
|
|
|
|
import blessed from 'blessed';
|
|
import { colors } from '../utils/colors.js';
|
|
|
|
export interface DiffViewOptions {
|
|
/** Parent screen */
|
|
parent: blessed.Widgets.Screen;
|
|
|
|
/** Position from top */
|
|
top: number | string;
|
|
|
|
/** Position from left */
|
|
left: number | string;
|
|
|
|
/** Width of the panel */
|
|
width: number | string;
|
|
|
|
/** Height of the panel */
|
|
height: number | string;
|
|
|
|
/** Maximum lines to show before truncation */
|
|
maxLines?: number;
|
|
}
|
|
|
|
export interface DiffLine {
|
|
/** Line type: added, removed, context, header */
|
|
type: 'added' | 'removed' | 'context' | 'header';
|
|
|
|
/** Original line number (for removed/context) */
|
|
oldLine?: number;
|
|
|
|
/** New line number (for added/context) */
|
|
newLine?: number;
|
|
|
|
/** Line content */
|
|
content: string;
|
|
}
|
|
|
|
export interface DiffHunk {
|
|
/** File path being diffed */
|
|
path: string;
|
|
|
|
/** Diff lines */
|
|
lines: DiffLine[];
|
|
|
|
/** Whether this is truncated */
|
|
truncated?: boolean;
|
|
}
|
|
|
|
/**
|
|
* Parse unified diff format into structured lines
|
|
*/
|
|
export function parseDiff(diffText: string): DiffLine[] {
|
|
const lines: DiffLine[] = [];
|
|
// Handle empty input - split returns [''] for empty string
|
|
if (diffText === '') {
|
|
return lines;
|
|
}
|
|
const rawLines = diffText.split('\n');
|
|
|
|
let oldLineNum = 1;
|
|
let newLineNum = 1;
|
|
|
|
for (const line of rawLines) {
|
|
// Hunk header @@ -a,b +c,d @@
|
|
if (line.startsWith('@@')) {
|
|
const match = line.match(/@@ -(\d+)(?:,\d+)? \+(\d+)(?:,\d+)? @@/);
|
|
if (match) {
|
|
oldLineNum = parseInt(match[1], 10);
|
|
newLineNum = parseInt(match[2], 10);
|
|
}
|
|
lines.push({ type: 'header', content: line });
|
|
continue;
|
|
}
|
|
|
|
// File header
|
|
if (line.startsWith('---') || line.startsWith('+++') || line.startsWith('diff')) {
|
|
lines.push({ type: 'header', content: line });
|
|
continue;
|
|
}
|
|
|
|
// Context line
|
|
if (line.startsWith(' ') || line === '') {
|
|
lines.push({
|
|
type: 'context',
|
|
oldLine: oldLineNum++,
|
|
newLine: newLineNum++,
|
|
content: line.slice(1),
|
|
});
|
|
continue;
|
|
}
|
|
|
|
// Added line
|
|
if (line.startsWith('+')) {
|
|
lines.push({
|
|
type: 'added',
|
|
newLine: newLineNum++,
|
|
content: line.slice(1),
|
|
});
|
|
continue;
|
|
}
|
|
|
|
// Removed line
|
|
if (line.startsWith('-')) {
|
|
lines.push({
|
|
type: 'removed',
|
|
oldLine: oldLineNum++,
|
|
content: line.slice(1),
|
|
});
|
|
continue;
|
|
}
|
|
|
|
// Other lines (e.g., index, mode changes)
|
|
lines.push({ type: 'context', content: line });
|
|
}
|
|
|
|
return lines;
|
|
}
|
|
|
|
/**
|
|
* Format a single diff line for blessed display
|
|
*/
|
|
function formatDiffLine(line: DiffLine, width: number): string {
|
|
const maxContentWidth = width - 12; // Account for line numbers and padding
|
|
|
|
switch (line.type) {
|
|
case 'header':
|
|
return `{cyan-fg}${line.content.slice(0, maxContentWidth)}{/}`;
|
|
|
|
case 'added':
|
|
const addedNum = line.newLine?.toString().padStart(4) || ' ';
|
|
return `{green-fg}+${addedNum} ${line.content.slice(0, maxContentWidth)}{/}`;
|
|
|
|
case 'removed':
|
|
const removedNum = line.oldLine?.toString().padStart(4) || ' ';
|
|
return `{red-fg}-${removedNum} ${line.content.slice(0, maxContentWidth)}{/}`;
|
|
|
|
case 'context':
|
|
const oldNum = line.oldLine?.toString().padStart(4) || ' ';
|
|
const newNum = line.newLine?.toString().padStart(4) || ' ';
|
|
const truncatedContent = line.content.slice(0, maxContentWidth - 10);
|
|
return `{gray-fg} ${oldNum} ${newNum} ${truncatedContent}{/}`;
|
|
|
|
default:
|
|
return line.content;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* DiffView displays inline diffs from Edit tool calls
|
|
*/
|
|
export class DiffView {
|
|
private box: blessed.Widgets.BoxElement;
|
|
private currentHunk: DiffHunk | null = null;
|
|
private maxLines: number;
|
|
|
|
constructor(options: DiffViewOptions) {
|
|
this.maxLines = options.maxLines || 50;
|
|
|
|
this.box = blessed.box({
|
|
parent: options.parent,
|
|
tags: true,
|
|
top: options.top,
|
|
left: options.left,
|
|
width: options.width,
|
|
height: options.height,
|
|
label: ' Diff View ',
|
|
border: { type: 'line' },
|
|
style: {
|
|
border: { fg: colors.border },
|
|
label: { fg: colors.header },
|
|
},
|
|
scrollable: true,
|
|
alwaysScroll: true,
|
|
keys: true,
|
|
vi: true,
|
|
mouse: true,
|
|
hidden: true,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Set the diff to display
|
|
*/
|
|
setDiff(path: string, diffText: string): void {
|
|
const lines = parseDiff(diffText);
|
|
const truncated = lines.length > this.maxLines;
|
|
|
|
this.currentHunk = {
|
|
path,
|
|
lines: truncated ? lines.slice(0, this.maxLines) : lines,
|
|
truncated,
|
|
};
|
|
|
|
this.render();
|
|
}
|
|
|
|
/**
|
|
* Set diff from Edit tool parameters
|
|
*/
|
|
setEditDiff(path: string, oldString: string, newString: string): void {
|
|
// Generate a simple unified diff
|
|
const diff = this.generateSimpleDiff(path, oldString, newString);
|
|
this.setDiff(path, diff);
|
|
}
|
|
|
|
/**
|
|
* Generate a simple unified diff from old/new strings
|
|
*/
|
|
private generateSimpleDiff(path: string, oldString: string, newString: string): string {
|
|
const oldLines = oldString.split('\n');
|
|
const newLines = newString.split('\n');
|
|
|
|
let diff = `--- a/${path}\n+++ b/${path}\n@@ -1,${oldLines.length} +1,${newLines.length} @@\n`;
|
|
|
|
// Show removed lines
|
|
for (const line of oldLines) {
|
|
diff += `-${line}\n`;
|
|
}
|
|
|
|
// Show added lines
|
|
for (const line of newLines) {
|
|
diff += `+${line}\n`;
|
|
}
|
|
|
|
return diff;
|
|
}
|
|
|
|
/**
|
|
* Render the current diff
|
|
*/
|
|
render(): void {
|
|
if (!this.currentHunk) {
|
|
this.box.setContent('{gray-fg}No diff to display{/}');
|
|
this.box.screen.render();
|
|
return;
|
|
}
|
|
|
|
const hunk = this.currentHunk;
|
|
const width = (this.box.width as number) - 2; // Account for border
|
|
const lines: string[] = [];
|
|
|
|
// Header with file path
|
|
lines.push(`{bold}${hunk.path}{/}`);
|
|
lines.push('{gray-fg}─────────────────────────────────────{/}');
|
|
lines.push('');
|
|
|
|
// Diff lines
|
|
for (const line of hunk.lines) {
|
|
lines.push(formatDiffLine(line, width));
|
|
}
|
|
|
|
// Truncation notice
|
|
if (hunk.truncated) {
|
|
lines.push('');
|
|
lines.push('{yellow-fg}... truncated (press Enter to expand){/}');
|
|
}
|
|
|
|
this.box.setContent(lines.join('\n'));
|
|
this.box.screen.render();
|
|
}
|
|
|
|
/**
|
|
* Show the diff view
|
|
*/
|
|
show(): void {
|
|
this.box.show();
|
|
this.box.screen.render();
|
|
}
|
|
|
|
/**
|
|
* Hide the diff view
|
|
*/
|
|
hide(): void {
|
|
this.box.hide();
|
|
this.box.screen.render();
|
|
}
|
|
|
|
/**
|
|
* Toggle visibility
|
|
*/
|
|
toggle(): void {
|
|
if (this.box.hidden) {
|
|
this.show();
|
|
} else {
|
|
this.hide();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check if visible
|
|
*/
|
|
isVisible(): boolean {
|
|
return !this.box.hidden;
|
|
}
|
|
|
|
/**
|
|
* Get current hunk
|
|
*/
|
|
getHunk(): DiffHunk | null {
|
|
return this.currentHunk;
|
|
}
|
|
|
|
/**
|
|
* Clear the diff
|
|
*/
|
|
clear(): void {
|
|
this.currentHunk = null;
|
|
this.box.setContent('{gray-fg}No diff to display{/}');
|
|
this.box.screen.render();
|
|
}
|
|
|
|
/**
|
|
* Focus this component
|
|
*/
|
|
focus(): void {
|
|
this.box.focus();
|
|
}
|
|
|
|
/**
|
|
* Get the underlying blessed element
|
|
*/
|
|
getElement(): blessed.Widgets.BoxElement {
|
|
return this.box;
|
|
}
|
|
}
|
|
|
|
export default DiffView;
|