From 662301a78c05a54daaff8be5f7ebd19061412d8e Mon Sep 17 00:00:00 2001 From: Michael Neale Date: Tue, 27 Jan 2026 12:02:48 +1100 Subject: [PATCH] show an explanation of how things were as well as the change --- .gitignore | 4 +- src-tauri/src/ai/prompt.rs | 3 + src-tauri/src/ai/types.rs | 4 + src-tauri/src/git/files.rs | 2 +- src/lib/BeforeAnnotationOverlay.svelte | 101 +++++++++++++++++++++++++ src/lib/DiffViewer.svelte | 71 ++++++++++++++++- src/lib/types.ts | 2 + 7 files changed, 183 insertions(+), 4 deletions(-) create mode 100644 src/lib/BeforeAnnotationOverlay.svelte diff --git a/.gitignore b/.gitignore index bd560c4..d6d8367 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,9 @@ yarn-debug.log* yarn-error.log* pnpm-debug.log* lerna-debug.log* +.g3/ +analysis/ +requirements.md node_modules dist @@ -24,4 +27,3 @@ dist-ssr *.sln *.sw? .claude/ - diff --git a/src-tauri/src/ai/prompt.rs b/src-tauri/src/ai/prompt.rs index 11e9f5c..c887174 100644 --- a/src-tauri/src/ai/prompt.rs +++ b/src-tauri/src/ai/prompt.rs @@ -81,6 +81,8 @@ Respond with ONLY valid JSON matching this structure (no markdown code fences, n { "id": "1", "file_path": "path/to/file.rs", + "before_span": {"start": 8, "end": 15}, + "before_description": "Previously handled errors by panicking", "after_span": {"start": 10, "end": 20}, "content": "Your commentary on this section", "category": "explanation" @@ -99,6 +101,7 @@ Rules: - "id": Unique across ALL annotations (use "1", "2", "3", etc.) - "file_path": Must match the key exactly - "before_span": Line range in BEFORE content (0-indexed, exclusive end). Omit if only about new code. + - "before_description": When before_span is provided, describe what the old code was doing (1 sentence). Required if before_span is set. - "after_span": Line range in AFTER content (0-indexed, exclusive end). Omit if only about deleted code. - "content": Your commentary (1-3 sentences) - "category": One of "explanation", "warning", "suggestion", "context""#; diff --git a/src-tauri/src/ai/types.rs b/src-tauri/src/ai/types.rs index bbb59c3..4aa7557 100644 --- a/src-tauri/src/ai/types.rs +++ b/src-tauri/src/ai/types.rs @@ -31,6 +31,10 @@ pub struct SmartDiffAnnotation { /// Unique identifier for this annotation pub id: String, + /// Description of the old state (for before_span annotations) + #[serde(skip_serializing_if = "Option::is_none")] + pub before_description: Option, + /// File path this annotation belongs to #[serde(skip_serializing_if = "Option::is_none")] pub file_path: Option, diff --git a/src-tauri/src/git/files.rs b/src-tauri/src/git/files.rs index f2786e3..a9e2509 100644 --- a/src-tauri/src/git/files.rs +++ b/src-tauri/src/git/files.rs @@ -212,7 +212,7 @@ mod tests { // No match assert!(fuzzy_match("src/main.rs", "xyz").is_none()); - assert!(fuzzy_match("src/main.rs", "srm").is_none()); // chars not in order + assert!(fuzzy_match("src/main.rs", "nim").is_none()); // 'n' before 'i' before 'm' - but in path it's m-a-i-n } #[test] diff --git a/src/lib/BeforeAnnotationOverlay.svelte b/src/lib/BeforeAnnotationOverlay.svelte new file mode 100644 index 0000000..e266d41 --- /dev/null +++ b/src/lib/BeforeAnnotationOverlay.svelte @@ -0,0 +1,101 @@ + + + +
+

{displayText}

+
+ + diff --git a/src/lib/DiffViewer.svelte b/src/lib/DiffViewer.svelte index 258b62b..25b5cbc 100644 --- a/src/lib/DiffViewer.svelte +++ b/src/lib/DiffViewer.svelte @@ -49,6 +49,7 @@ import { DiffSpec } from './types'; import CommentEditor from './CommentEditor.svelte'; import AnnotationOverlay from './AnnotationOverlay.svelte'; + import BeforeAnnotationOverlay from './BeforeAnnotationOverlay.svelte'; import { smartDiffState, setAnnotationsRevealed } from './stores/smartDiff.svelte'; import Scrollbar from './Scrollbar.svelte'; @@ -105,6 +106,9 @@ /** Tracked width of afterPane for annotation overlays (updated via ResizeObserver) */ let afterPaneWidth = $state(0); + /** Tracked width of beforePane for annotation overlays (updated via ResizeObserver) */ + let beforePaneWidth = $state(0); + // ========================================================================== // Highlighter state // ========================================================================== @@ -212,7 +216,7 @@ if (!currentFilePath) return []; const result = smartDiffState.results.get(currentFilePath); if (!result) return []; - // Only return annotations with after_span (we're ignoring before-only for now) + // Return annotations with after_span for the right pane const annotations = result.annotations.filter((a) => a.after_span); if (annotations.length > 0) { console.log( @@ -226,6 +230,19 @@ return annotations; }); + // AI annotations with before_span for the left pane + let beforeFileAnnotations = $derived.by(() => { + if (!currentFilePath) return []; + const result = smartDiffState.results.get(currentFilePath); + if (!result) return []; + // Return annotations with before_span for the left pane + return result.annotations.filter((a) => a.before_span); + }); + + let showBeforeAnnotations = $derived( + beforeFileAnnotations.length > 0 && smartDiffState.showAnnotations + ); + // Whether annotations should be shown (have results and not globally hidden) let showAiAnnotations = $derived( currentFileAnnotations.length > 0 && smartDiffState.showAnnotations @@ -320,13 +337,26 @@ // Scrollbar marker computation let beforeMarkers = $derived.by(() => { if (beforeLines.length === 0) return []; - return changedAlignments.map(({ alignment }) => { + const changeMarkers = changedAlignments.map(({ alignment }) => { const span = alignment.before; const startPercent = (span.start / beforeLines.length) * 100; const rangeSize = span.end - span.start; const heightPercent = Math.max(0.5, (rangeSize / beforeLines.length) * 100); return { top: startPercent, height: heightPercent, type: 'change' as const }; }); + + // AI annotation markers for before pane + const annotationMarkers = beforeFileAnnotations + .filter((a) => a.before_span) + .map((annotation) => { + const span = annotation.before_span!; + const startPercent = (span.start / beforeLines.length) * 100; + const rangeSize = Math.max(1, span.end - span.start); + const heightPercent = Math.max(0.5, (rangeSize / beforeLines.length) * 100); + return { top: startPercent, height: heightPercent, type: 'annotation' as const }; + }); + + return [...changeMarkers, ...annotationMarkers]; }); let afterMarkers = $derived.by(() => { @@ -1289,6 +1319,24 @@ return () => resizeObserver.disconnect(); }); + // Track beforePane width for annotation overlays + $effect(() => { + if (!beforePane) return; + + // Set initial width + beforePaneWidth = beforePane.clientWidth; + + // Update on resize + const resizeObserver = new ResizeObserver((entries) => { + for (const entry of entries) { + beforePaneWidth = entry.contentRect.width; + } + }); + resizeObserver.observe(beforePane); + + return () => resizeObserver.disconnect(); + }); + // Handle external scroll target requests (e.g., from sidebar comment clicks) $effect(() => { const targetLine = diffState.scrollTargetLine; @@ -1386,6 +1434,25 @@ {/if} + + {#if showBeforeAnnotations && annotationsRevealed} + {@const lineHeight = scrollController.getDimensions('before').lineHeight || 20} +
+ {#each beforeFileAnnotations as annotation} + {#if annotation.before_span} + + {/if} + {/each} +
+ {/if} diff --git a/src/lib/types.ts b/src/lib/types.ts index b834074..2d9c00e 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -213,6 +213,8 @@ export type AnnotationCategory = 'explanation' | 'warning' | 'suggestion' | 'con /** A single AI annotation on a diff */ export interface SmartDiffAnnotation { id: string; + /** Description of the old state (for before_span annotations) */ + before_description?: string; /** File path this annotation belongs to (for changeset-level analysis) */ file_path?: string; /** Span in 'before' content (undefined if only applies to 'after') */