Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
.g3/
analysis/
requirements.md

node_modules
dist
Expand All @@ -24,4 +27,3 @@ dist-ssr
*.sln
*.sw?
.claude/

3 changes: 3 additions & 0 deletions src-tauri/src/ai/prompt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,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"
Expand All @@ -263,6 +265,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""#;
Expand Down
4 changes: 4 additions & 0 deletions src-tauri/src/ai/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<String>,

/// File path this annotation belongs to
#[serde(skip_serializing_if = "Option::is_none")]
pub file_path: Option<String>,
Expand Down
2 changes: 1 addition & 1 deletion src-tauri/src/git/files.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
101 changes: 101 additions & 0 deletions src/lib/BeforeAnnotationOverlay.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
<!--
BeforeAnnotationOverlay.svelte - AI annotation blur overlay for the "before" pane

Renders a blurred overlay on the left (before) pane showing what the old code
was doing. Uses the before_description field from annotations.
-->
<script lang="ts">
import type { SmartDiffAnnotation } from './types';

interface Props {
annotation: SmartDiffAnnotation;
/** Top position in pixels (relative to lines-wrapper) */
top: number;
/** Height in pixels */
height: number;
/** Whether the annotation is currently revealed (overlay visible) */
revealed: boolean;
/** Width of the visible container in pixels */
containerWidth: number;
}

let { annotation, top, height, revealed, containerWidth }: Props = $props();

// Use before_description if available, otherwise fall back to a generic message
let displayText = $derived(annotation.before_description || 'Previous implementation');
</script>

<div
class="before-annotation-overlay"
class:revealed
class:category-explanation={annotation.category === 'explanation'}
class:category-warning={annotation.category === 'warning'}
class:category-suggestion={annotation.category === 'suggestion'}
class:category-context={annotation.category === 'context'}
style="top: {top}px; height: {height}px; width: {containerWidth}px;"
>
<p class="annotation-text">{displayText}</p>
</div>

<style>
.before-annotation-overlay {
position: absolute;
left: 0;
z-index: 10;

/* Width is set via inline style from containerWidth prop */

/* Blur effect on the code underneath */
backdrop-filter: blur(6px);
-webkit-backdrop-filter: blur(6px);

/* Layout - right aligned for before pane (opposite of after pane) */
display: flex;
align-items: center;
justify-content: flex-end;
padding: 8px 16px 8px 12px; /* extra right padding for space before the accent bar */

/* Transitions - default hidden, shown when holding A */
opacity: 0;
pointer-events: none;
transition: opacity 200ms ease-out;

/* Category accent on right edge (opposite of after pane) */
border-right: 3px solid var(--annotation-accent);
}

.before-annotation-overlay.revealed {
opacity: 1;
pointer-events: auto;
}

/* Category colors */
.category-explanation {
--annotation-accent: var(--text-accent);
}

.category-warning {
--annotation-accent: var(--status-modified);
}

.category-suggestion {
--annotation-accent: var(--status-added);
}

.category-context {
--annotation-accent: var(--text-muted);
}

.annotation-text {
margin: 0;
font-size: var(--size-sm);
line-height: 1.5;
color: var(--text-secondary);
font-style: italic;
text-align: right;
/* Wrap text within container */
white-space: normal;
word-wrap: break-word;
overflow-wrap: break-word;
}
</style>
71 changes: 69 additions & 2 deletions src/lib/DiffViewer.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
import { DiffSpec, gitRefDisplay } 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';

Expand Down Expand Up @@ -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
// ==========================================================================
Expand Down Expand Up @@ -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(
Expand All @@ -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
Expand Down Expand Up @@ -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(() => {
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -1386,6 +1434,25 @@
</div>
{/if}
</div>
<!-- Full-pane AI blur overlay with before_description text -->
{#if showBeforeAnnotations && annotationsRevealed}
{@const lineHeight = scrollController.getDimensions('before').lineHeight || 20}
<div class="ai-blur-overlay">
{#each beforeFileAnnotations as annotation}
{#if annotation.before_span}
<BeforeAnnotationOverlay
{annotation}
top={annotation.before_span.start * lineHeight -
scrollController.beforeScrollY}
height={(annotation.before_span.end - annotation.before_span.start) *
lineHeight}
revealed={true}
containerWidth={beforePaneWidth}
/>
{/if}
{/each}
</div>
{/if}
</div>
</div>
</div>
Expand Down
2 changes: 2 additions & 0 deletions src/lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,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') */
Expand Down
Loading