Gray out Approve & Commit when no uncommitted changes
CommitDialog now uses uncommittedCount (always from getStatus) instead of approvedCount (which includes worktree-only files). Uncommitted files are fetched lazily when the review menu or commit dialog opens, avoiding a double fetch on every loadFiles() call. Worktree approve/unapprove only touches the in-memory review store, skipping no-op git stage/unstage.
4 files changed,  +48, -15
M src/lib/components/CommitDialog.svelte
+13, -6
 1@@ -1,12 +1,19 @@
 2 <script lang="ts">
 3 	import { showCommitDialog } from '$lib/stores/ui.ts';
 4-	import { approvedCount } from '$lib/stores/files.ts';
 5+	import { uncommittedCount, loadUncommittedFiles } from '$lib/stores/files.ts';
 6 
 7 	let message = $state('');
 8 	let commitMode = $state<'approved' | 'all'>('approved');
 9 	let committing = $state(false);
10 	let result = $state<{ success: boolean; hash?: string; error?: string } | null>(null);
11 
12+	// Refresh uncommitted file counts when dialog opens
13+	$effect(() => {
14+		if ($showCommitDialog) {
15+			loadUncommittedFiles();
16+		}
17+	});
18+
19 	async function handleCommit() {
20 		if (!message.trim()) return;
21 		committing = true;
22@@ -79,19 +86,19 @@
23 								onclick={() => (commitMode = 'approved')}
24 							>
25 								<div class="font-medium">Approved files only</div>
26-								<div class="text-xs mt-0.5 opacity-70">{$approvedCount.approved} of {$approvedCount.total} files</div>
27+								<div class="text-xs mt-0.5 opacity-70">{$uncommittedCount.approved} of {$uncommittedCount.total} files</div>
28 							</button>
29 							<button
30 								class="flex-1 text-left px-3 py-2 rounded-lg border text-sm transition-colors {commitMode === 'all' ? 'border-blue-600 bg-accent-blue/10 text-accent-blue' : 'border-border-strong text-tertiary hover:border-border-strong'}"
31 								onclick={() => (commitMode = 'all')}
32 							>
33 								<div class="font-medium">All changes</div>
34-								<div class="text-xs mt-0.5 opacity-70">{$approvedCount.total} files</div>
35+								<div class="text-xs mt-0.5 opacity-70">{$uncommittedCount.total} files</div>
36 							</button>
37 						</div>
38 					</div>
39 
40-					{#if commitMode === 'approved' && $approvedCount.approved === 0}
41+					{#if commitMode === 'approved' && $uncommittedCount.approved === 0}
42 						<div class="text-sm rounded-lg px-3 py-2 bg-accent-yellow/10 text-accent-yellow">
43 							No files approved yet. Approve files in the sidebar first, or commit all changes.
44 						</div>
45@@ -128,10 +135,10 @@
46 						</button>
47 						<button
48 							class="text-sm px-4 py-2 rounded bg-green-600 text-white hover:bg-green-500 transition-colors disabled:opacity-50"
49-							disabled={!message.trim() || committing || (commitMode === 'approved' && $approvedCount.approved === 0)}
50+							disabled={!message.trim() || committing || (commitMode === 'approved' && $uncommittedCount.approved === 0)}
51 							onclick={handleCommit}
52 						>
53-							{committing ? 'Committing...' : commitMode === 'approved' ? `Commit ${$approvedCount.approved} files` : 'Commit all'}
54+							{committing ? 'Committing...' : commitMode === 'approved' ? `Commit ${$uncommittedCount.approved} files` : 'Commit all'}
55 						</button>
56 					</div>
57 				</div>
M src/lib/components/Toolbar.svelte
+12, -3
 1@@ -1,10 +1,15 @@
 2 <script lang="ts">
 3 	import { viewMode, showCommitDialog, showExportDialog, theme } from '$lib/stores/ui.ts';
 4 	import { stats, reviewStatus, submitReview } from '$lib/stores/review.ts';
 5-	import { diffMode, diffScope, baseBranch, approvedCount, diffStats, loadAllDiffs, setDiffScope } from '$lib/stores/files.ts';
 6+	import { diffMode, diffScope, baseBranch, approvedCount, uncommittedCount, diffStats, loadAllDiffs, setDiffScope, loadUncommittedFiles } from '$lib/stores/files.ts';
 7 
 8 	let showReviewMenu = $state(false);
 9 
10+	function openReviewMenu() {
11+		showReviewMenu = !showReviewMenu;
12+		if (showReviewMenu) loadUncommittedFiles();
13+	}
14+
15 	async function handleApprove() {
16 		await submitReview('approved');
17 		$showCommitDialog = true;
18@@ -131,7 +136,7 @@
19 	<div class="relative">
20 		<button
21 			class="text-xs px-3 py-1.5 rounded bg-green-600 text-white hover:bg-green-500 transition-colors"
22-			onclick={() => (showReviewMenu = !showReviewMenu)}
23+			onclick={openReviewMenu}
24 		>
25 			Review ▾
26 		</button>
27@@ -151,10 +156,14 @@
28 					<span class="text-accent-yellow mr-2">↻</span> Request Changes (send to AI)
29 				</button>
30 				<button
31-					class="w-full text-left px-3 py-2 text-sm text-secondary hover:bg-hover transition-colors"
32+					class="w-full text-left px-3 py-2 text-sm transition-colors {$uncommittedCount.total === 0 ? 'text-muted cursor-not-allowed' : 'text-secondary hover:bg-hover'}"
33+					disabled={$uncommittedCount.total === 0}
34 					onclick={() => { handleApprove(); showReviewMenu = false; }}
35 				>
36 					<span class="text-accent-green mr-2">✓</span> Approve & Commit
37+					{#if $uncommittedCount.total === 0}
38+						<span class="text-xs text-muted ml-1">(no uncommitted changes)</span>
39+					{/if}
40 				</button>
41 			</div>
42 		{/if}
M src/lib/stores/files.ts
+19, -4
 1@@ -5,6 +5,7 @@ import { checkOutdated } from './review.ts';
 2 export type DiffMode = 'full' | 'unstaged';
 3 
 4 export const files = writable<FileChange[]>([]);
 5+export const uncommittedFiles = writable<FileChange[]>([]);
 6 export const selectedFile = writable<string | null>(null);
 7 export const diffFiles = writable<DiffFile[]>([]);
 8 export const loading = writable(false);
 9@@ -44,6 +45,11 @@ export const approvedCount = derived(files, ($files) => {
10 	return { approved, total: $files.length };
11 });
12 
13+export const uncommittedCount = derived(uncommittedFiles, ($uncommittedFiles) => {
14+	const approved = $uncommittedFiles.filter((f) => f.approved).length;
15+	return { approved, total: $uncommittedFiles.length };
16+});
17+
18 export const diffStats = derived(files, ($files) => {
19 	let additions = 0;
20 	let deletions = 0;
21@@ -75,15 +81,18 @@ export async function loadFiles() {
22 	loading.set(true);
23 	try {
24 		const scope = getStoreValue(diffScope);
25-		const url = scope === 'worktree' ? '/api/files?scope=worktree' : '/api/files';
26-		const res = await fetch(url);
27-		const data = await res.json();
28 
29 		if (scope === 'worktree') {
30+			const res = await fetch('/api/files?scope=worktree');
31+			const data = await res.json();
32 			files.set(data.files);
33 			baseBranch.set(data.baseBranch);
34 		} else {
35-			files.set(Array.isArray(data) ? data : data.files || []);
36+			const res = await fetch('/api/files');
37+			const data = await res.json();
38+			const fileList = Array.isArray(data) ? data : data.files || [];
39+			files.set(fileList);
40+			uncommittedFiles.set(fileList);
41 			baseBranch.set(null);
42 		}
43 
44@@ -102,6 +111,12 @@ export async function loadFiles() {
45 	}
46 }
47 
48+export async function loadUncommittedFiles() {
49+	const res = await fetch('/api/files');
50+	const data = await res.json();
51+	uncommittedFiles.set(Array.isArray(data) ? data : []);
52+}
53+
54 export async function loadAllDiffs(mode?: DiffMode) {
55 	const currentMode = mode || getStoreValue(diffMode);
56 	const currentScope = getStoreValue(diffScope);
M src/routes/api/files/+server.ts
+4, -2
 1@@ -36,13 +36,15 @@ export const POST: RequestHandler = async ({ request }) => {
 2 		if (action === 'approve') {
 3 			if (isWorktree) {
 4 				approveWorktreeFile(path);
 5+			} else {
 6+				await stageFile(path);
 7 			}
 8-			await stageFile(path);
 9 		} else if (action === 'unapprove') {
10 			if (isWorktree) {
11 				unapproveWorktreeFile(path);
12+			} else {
13+				await unstageFile(path);
14 			}
15-			await unstageFile(path);
16 		} else if (action === 'reset') {
17 			await resetFile(path);
18 		} else if (action === 'reset-hunk') {