diff --git a/.gitignore b/.gitignore index 77c7cee5..660a2107 100644 --- a/.gitignore +++ b/.gitignore @@ -50,7 +50,3 @@ temp-videos/ # Terminal output cache .terminal-output-cache/ - -/okegas -/okegas (1) -/okegas (2) diff --git a/frontend/components/chat/tools/variants/classic/EditTool.svelte b/frontend/components/chat/tools/variants/classic/EditTool.svelte index f52f3909..efdbd9c8 100644 --- a/frontend/components/chat/tools/variants/classic/EditTool.svelte +++ b/frontend/components/chat/tools/variants/classic/EditTool.svelte @@ -1,6 +1,7 @@ diff --git a/frontend/components/chat/tools/variants/classic/WriteTool.svelte b/frontend/components/chat/tools/variants/classic/WriteTool.svelte index 0c708540..38352356 100644 --- a/frontend/components/chat/tools/variants/classic/WriteTool.svelte +++ b/frontend/components/chat/tools/variants/classic/WriteTool.svelte @@ -2,6 +2,7 @@ import type { ToolUseBlock, WriteInput } from '$shared/types/unified'; import { FileHeader, DiffBlock } from './components'; import TextMessage from '../../../formatters/TextMessage.svelte'; + import { addAiChange } from '$frontend/utils/ai-changes'; const { toolInput }: { toolInput: ToolUseBlock } = $props(); const input = $derived(toolInput.input as WriteInput); @@ -9,11 +10,21 @@ const filePath = $derived(input.filePath || ''); const fileName = $derived(filePath.split(/[/\\]/).pop() || filePath || 'unknown'); const content = $derived(input.content || ''); + + const hasResult = $derived(!!toolInput.result && !toolInput.result.isError); + let editIndex = $state(null); + + $effect(() => { + if (hasResult && filePath && content) { + editIndex = addAiChange(filePath, '', content); + } + }); diff --git a/frontend/components/chat/tools/variants/classic/components/FileHeader.svelte b/frontend/components/chat/tools/variants/classic/components/FileHeader.svelte index 237a46c0..ca143026 100644 --- a/frontend/components/chat/tools/variants/classic/components/FileHeader.svelte +++ b/frontend/components/chat/tools/variants/classic/components/FileHeader.svelte @@ -2,9 +2,13 @@ import Icon from '$frontend/components/common/display/Icon.svelte'; import { getFileIcon } from '$frontend/utils/file-icon-mappings'; import { revealFile } from '$frontend/stores/ui/file-peek.svelte'; + import { requestAiScrollReveal } from '$frontend/utils/ai-changes'; function handleClick() { revealFile(filePath); + if (editIndex !== undefined && editIndex !== null) { + requestAiScrollReveal(filePath, editIndex); + } } interface Props { @@ -13,9 +17,10 @@ iconColor?: string; badges?: Array<{ text: string; color: string }>; box?: boolean; + editIndex?: number | null; } - const { filePath, fileName, iconColor, badges = [], box = true }: Props = $props(); + const { filePath, fileName, iconColor, badges = [], box = true, editIndex = null }: Props = $props(); const displayFileName = $derived(fileName || filePath.split(/[/\\]/).pop() || filePath); diff --git a/frontend/components/chat/tools/variants/compact/EditTool.svelte b/frontend/components/chat/tools/variants/compact/EditTool.svelte index c6207fee..a6921b75 100644 --- a/frontend/components/chat/tools/variants/compact/EditTool.svelte +++ b/frontend/components/chat/tools/variants/compact/EditTool.svelte @@ -2,6 +2,7 @@ import type { ToolUseBlock, EditInput } from '$shared/types/unified'; import { countLineChanges } from '$shared/utils/diff-calculator'; import { ToolRow } from './components'; + import { addAiChange } from '$frontend/utils/ai-changes'; const { toolInput }: { toolInput: ToolUseBlock } = $props(); const input = $derived(toolInput.input as EditInput); @@ -12,7 +13,18 @@ // Count only lines that actually changed (LCS-based), so identical context // lines inside the edited region aren't miscounted as +/-. const diff = $derived(countLineChanges(input.oldString || '', input.newString || '')); + + const hasResult = $derived(!!toolInput.result && !toolInput.result.isError); + const oldString = $derived(input.oldString || ''); + const newString = $derived(input.newString || ''); + let editIndex = $state(null); + + $effect(() => { + if (hasResult && filePath) { + editIndex = addAiChange(filePath, oldString, newString); + } + }); - + diff --git a/frontend/components/chat/tools/variants/compact/WriteTool.svelte b/frontend/components/chat/tools/variants/compact/WriteTool.svelte index 5affde22..7eceb17c 100644 --- a/frontend/components/chat/tools/variants/compact/WriteTool.svelte +++ b/frontend/components/chat/tools/variants/compact/WriteTool.svelte @@ -1,6 +1,7 @@ - + diff --git a/frontend/components/chat/tools/variants/compact/components/FileHeader.svelte b/frontend/components/chat/tools/variants/compact/components/FileHeader.svelte index ac96cf31..910aba5f 100644 --- a/frontend/components/chat/tools/variants/compact/components/FileHeader.svelte +++ b/frontend/components/chat/tools/variants/compact/components/FileHeader.svelte @@ -1,19 +1,24 @@ diff --git a/frontend/components/chat/tools/variants/compact/components/ToolRow.svelte b/frontend/components/chat/tools/variants/compact/components/ToolRow.svelte index c922b536..ec07c557 100644 --- a/frontend/components/chat/tools/variants/compact/components/ToolRow.svelte +++ b/frontend/components/chat/tools/variants/compact/components/ToolRow.svelte @@ -3,6 +3,7 @@ import { getFileIcon } from '$frontend/utils/file-icon-mappings'; import { getFolderIcon } from '$frontend/utils/folder-icon-mappings'; import { revealFile } from '$frontend/stores/ui/file-peek.svelte'; + import { requestAiScrollReveal } from '$frontend/utils/ai-changes'; import type { IconName } from '$shared/types/ui/icons'; interface DiffStat { @@ -37,6 +38,8 @@ expanded?: boolean; /** Called when header row is clicked (for expandable rows) */ onclick?: () => void; + /** AI edit index for scroll-reveal targeting */ + editIndex?: number | null; } let { @@ -53,6 +56,7 @@ expandable = false, expanded = $bindable(false), onclick, + editIndex = null, }: Props = $props(); const displayName = $derived(fileName || (filePath ? filePath.split(/[/\\]/).pop() || filePath : '')); @@ -68,6 +72,9 @@ e.stopPropagation(); if (!filePath) return; revealFile(filePath); + if (editIndex !== null) { + requestAiScrollReveal(filePath, editIndex); + } } function handleRowClick() { diff --git a/frontend/components/files/FileNode.svelte b/frontend/components/files/FileNode.svelte index a5f19a90..55c70bd9 100644 --- a/frontend/components/files/FileNode.svelte +++ b/frontend/components/files/FileNode.svelte @@ -36,7 +36,8 @@ onNodeDrop, onNodeDragEnd, dropTargetPath = null, - busyPaths = new Set() + busyPaths = new Set(), + aiChangesSet = new Set() }: { file: FileNodeType; isSelected?: boolean; @@ -63,6 +64,7 @@ onNodeDragEnd?: (file: FileNodeType, event: DragEvent) => void; dropTargetPath?: string | null; busyPaths?: Set; + aiChangesSet?: Set; } = $props(); const revealLabel = $derived( @@ -119,6 +121,11 @@ gitStatusCode ? getGitStatusBadgeLabel(gitStatusCode) : '' ); + // AI changes indicator + const hasAiChanges = $derived( + file.type === 'file' && aiChangesSet.has(file.path) + ); + let nodeElement: HTMLDivElement; let menuButtonElement: HTMLButtonElement; let menuStyle = $state(''); @@ -258,8 +265,8 @@ {file.name} - - {#if showModifiedIndicator || gitStatusCode} + + {#if showModifiedIndicator || hasAiChanges || gitStatusCode} {#if showModifiedIndicator} {/if} + {#if hasAiChanges} + + {/if} {#if gitStatusCode} {/each} {/if} diff --git a/frontend/components/files/FileTree.svelte b/frontend/components/files/FileTree.svelte index 18c2e926..bcc982c2 100644 --- a/frontend/components/files/FileTree.svelte +++ b/frontend/components/files/FileTree.svelte @@ -66,6 +66,8 @@ busyPaths?: Set; /** Root-level operation in progress (e.g. upload to project root). */ isRootBusy?: boolean; + /** File paths that have pending AI changes (for dot indicator). */ + aiChangesSet?: Set; } let { @@ -100,7 +102,8 @@ onClearSelection, isRootDropTarget = false, busyPaths = new Set(), - isRootBusy = false + isRootBusy = false, + aiChangesSet = new Set() }: Props = $props(); // Create local state if expandedFolders is not provided @@ -839,6 +842,7 @@ {onNodeDragEnd} {dropTargetPath} {busyPaths} + {aiChangesSet} /> {/each} diff --git a/frontend/components/files/FileViewer.svelte b/frontend/components/files/FileViewer.svelte index ec81f85a..69d1a295 100644 --- a/frontend/components/files/FileViewer.svelte +++ b/frontend/components/files/FileViewer.svelte @@ -11,7 +11,7 @@ import { getFolderIcon } from '$frontend/utils/folder-icon-mappings'; import { isImageFile, isSvgFile, isPdfFile, isAudioFile, isVideoFile, isBinaryFile, isBinaryContent, isPreviewableFile, isEditableImageFile } from '$frontend/utils/file-type'; import { formatFileSize } from '$frontend/utils/format'; - import { onMount } from 'svelte'; + import { onMount, untrack } from 'svelte'; import type { IconName } from '$shared/types/ui/icons'; import type { editor } from 'monaco-editor'; import { debug } from '$shared/utils/logger'; @@ -20,6 +20,8 @@ import { gitStatusState } from '$frontend/stores/features/git-status.svelte'; import { settings } from '$frontend/stores/features/settings.svelte'; import { revealFile } from '$frontend/stores/ui/file-peek.svelte'; + import { getAiChanges, clearAiChange, onAiChange, onAiScrollReveal, consumeAiScrollReveal, getGutterViewMode, setGutterViewMode, onGutterViewModeChange } from '$frontend/utils/ai-changes'; + import type { GutterViewMode } from '$frontend/utils/ai-changes'; // Interface untuk MonacoCodeEditor component interface MonacoEditorComponent { @@ -150,9 +152,15 @@ // Git gutter decorations + HEAD content cache let gutterDecorations: string[] = []; + let aiChangeDecorations: string[] = []; let envDecorations: string[] = []; let envDecoEditor: editor.IStandaloneCodeEditor | null = null; let gutterChanges: GutterChange[] = []; + let aiGutterChanges: GutterChange[] = []; + let activeRevealEditIdx = $state(null); + let lastRevealEditIdx = $state(null); + let hasAiChanges = $state(false); + let gutterMode = $state(getGutterViewMode()); let headContent = $state(null); let headContentForPath = ''; let pendingScrollRestore: number | null = null; @@ -162,6 +170,7 @@ let activeDiffZone: { id: string; line: number; + isAi: boolean; escHandler: (e: KeyboardEvent) => void; domNode: HTMLElement; overlayWidget: editor.IOverlayWidget; @@ -360,11 +369,19 @@ if (!path || !projectId) { headContent = null; headContentForPath = ''; + activeRevealEditIdx = null; + lastRevealEditIdx = null; + setGutterViewMode('git'); + closeDiffPeek(); return; } if (path === headContentForPath) return; headContentForPath = path; headContent = null; + activeRevealEditIdx = null; + lastRevealEditIdx = null; + setGutterViewMode('git'); + closeDiffPeek(); // Only fetch when git is tracking this file if (!gitStatusState.isRepo) return; @@ -400,6 +417,169 @@ } }); + // React to gutter view mode changes (AI vs Git) + $effect(() => { + const mode = gutterMode; + const editor = monacoEditorRef?.getEditor(); + if (!editor) return; + if (mode === 'ai') { + untrack(() => { + if (activeRevealEditIdx === null && lastRevealEditIdx === null) { + const allChanges = getAiChanges(file?.path || ''); + if (allChanges.length > 0) { + activeRevealEditIdx = allChanges.length - 1; + lastRevealEditIdx = allChanges.length - 1; + } + } + }); + applyAiChangeDecorations(); // re-apply AI gutter + updateGutterDecorations(true); // clear git gutter + } else { + applyAiChangeDecorations(true); // clear AI gutter + scheduleGutterUpdate(); + if (activeDiffZone && activeDiffZone.isAi) { + closeDiffPeek(); + } + } + }); + + // Sync with external preference changes + onMount(() => { + const unsub = onGutterViewModeChange((mode) => { + gutterMode = mode; + }); + return unsub; + }); + + // AI change decorations — highlight lines modified by AI (Write/Edit tools) + function applyAiChangeDecorations(forceClear = false) { + const editor = monacoEditorRef?.getEditor(); + if (!editor) return; + + const path = file?.path || ''; + if (!path) { + aiChangeDecorations = editor.deltaDecorations(aiChangeDecorations, []); + hasAiChanges = false; + return; + } + + const allChanges = getAiChanges(path); + hasAiChanges = allChanges.length > 0; + + // Capture scroll-reveal edit index first so we can filter by it on this pass + const revealEditIdx = consumeAiScrollReveal(path); + if (revealEditIdx >= 0) { + activeRevealEditIdx = revealEditIdx; + lastRevealEditIdx = revealEditIdx; + setGutterViewMode('ai'); + } + + if (forceClear || gutterMode === 'git') { + aiChangeDecorations = editor.deltaDecorations(aiChangeDecorations, []); + aiGutterChanges = []; + return; + } + + if (allChanges.length === 0) { + aiChangeDecorations = editor.deltaDecorations(aiChangeDecorations, []); + return; + } + + const merged: Array = []; + + for (let editIdx = 0; editIdx < allChanges.length; editIdx++) { + if (activeRevealEditIdx !== null && editIdx !== activeRevealEditIdx) continue; + + const change = allChanges[editIdx]; + const rawChanges = change.oldContent + ? computeLineDiff(change.oldContent, change.newContent) + : computeLineDiff('', change.newContent); + + let local: GutterChange[]; + if (!change.oldContent) { + local = rawChanges; + } else { + const idx = editableContent.indexOf(change.newContent); + if (idx >= 0) { + const beforeContent = editableContent.substring(0, idx); + const startLine = beforeContent.split('\n').length; + local = rawChanges.map((c) => ({ + ...c, + startLine: c.startLine + startLine - 1, + endLine: c.endLine + startLine - 1 + })); + } else { + local = []; + } + } + + for (const gc of local) { + merged.push({ ...gc, _editIdx: editIdx, _timestamp: change.timestamp }); + } + } + + const newDecorations = merged.map((entry) => { + const { _editIdx, _timestamp, ...change } = entry; + + const options: any = { + isWholeLine: false, + linesDecorationsClassName: `ai-gutter-${change.type}`, + overviewRuler: { + color: colorForChangeType(change.type), + position: OVERVIEW_RULER_RIGHT + } + }; + + return { + range: { + startLineNumber: change.startLine, + startColumn: 1, + endLineNumber: change.endLine, + endColumn: 1 + }, + options + }; + }); + + // Store tagged changes for peek + aiGutterChanges = merged.map(({ _editIdx, _timestamp, ...change }) => ({ + ...change, + editIndex: _editIdx, + timestamp: _timestamp, + })); + aiChangeDecorations = editor.deltaDecorations(aiChangeDecorations, newDecorations); + + // Scroll-reveal: focus on the first line of the specific edit the user clicked + if (revealEditIdx >= 0 && aiGutterChanges.length > 0) { + const targetChange = aiGutterChanges.find((g) => g.editIndex === revealEditIdx) + ?? aiGutterChanges[0]; + requestAnimationFrame(() => { + editor.revealLineInCenter(targetChange.startLine); + editor.setPosition({ lineNumber: targetChange.startLine, column: 1 }); + editor.focus(); + showDiffPeek(targetChange, true); + }); + } + + } + + // Register callback for AI change notifications (fires when a new change arrives) + onMount(() => { + const unregister = onAiChange(() => { + applyAiChangeDecorations(); + }); + const unregisterReveal = onAiScrollReveal((revealPath) => { + const currentPath = file?.path || ''; + if (revealPath === currentPath) { + applyAiChangeDecorations(); + } + }); + return () => { + unregister(); + unregisterReveal(); + }; + }); + function scheduleGutterUpdate() { if (gutterUpdateTimer) clearTimeout(gutterUpdateTimer); gutterUpdateTimer = setTimeout(() => { @@ -415,15 +595,26 @@ return type === 'added' ? '#10b981' : type === 'modified' ? '#3b82f6' : '#ef4444'; } - function updateGutterDecorations() { + function updateGutterDecorations(forceClear = false) { const editor = monacoEditorRef?.getEditor(); if (!editor) return; + if (forceClear || gutterMode === 'ai') { + gutterChanges = []; + gutterDecorations = editor.deltaDecorations(gutterDecorations, []); + if (activeDiffZone && !activeDiffZone.isAi) { + closeDiffPeek(); + } + return; + } + // No HEAD content (untracked, missing repo) — clear any existing gutter if (headContent === null || headContent === undefined) { gutterChanges = []; gutterDecorations = editor.deltaDecorations(gutterDecorations, []); - closeDiffPeek(); + if (activeDiffZone && !activeDiffZone.isAi) { + closeDiffPeek(); + } return; } @@ -431,7 +622,7 @@ gutterChanges = changes; // Close any open peek whose anchor line is no longer marked as changed - if (activeDiffZone) { + if (activeDiffZone && !activeDiffZone.isAi) { const stillExists = changes.some( (c) => activeDiffZone!.line >= c.startLine && activeDiffZone!.line <= c.endLine ); @@ -465,9 +656,12 @@ gutterDecorations = editor.deltaDecorations(gutterDecorations, newDecorations); } - function findChangeAtLine(line: number): GutterChange | null { + function findChangeAtLine(line: number): { change: GutterChange; isAi: boolean } | null { for (const change of gutterChanges) { - if (line >= change.startLine && line <= change.endLine) return change; + if (line >= change.startLine && line <= change.endLine) return { change, isAi: false }; + } + for (const change of aiGutterChanges) { + if (line >= change.startLine && line <= change.endLine) return { change, isAi: true }; } return null; } @@ -545,7 +739,7 @@ }); } - function buildPeekDom(change: GutterChange): HTMLElement { + function buildPeekDom(change: GutterChange, isAi = false): HTMLElement { const root = document.createElement('div'); root.className = `git-diff-peek git-diff-peek-${change.type}`; @@ -553,6 +747,8 @@ inner.className = 'git-diff-peek-inner'; root.appendChild(inner); + + // Old (HEAD) lines — red background, shown for deletions and modifications if (change.oldLines.length > 0) { const body = document.createElement('div'); @@ -691,7 +887,8 @@ function buildPeekOverlayHeader( change: GutterChange, index: number, - total: number + total: number, + isAi = false ): HTMLElement { const root = document.createElement('div'); root.className = `git-diff-peek-overlay-header git-diff-peek-overlay-header-${change.type}`; @@ -725,14 +922,16 @@ attachPeekButton(nextBtn, () => navigatePeek(1)); actions.appendChild(nextBtn); - const discardBtn = document.createElement('button'); - discardBtn.className = 'git-diff-peek-discard-btn'; - discardBtn.type = 'button'; - discardBtn.title = 'Discard this change (revert to HEAD)'; - discardBtn.setAttribute('aria-label', 'Discard this change'); - discardBtn.innerHTML = `${ICON_DISCARD}Discard`; - attachPeekButton(discardBtn, () => discardHunk(change)); - actions.appendChild(discardBtn); + if (!isAi) { + const discardBtn = document.createElement('button'); + discardBtn.className = 'git-diff-peek-discard-btn'; + discardBtn.type = 'button'; + discardBtn.title = 'Discard this change (revert to HEAD)'; + discardBtn.setAttribute('aria-label', 'Discard this change'); + discardBtn.innerHTML = `${ICON_DISCARD}Discard`; + attachPeekButton(discardBtn, () => discardHunk(change)); + actions.appendChild(discardBtn); + } const closeBtn = document.createElement('button'); closeBtn.className = 'git-diff-peek-iconbtn git-diff-peek-close'; @@ -748,18 +947,20 @@ } function navigatePeek(direction: 1 | -1) { - if (!activeDiffZone || gutterChanges.length === 0) return; + if (!activeDiffZone) return; + const changes = getActiveChanges(activeDiffZone.isAi); + if (changes.length === 0) return; const currentLine = activeDiffZone.line; - const currentIdx = gutterChanges.findIndex( + const currentIdx = changes.findIndex( (c) => c.startLine === currentLine ); if (currentIdx === -1) return; const nextIdx = - (currentIdx + direction + gutterChanges.length) % gutterChanges.length; - const next = gutterChanges[nextIdx]; + (currentIdx + direction + changes.length) % changes.length; + const next = changes[nextIdx]; const editor = monacoEditorRef?.getEditor(); if (editor) editor.revealLineInCenter(next.startLine); - showDiffPeek(next); + showDiffPeek(next, activeDiffZone.isAi); } function applyPeekSizing(editorInstance: editor.IStandaloneCodeEditor, domNode: HTMLElement) { @@ -783,17 +984,19 @@ domNode.style.transform = `translateX(${scrollLeft}px)`; } - function showDiffPeek(change: GutterChange) { + function showDiffPeek(change: GutterChange, isAi = false) { const editorInstance = monacoEditorRef?.getEditor(); if (!editorInstance) return; closeDiffPeek(); - const index = gutterChanges.indexOf(change) + 1; - const total = gutterChanges.length; - const domNode = buildPeekDom(change); + const changes = getActiveChanges(isAi); + const foundIdx = changes.findIndex(c => c.startLine === change.startLine && c.type === change.type); + const index = foundIdx >= 0 ? foundIdx + 1 : 1; + const total = changes.length; + const domNode = buildPeekDom(change, isAi); const marginDomNode = buildPeekMargin(change); - const overlayHeader = buildPeekOverlayHeader(change, index, total); + const overlayHeader = buildPeekOverlayHeader(change, index, total, isAi); applyPeekSizing(editorInstance, domNode); applyPeekScroll(domNode, editorInstance.getScrollLeft()); @@ -921,6 +1124,7 @@ activeDiffZone = { id: zoneId, line: change.startLine, + isAi, escHandler, domNode, overlayWidget, @@ -930,6 +1134,26 @@ }; } + function getActiveChanges(isAi: boolean): GutterChange[] { + return isAi ? aiGutterChanges : gutterChanges; + } + + function refreshActiveDiffPeek() { + if (!activeDiffZone) return; + const isAi = activeDiffZone.isAi; + const changes = getActiveChanges(isAi); + if (changes.length === 0) { + closeDiffPeek(); + return; + } + const targetChange = changes[0]; + showDiffPeek(targetChange, isAi); + const editor = monacoEditorRef?.getEditor(); + if (editor) { + editor.revealLineInCenterIfOutsideViewport(targetChange.startLine); + } + } + function closeDiffPeek() { if (!activeDiffZone) return; const editorInstance = monacoEditorRef?.getEditor(); @@ -954,12 +1178,13 @@ if (e.target.type !== GUTTER_LINE_DECORATIONS) return; const line = e.target.position?.lineNumber; if (!line) return; - const change = findChangeAtLine(line); - if (!change) return; + const found = findChangeAtLine(line); + if (!found) return; + const { change, isAi } = found; if (activeDiffZone && activeDiffZone.line === change.startLine) { closeDiffPeek(); } else { - showDiffPeek(change); + showDiffPeek(change, isAi); } }); gutterClickDispose = () => disposable.dispose(); @@ -1003,6 +1228,7 @@ // Reset decoration ids — the prior editor instance is gone so its ids // are no longer valid. gutterDecorations = []; + aiChangeDecorations = []; // Previous editor's view zones are gone with the disposed instance if (activeDiffZone) { @@ -1044,6 +1270,7 @@ } scheduleGutterUpdate(); + applyAiChangeDecorations(); } // Handle content changes from editor @@ -1051,16 +1278,27 @@ hasChanges = newContent !== referenceContent; onContentChange?.(newContent); // User edits invalidate the captured HEAD-side hunk in the peek; close it - // so the next click reflects the up-to-date diff. if (activeDiffZone) closeDiffPeek(); scheduleGutterUpdate(); + // Clear AI change highlights when user starts editing + if (file?.path) { + clearAiChange(file.path); + const editor = monacoEditorRef?.getEditor(); + if (editor) aiChangeDecorations = editor.deltaDecorations(aiChangeDecorations, []); + } } - // Expose scroll position to parent (FilesPanel snapshots it on tab switches) export function getEditorScrollTop(): number { return monacoEditorRef?.getScrollTop?.() ?? 0; } + export function resetRevealFilter() { + activeRevealEditIdx = null; + lastRevealEditIdx = null; + setGutterViewMode('git'); + closeDiffPeek(); + } + // Update Monaco word wrap when prop changes // Read wordWrap BEFORE the if-check so it's always tracked by $effect $effect(() => { @@ -1394,6 +1632,50 @@ {/if} + + {#if hasAiChanges} + + {/if} + + {#if gutterMode === 'ai' && lastRevealEditIdx !== null} + {#if activeRevealEditIdx !== null} + + {:else} + + {/if} + {/if}