Improve file tree, fix copying to clipboard
10 files changed,  +513, -58
M src/lib/components/DiffView.svelte
+8, -0
 1@@ -3,6 +3,7 @@
 2 	import type { ReviewThread } from '$lib/types/index.ts';
 3 	import { viewMode, commentingLine } from '$lib/stores/ui.ts';
 4 	import { threadsByFile } from '$lib/stores/review.ts';
 5+	import { resetHunk } from '$lib/stores/files.ts';
 6 	import CommentEditor from './CommentEditor.svelte';
 7 	import CommentThread from './CommentThread.svelte';
 8 
 9@@ -169,6 +170,13 @@
10 					</button>
11 				{/if}
12 				<span class="flex-1 px-2 py-1">{hunk.header}</span>
13+				<button
14+					class="text-accent-red/60 hover:text-accent-red hover:bg-hover transition-colors px-2 py-1 text-xs"
15+					onclick={() => resetHunk(file.path, hunk.header)}
16+					title="Reset this change block"
17+				>
18+					↩ Reset
19+				</button>
20 			</div>
21 		{/if}
22 
M src/lib/components/ExportDialog.svelte
+71, -11
  1@@ -4,13 +4,40 @@
  2 
  3 	let exporting = $state(false);
  4 	let result = $state<{ success: boolean; path?: string; message?: string; text?: string } | null>(null);
  5+	let clipboardState = $state<'idle' | 'loading' | 'ready' | 'copied'>('idle');
  6+	let pendingClipboardText = $state<string | null>(null);
  7 
  8 	async function exportAs(format: string) {
  9+		if (format === 'clipboard') {
 10+			clipboardState = 'loading';
 11+			pendingClipboardText = null;
 12+			try {
 13+				const res = await fetch('/api/export', {
 14+					method: 'POST',
 15+					headers: { 'Content-Type': 'application/json' },
 16+					body: JSON.stringify({ format })
 17+				});
 18+				const data = await res.json();
 19+				if (data?.text) {
 20+					pendingClipboardText = data.text;
 21+					// Ensure "Preparing..." is visible long enough for user to notice
 22+					await new Promise((r) => setTimeout(r, 800));
 23+					clipboardState = 'ready';
 24+				} else {
 25+					result = { success: false, message: 'No content to copy.' };
 26+					clipboardState = 'idle';
 27+				}
 28+			} catch (err) {
 29+				result = { success: false, message: String(err) };
 30+				clipboardState = 'idle';
 31+			}
 32+			return;
 33+		}
 34+
 35 		exporting = true;
 36 		result = null;
 37 		try {
 38 			if (format === 'claude') {
 39-				// Use terminal panel with streaming
 40 				const res = await sendReviewToClaude();
 41 				if (res.success) {
 42 					$showExportDialog = false;
 43@@ -26,11 +53,6 @@
 44 				body: JSON.stringify({ format })
 45 			});
 46 			result = await res.json();
 47-
 48-			if (format === 'clipboard' && result?.text) {
 49-				await navigator.clipboard.writeText(result.text);
 50-				result = { success: true, message: 'Copied to clipboard!' };
 51-			}
 52 		} catch (err) {
 53 			result = { success: false, message: String(err) };
 54 		} finally {
 55@@ -38,6 +60,28 @@
 56 		}
 57 	}
 58 
 59+	function copyNow() {
 60+		if (!pendingClipboardText) return;
 61+		navigator.clipboard.writeText(pendingClipboardText).then(() => {
 62+			clipboardState = 'copied';
 63+			pendingClipboardText = null;
 64+			setTimeout(() => { clipboardState = 'idle'; }, 2000);
 65+		}).catch(() => {
 66+			const textarea = document.createElement('textarea');
 67+			textarea.value = pendingClipboardText!;
 68+			textarea.style.position = 'fixed';
 69+			textarea.style.left = '-9999px';
 70+			document.body.appendChild(textarea);
 71+			textarea.focus();
 72+			textarea.select();
 73+			document.execCommand('copy');
 74+			document.body.removeChild(textarea);
 75+			clipboardState = 'copied';
 76+			pendingClipboardText = null;
 77+			setTimeout(() => { clipboardState = 'idle'; }, 2000);
 78+		});
 79+	}
 80+
 81 	function close() {
 82 		$showExportDialog = false;
 83 		result = null;
 84@@ -67,12 +111,28 @@
 85 				</button>
 86 
 87 				<button
 88-					class="w-full text-left px-4 py-3 rounded-lg border border-border-strong hover:bg-hover/50 transition-colors group"
 89-					onclick={() => exportAs('clipboard')}
 90-					disabled={exporting}
 91+					class="w-full text-left px-4 py-3 rounded-lg border transition-all {clipboardState === 'ready' ? 'border-accent-blue bg-accent-blue/10 ring-1 ring-accent-blue/30' : clipboardState === 'copied' ? 'border-accent-green bg-accent-green/10' : 'border-border-strong hover:bg-hover/50'} group"
 92+					onclick={() => clipboardState === 'ready' ? copyNow() : exportAs('clipboard')}
 93+					disabled={exporting || clipboardState === 'loading'}
 94 				>
 95-					<div class="text-sm font-medium text-primary group-hover:opacity-80">Copy to Clipboard</div>
 96-					<div class="text-xs text-tertiary mt-0.5">Copy structured review text for any AI tool</div>
 97+					{#if clipboardState === 'loading'}
 98+						<div class="text-sm font-medium text-primary flex items-center gap-2">
 99+							<span class="animate-spin">⏳</span> Preparing content...
100+						</div>
101+						<div class="text-xs text-tertiary mt-0.5">Generating review text</div>
102+					{:else if clipboardState === 'ready'}
103+						<div class="text-sm font-medium text-accent-blue flex items-center gap-2">
104+							📋 Content ready — click to copy
105+						</div>
106+						<div class="text-xs text-accent-blue/70 mt-0.5">Click now to copy to clipboard</div>
107+					{:else if clipboardState === 'copied'}
108+						<div class="text-sm font-medium text-accent-green flex items-center gap-2">
109+							✓ Copied to clipboard!
110+						</div>
111+					{:else}
112+						<div class="text-sm font-medium text-primary group-hover:opacity-80">Copy to Clipboard</div>
113+						<div class="text-xs text-tertiary mt-0.5">Copy structured review text for any AI tool</div>
114+					{/if}
115 				</button>
116 
117 				<button
M src/lib/components/FileTree.svelte
+238, -41
  1@@ -1,5 +1,5 @@
  2 <script lang="ts">
  3-	import { files, selectedFile, fileCommentCounts, toggleApproval } from '$lib/stores/files.ts';
  4+	import { files, selectedFile, fileCommentCounts, toggleApproval, resetFile, contextFiles, toggleContextFile } from '$lib/stores/files.ts';
  5 
  6 	const statusColors: Record<string, string> = {
  7 		added: 'text-accent-green',
  8@@ -15,6 +15,50 @@
  9 		renamed: 'R'
 10 	};
 11 
 12+	let contextMenu = $state<{ x: number; y: number; path: string; status: string } | null>(null);
 13+	let confirmingReset = $state<string | null>(null);
 14+	let contextFileInput = $state('');
 15+	let showContextInput = $state(false);
 16+	let collapsedDirs = $state<Set<string>>(new Set());
 17+
 18+	interface FileGroup {
 19+		dir: string;
 20+		files: typeof $files;
 21+	}
 22+
 23+	let groupedFiles = $derived.by(() => {
 24+		const groups = new Map<string, typeof $files>();
 25+		for (const file of $files) {
 26+			const dir = getDirectory(file.path) || '.';
 27+			if (!groups.has(dir)) groups.set(dir, []);
 28+			groups.get(dir)!.push(file);
 29+		}
 30+		// Sort groups by directory name
 31+		const sorted: FileGroup[] = [];
 32+		for (const [dir, files] of groups) {
 33+			sorted.push({ dir, files });
 34+		}
 35+		sorted.sort((a, b) => a.dir.localeCompare(b.dir));
 36+		return sorted;
 37+	});
 38+
 39+	function toggleDir(dir: string) {
 40+		const next = new Set(collapsedDirs);
 41+		if (next.has(dir)) {
 42+			next.delete(dir);
 43+		} else {
 44+			next.add(dir);
 45+		}
 46+		collapsedDirs = next;
 47+	}
 48+
 49+	async function addContextFromInput() {
 50+		if (!contextFileInput.trim()) return;
 51+		await toggleContextFile(contextFileInput.trim());
 52+		contextFileInput = '';
 53+		showContextInput = false;
 54+	}
 55+
 56 	function selectFile(path: string) {
 57 		$selectedFile = path;
 58 	}
 59@@ -33,6 +77,28 @@
 60 		e.stopPropagation();
 61 		toggleApproval(path);
 62 	}
 63+
 64+	function handleContextMenu(e: MouseEvent, path: string, status: string) {
 65+		e.preventDefault();
 66+		e.stopPropagation();
 67+		contextMenu = { x: e.clientX, y: e.clientY, path, status };
 68+	}
 69+
 70+	function closeContextMenu() {
 71+		contextMenu = null;
 72+	}
 73+
 74+	function handleReset(path: string) {
 75+		contextMenu = null;
 76+		confirmingReset = path;
 77+	}
 78+
 79+	async function confirmReset() {
 80+		if (confirmingReset) {
 81+			await resetFile(confirmingReset);
 82+			confirmingReset = null;
 83+		}
 84+	}
 85 </script>
 86 
 87 <div class="h-full overflow-y-auto">
 88@@ -40,49 +106,180 @@
 89 		Changed Files ({$files.length})
 90 	</div>
 91 	<div class="space-y-px">
 92-		{#each $files as file}
 93-			{@const isSelected = $selectedFile === file.path}
 94-			{@const commentCount = $fileCommentCounts[file.path] || 0}
 95-			<!-- svelte-ignore a11y_click_events_have_key_events -->
 96-			<!-- svelte-ignore a11y_no_static_element_interactions -->
 97-			<div
 98-				class="w-full text-left px-3 py-1.5 flex items-center gap-2 text-sm hover:bg-hover/50 transition-colors cursor-pointer {isSelected ? 'bg-hover/80 text-primary' : 'text-secondary'} {file.approved ? 'opacity-60' : ''}"
 99-				onclick={() => selectFile(file.path)}
100-			>
101-				<!-- Approve checkbox -->
102-				<button
103-					class="shrink-0 w-4 h-4 rounded border flex items-center justify-center transition-colors {file.approved ? 'bg-green-600 border-green-600 text-white' : 'border-border-strong hover:border-accent-green text-transparent hover:text-accent-green'}"
104-					onclick={(e) => handleApprove(e, file.path)}
105-					title={file.approved ? 'Unapprove file' : 'Approve file'}
106+		{#each groupedFiles as group}
107+			{@const isCollapsed = collapsedDirs.has(group.dir)}
108+			{@const dirFileCount = group.files.length}
109+			<!-- Directory header -->
110+			{#if groupedFiles.length > 1}
111+				<!-- svelte-ignore a11y_click_events_have_key_events -->
112+				<!-- svelte-ignore a11y_no_static_element_interactions -->
113+				<div
114+					class="flex items-center gap-1 px-2 py-1 text-xs text-muted hover:text-secondary cursor-pointer hover:bg-hover/30 transition-colors select-none"
115+					onclick={() => toggleDir(group.dir)}
116 				>
117-					<svg class="w-3 h-3" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="3">
118-						<path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7" />
119-					</svg>
120-				</button>
121+					<span class="w-4 text-center transition-transform {isCollapsed ? '' : 'rotate-90'}">▸</span>
122+					<span class="truncate font-mono">{group.dir}</span>
123+					<span class="text-muted/60 ml-1">({dirFileCount})</span>
124+				</div>
125+			{/if}
126 
127-				<span class="font-mono text-xs font-bold w-4 text-center {statusColors[file.status]}">
128-					{statusLabels[file.status]}
129-				</span>
130-				<span class="truncate flex-1 {file.approved ? 'line-through decoration-muted' : ''}">
131-					{#if getDirectory(file.path)}
132-						<span class="text-muted">{getDirectory(file.path)}/</span>
133-					{/if}
134-					<span class="font-medium">{getFilename(file.path)}</span>
135-				</span>
136-				<span class="text-xs text-muted tabular-nums shrink-0">
137-					{#if file.additions > 0}
138-						<span class="text-accent-green">+{file.additions}</span>
139-					{/if}
140-					{#if file.deletions > 0}
141-						<span class="text-accent-red">-{file.deletions}</span>
142-					{/if}
143-				</span>
144-				{#if commentCount > 0}
145-					<span class="bg-accent-blue/20 text-accent-blue text-xs px-1.5 py-0.5 rounded-full font-medium">
146-						{commentCount}
147-					</span>
148-				{/if}
149+			<!-- Files in this directory -->
150+			{#if !isCollapsed}
151+				{#each group.files as file}
152+					{@const isSelected = $selectedFile === file.path}
153+					{@const commentCount = $fileCommentCounts[file.path] || 0}
154+					<!-- svelte-ignore a11y_click_events_have_key_events -->
155+					<!-- svelte-ignore a11y_no_static_element_interactions -->
156+					<div
157+						class="w-full text-left py-1.5 flex items-center gap-2 text-sm hover:bg-hover/50 transition-colors cursor-pointer group {isSelected ? 'bg-hover/80 text-primary' : 'text-secondary'} {file.approved ? 'opacity-60' : ''} {groupedFiles.length > 1 ? 'pl-7 pr-3' : 'px-3'}"
158+						onclick={() => selectFile(file.path)}
159+						oncontextmenu={(e) => handleContextMenu(e, file.path, file.status)}
160+					>
161+						<!-- Approve checkbox -->
162+						<button
163+							class="shrink-0 w-4 h-4 rounded border flex items-center justify-center transition-colors {file.approved ? 'bg-green-600 border-green-600 text-white' : 'border-border-strong hover:border-accent-green text-transparent hover:text-accent-green'}"
164+							onclick={(e) => handleApprove(e, file.path)}
165+							title={file.approved ? 'Unapprove file' : 'Approve file'}
166+						>
167+							<svg class="w-3 h-3" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="3">
168+								<path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7" />
169+							</svg>
170+						</button>
171+
172+						<span class="font-mono text-xs font-bold w-4 text-center {statusColors[file.status]}">
173+							{statusLabels[file.status]}
174+						</span>
175+						<span class="truncate flex-1 {file.approved ? 'line-through decoration-muted' : ''}">
176+							<span class="font-medium">{getFilename(file.path)}</span>
177+						</span>
178+						<span class="text-xs text-muted tabular-nums shrink-0">
179+							{#if file.additions > 0}
180+								<span class="text-accent-green">+{file.additions}</span>
181+							{/if}
182+							{#if file.deletions > 0}
183+								<span class="text-accent-red">-{file.deletions}</span>
184+							{/if}
185+						</span>
186+						{#if commentCount > 0}
187+							<span class="bg-accent-blue/20 text-accent-blue text-xs px-1.5 py-0.5 rounded-full font-medium">
188+								{commentCount}
189+							</span>
190+						{/if}
191+						<!-- More actions button -->
192+						<button
193+							class="shrink-0 opacity-0 group-hover:opacity-100 text-muted hover:text-primary transition-all text-xs px-0.5"
194+							onclick={(e) => { e.stopPropagation(); handleContextMenu(e, file.path, file.status); }}
195+							title="More actions"
196+						>
197+							⋯
198+						</button>
199+					</div>
200+				{/each}
201+			{/if}
202+		{/each}
203+	</div>
204+</div>
205+
206+<!-- Context files section -->
207+{#if $contextFiles.length > 0 || showContextInput}
208+	<div class="px-3 py-2 text-xs font-semibold text-tertiary uppercase tracking-wider border-t border-border mt-2">
209+		Context Files ({$contextFiles.length})
210+	</div>
211+	<div class="space-y-px px-1">
212+		{#each $contextFiles as ctxFile}
213+			<div class="flex items-center gap-2 px-2 py-1 text-xs text-secondary">
214+				<span class="text-muted">📎</span>
215+				<span class="truncate flex-1 font-mono">{ctxFile}</span>
216+				<button
217+					class="text-muted hover:text-accent-red transition-colors shrink-0"
218+					onclick={() => toggleContextFile(ctxFile)}
219+					title="Remove from context"
220+				>✗</button>
221 			</div>
222 		{/each}
223 	</div>
224+{/if}
225+{#if showContextInput}
226+	<div class="px-3 py-2">
227+		<input
228+			type="text"
229+			class="w-full bg-surface text-primary border border-border-strong rounded px-2 py-1 text-xs focus:outline-none focus:border-accent-blue placeholder:text-muted"
230+			placeholder="path/to/file.ts"
231+			bind:value={contextFileInput}
232+			onkeydown={(e) => { if (e.key === 'Enter') addContextFromInput(); if (e.key === 'Escape') showContextInput = false; }}
233+		/>
234+	</div>
235+{/if}
236+<div class="px-3 py-1">
237+	<button
238+		class="text-xs text-muted hover:text-secondary transition-colors"
239+		onclick={() => (showContextInput = !showContextInput)}
240+	>
241+		+ Add context file
242+	</button>
243 </div>
244+
245+<!-- Context menu -->
246+{#if contextMenu}
247+	<!-- svelte-ignore a11y_click_events_have_key_events -->
248+	<!-- svelte-ignore a11y_no_static_element_interactions -->
249+	<div class="fixed inset-0 z-50" onclick={closeContextMenu}></div>
250+	<div
251+		class="fixed z-50 bg-panel border border-border-strong rounded-lg shadow-xl py-1 min-w-[180px]"
252+		style="left: {contextMenu.x}px; top: {contextMenu.y}px;"
253+	>
254+		<button
255+			class="w-full text-left px-3 py-1.5 text-sm text-secondary hover:bg-hover transition-colors"
256+			onclick={() => { toggleApproval(contextMenu!.path); closeContextMenu(); }}
257+		>
258+			{$files.find(f => f.path === contextMenu!.path)?.approved ? '✗ Unapprove' : '✓ Approve'}
259+		</button>
260+		<button
261+			class="w-full text-left px-3 py-1.5 text-sm text-secondary hover:bg-hover transition-colors"
262+			onclick={() => { toggleContextFile(contextMenu!.path); closeContextMenu(); }}
263+		>
264+			{$contextFiles.includes(contextMenu!.path) ? '✗ Remove from context' : '📎 Add as context'}
265+		</button>
266+		<div class="border-t border-border my-1"></div>
267+		<button
268+			class="w-full text-left px-3 py-1.5 text-sm text-accent-red hover:bg-hover transition-colors"
269+			onclick={() => handleReset(contextMenu!.path)}
270+		>
271+			{contextMenu.status === 'added' ? '🗑 Delete file' : '↩ Reset file'}
272+		</button>
273+	</div>
274+{/if}
275+
276+<!-- Reset confirmation dialog -->
277+{#if confirmingReset}
278+	<!-- svelte-ignore a11y_click_events_have_key_events -->
279+	<!-- svelte-ignore a11y_no_static_element_interactions -->
280+	<div class="fixed inset-0 z-50 bg-black/50 flex items-center justify-center" onclick={() => (confirmingReset = null)}>
281+		<!-- svelte-ignore a11y_click_events_have_key_events -->
282+		<!-- svelte-ignore a11y_no_static_element_interactions -->
283+		<div class="bg-panel border border-border-strong rounded-lg p-4 max-w-md shadow-xl" onclick={(e) => e.stopPropagation()}>
284+			<h3 class="text-primary font-medium mb-2">Reset file?</h3>
285+			<p class="text-sm text-secondary mb-1">
286+				{$files.find(f => f.path === confirmingReset)?.status === 'added'
287+					? 'This will delete the file:'
288+					: 'This will discard all changes in:'}
289+			</p>
290+			<p class="text-sm text-primary font-mono mb-4">{confirmingReset}</p>
291+			<p class="text-xs text-accent-red mb-4">This action cannot be undone.</p>
292+			<div class="flex justify-end gap-2">
293+				<button
294+					class="text-sm px-3 py-1.5 rounded text-secondary hover:text-primary hover:bg-hover transition-colors"
295+					onclick={() => (confirmingReset = null)}
296+				>
297+					Cancel
298+				</button>
299+				<button
300+					class="text-sm px-3 py-1.5 rounded bg-red-600 text-white hover:bg-red-500 transition-colors"
301+					onclick={confirmReset}
302+				>
303+					{$files.find(f => f.path === confirmingReset)?.status === 'added' ? 'Delete' : 'Reset'}
304+				</button>
305+			</div>
306+		</div>
307+	</div>
308+{/if}
M src/lib/server/export.ts
+20, -3
 1@@ -2,8 +2,9 @@ import { writeFile } from 'fs/promises';
 2 import { join } from 'path';
 3 import { spawn } from 'child_process';
 4 import type { ReviewExport, ReviewExportFile, ReviewExportThread } from '$lib/types/index.ts';
 5+import { readFile } from 'fs/promises';
 6 import { getRepoDir, getRepoInfo, getStatus, getFileContent } from './git.ts';
 7-import { getReview, getThreads } from './review-store.ts';
 8+import { getReview, getThreads, getContextFiles } from './review-store.ts';
 9 
10 export async function buildExport(): Promise<ReviewExport> {
11 	const { branch, repoName } = await getRepoInfo();
12@@ -126,7 +127,7 @@ export async function exportToClaude(): Promise<{ success: boolean; message: str
13 		return { success: false, message: 'No review comments to send. Add comments first.' };
14 	}
15 
16-	const prompt = buildClaudePrompt(data);
17+	const prompt = await buildClaudePrompt(data);
18 	const claudePath = findClaude();
19 
20 	return new Promise((resolve) => {
21@@ -168,7 +169,7 @@ export async function exportToClaude(): Promise<{ success: boolean; message: str
22 	});
23 }
24 
25-export function buildClaudePrompt(data: ReviewExport): string {
26+export async function buildClaudePrompt(data: ReviewExport): Promise<string> {
27 	const lines: string[] = [
28 		'I have completed a code review of the current changes. Please address each review comment below by making the necessary changes to the codebase.',
29 		''
30@@ -192,6 +193,22 @@ export function buildClaudePrompt(data: ReviewExport): string {
31 		}
32 	}
33 
34+	// Include context files for additional reference
35+	const contextFilePaths = getContextFiles();
36+	if (contextFilePaths.length > 0) {
37+		lines.push('', '--- Additional Context Files ---', '');
38+		lines.push('The following files are provided as additional context for your reference when making changes:', '');
39+		const repoDir = getRepoDir();
40+		for (const filePath of contextFilePaths) {
41+			try {
42+				const content = await readFile(join(repoDir, filePath), 'utf-8');
43+				lines.push(`File: ${filePath}`, '```', content, '```', '');
44+			} catch {
45+				lines.push(`File: ${filePath} (could not read)`, '');
46+			}
47+		}
48+	}
49+
50 	return lines.join('\n');
51 }
52 
M src/lib/server/git.ts
+47, -1
 1@@ -45,11 +45,12 @@ export async function getStatus(): Promise<FileChange[]> {
 2 	for (const f of status.renamed) {
 3 		addFile(f.to, 'renamed', false, f.from);
 4 	}
 5-	// Mark staged files
 6+	// Mark staged files as both staged and approved
 7 	for (const f of status.staged) {
 8 		const existing = files.find((x) => x.path === f);
 9 		if (existing) {
10 			existing.staged = true;
11+			existing.approved = true;
12 		}
13 	}
14 
15@@ -343,6 +344,51 @@ export async function stageAll(): Promise<void> {
16 	await g.add('-A');
17 }
18 
19+export async function resetFile(filePath: string): Promise<void> {
20+	const g = getGit();
21+	const status = await g.status();
22+
23+	// Check if it's an untracked/new file
24+	if (status.not_added.includes(filePath) || status.created.includes(filePath)) {
25+		// Remove the file
26+		const { unlink } = await import('fs/promises');
27+		const { join } = await import('path');
28+		await unlink(join(getRepoDir(), filePath));
29+		// If it was staged, also remove from index
30+		if (status.created.includes(filePath)) {
31+			await g.raw(['rm', '--cached', filePath]);
32+		}
33+	} else {
34+		// Modified or deleted file: restore to HEAD version
35+		await g.checkout(['HEAD', '--', filePath]);
36+	}
37+}
38+
39+export async function resetHunk(filePath: string, hunkHeader: string): Promise<void> {
40+	const g = getGit();
41+	// Get the full diff for this file, then construct a reverse patch for just this hunk
42+	const diff = await g.diff(['HEAD', '--', filePath]);
43+	const hunks = diff.split(/(?=^@@)/m);
44+	const fileHeader = hunks[0] || '';
45+
46+	// Find the matching hunk
47+	const targetHunk = hunks.find((h) => h.startsWith(hunkHeader));
48+	if (!targetHunk) return;
49+
50+	// Build a patch with just this hunk and apply in reverse
51+	const patch = fileHeader + targetHunk;
52+	const { writeFile, unlink } = await import('fs/promises');
53+	const { join } = await import('path');
54+	const { tmpdir } = await import('os');
55+	const patchFile = join(tmpdir(), `lcr-patch-${Date.now()}.patch`);
56+	await writeFile(patchFile, patch);
57+	try {
58+		await g.raw(['apply', '--reverse', patchFile]);
59+	} finally {
60+		await unlink(patchFile).catch(() => {});
61+	}
62+}
63+
64 export async function commitStaged(message: string): Promise<string> {
65 	const g = getGit();
66 	const result = await g.commit(message);
M src/lib/server/review-store.ts
+14, -0
 1@@ -4,6 +4,19 @@ import { randomUUID } from 'crypto';
 2 const threads = new Map<string, ReviewThread>();
 3 let reviewStatus: 'pending' | 'approved' | 'changes_requested' = 'pending';
 4 let reviewSummary: string | undefined;
 5+const contextFiles = new Set<string>();
 6+
 7+export function addContextFile(filePath: string): void {
 8+	contextFiles.add(filePath);
 9+}
10+
11+export function removeContextFile(filePath: string): void {
12+	contextFiles.delete(filePath);
13+}
14+
15+export function getContextFiles(): string[] {
16+	return Array.from(contextFiles);
17+}
18 
19 export function addComment(
20 	filePath: string,
21@@ -101,6 +114,7 @@ export function clearAll(): void {
22 	threads.clear();
23 	reviewStatus = 'pending';
24 	reviewSummary = undefined;
25+	contextFiles.clear();
26 }
27 
28 export function markOutdated(threadId: string, outdated: boolean): boolean {
M src/lib/stores/files.ts
+54, -0
 1@@ -16,6 +16,26 @@ export const selectedDiff = derived([diffFiles, selectedFile], ([$diffFiles, $se
 2 });
 3 
 4 export const fileCommentCounts = writable<Record<string, number>>({});
 5+export const contextFiles = writable<string[]>([]);
 6+
 7+export async function loadContextFiles() {
 8+	const res = await fetch('/api/context-files');
 9+	const data = await res.json();
10+	if (data.files) contextFiles.set(data.files);
11+}
12+
13+export async function toggleContextFile(path: string) {
14+	let current: string[] = [];
15+	contextFiles.subscribe((v) => (current = v))();
16+	const isContext = current.includes(path);
17+	const res = await fetch('/api/context-files', {
18+		method: 'POST',
19+		headers: { 'Content-Type': 'application/json' },
20+		body: JSON.stringify({ path, action: isContext ? 'remove' : 'add' })
21+	});
22+	const data = await res.json();
23+	if (data.files) contextFiles.set(data.files);
24+}
25 
26 export const approvedCount = derived(files, ($files) => {
27 	const approved = $files.filter((f) => f.approved).length;
28@@ -104,6 +124,40 @@ export async function unapproveFile(path: string) {
29 	}
30 }
31 
32+export async function resetFile(path: string) {
33+	const res = await fetch('/api/files', {
34+		method: 'POST',
35+		headers: { 'Content-Type': 'application/json' },
36+		body: JSON.stringify({ path, action: 'reset' })
37+	});
38+	const data = await res.json();
39+	if (Array.isArray(data)) {
40+		files.set(data);
41+		await loadAllDiffs();
42+		// If the reset file was selected and is now gone, select another
43+		const currentSelected = await new Promise<string | null>((resolve) => {
44+			selectedFile.subscribe((v) => resolve(v))();
45+		});
46+		if (currentSelected === path) {
47+			const remaining = data as FileChange[];
48+			selectedFile.set(remaining.length > 0 ? remaining[0].path : null);
49+		}
50+	}
51+}
52+
53+export async function resetHunk(path: string, hunkHeader: string) {
54+	const res = await fetch('/api/files', {
55+		method: 'POST',
56+		headers: { 'Content-Type': 'application/json' },
57+		body: JSON.stringify({ path, action: 'reset-hunk', hunkHeader })
58+	});
59+	const data = await res.json();
60+	if (Array.isArray(data)) {
61+		files.set(data);
62+		await loadAllDiffs();
63+	}
64+}
65+
66 export async function toggleApproval(path: string) {
67 	const currentFiles = await new Promise<FileChange[]>((resolve) => {
68 		files.subscribe((v) => resolve(v))();
M src/routes/+page.svelte
+30, -1
 1@@ -13,6 +13,29 @@
 2 
 3 	let loading = $state(true);
 4 	let error = $state<string | null>(null);
 5+	let sidebarWidth = $state(288); // 18rem default
 6+	let isResizingSidebar = $state(false);
 7+
 8+	function startSidebarResize(e: MouseEvent) {
 9+		e.preventDefault();
10+		isResizingSidebar = true;
11+		const startX = e.clientX;
12+		const startWidth = sidebarWidth;
13+
14+		function onMouseMove(e: MouseEvent) {
15+			const newWidth = startWidth + (e.clientX - startX);
16+			sidebarWidth = Math.max(200, Math.min(600, newWidth));
17+		}
18+
19+		function onMouseUp() {
20+			isResizingSidebar = false;
21+			window.removeEventListener('mousemove', onMouseMove);
22+			window.removeEventListener('mouseup', onMouseUp);
23+		}
24+
25+		window.addEventListener('mousemove', onMouseMove);
26+		window.addEventListener('mouseup', onMouseUp);
27+	}
28 
29 	onMount(async () => {
30 		try {
31@@ -59,8 +82,14 @@
32 	<div class="flex-1 flex overflow-hidden">
33 		<!-- Sidebar -->
34 		{#if $sidebarOpen}
35-			<aside class="w-72 shrink-0 bg-panel/50 border-r border-border overflow-hidden flex flex-col">
36+			<aside class="shrink-0 bg-panel/50 border-r border-border overflow-hidden flex flex-col relative" style="width: {sidebarWidth}px">
37 				<FileTree />
38+				<!-- Resize handle -->
39+				<!-- svelte-ignore a11y_no_static_element_interactions -->
40+				<div
41+					class="absolute top-0 right-0 w-1 h-full cursor-col-resize hover:bg-accent-blue/50 transition-colors {isResizingSidebar ? 'bg-accent-blue/50' : ''}"
42+					onmousedown={startSidebarResize}
43+				></div>
44 			</aside>
45 		{/if}
46 
A src/routes/api/context-files/+server.ts
+24, -0
 1@@ -0,0 +1,24 @@
 2+import { json } from '@sveltejs/kit';
 3+import { addContextFile, removeContextFile, getContextFiles } from '$lib/server/review-store.ts';
 4+import type { RequestHandler } from './$types.ts';
 5+
 6+export const GET: RequestHandler = async () => {
 7+	return json({ files: getContextFiles() });
 8+};
 9+
10+export const POST: RequestHandler = async ({ request }) => {
11+	const body = await request.json();
12+	const { path, action } = body;
13+
14+	if (!path) {
15+		return json({ error: 'Missing path' }, { status: 400 });
16+	}
17+
18+	if (action === 'remove') {
19+		removeContextFile(path);
20+	} else {
21+		addContextFile(path);
22+	}
23+
24+	return json({ files: getContextFiles() });
25+};
M src/routes/api/files/+server.ts
+7, -1
 1@@ -1,5 +1,5 @@
 2 import { json } from '@sveltejs/kit';
 3-import { getStatus, stageFile, unstageFile } from '$lib/server/git.ts';
 4+import { getStatus, stageFile, unstageFile, resetFile, resetHunk } from '$lib/server/git.ts';
 5 import type { RequestHandler } from './$types.ts';
 6 
 7 export const GET: RequestHandler = async () => {
 8@@ -24,6 +24,12 @@ export const POST: RequestHandler = async ({ request }) => {
 9 			await stageFile(path);
10 		} else if (action === 'unapprove') {
11 			await unstageFile(path);
12+		} else if (action === 'reset') {
13+			await resetFile(path);
14+		} else if (action === 'reset-hunk') {
15+			const { hunkHeader } = body;
16+			if (!hunkHeader) return json({ error: 'Missing hunkHeader' }, { status: 400 });
17+			await resetHunk(path, hunkHeader);
18 		} else {
19 			return json({ error: 'Unknown action' }, { status: 400 });
20 		}