Toolbar.svelte
1<script lang="ts">
2 import { viewMode, showCommitDialog, showExportDialog, theme } from '$lib/stores/ui';
3 import { stats, reviewStatus, submitReview } from '$lib/stores/review';
4 import { diffMode, diffScope, baseBranch, approvedCount, uncommittedCount, diffStats, vcs, loadAllDiffs, setDiffScope, loadUncommittedFiles } from '$lib/stores/files';
5 import { changes, currentChange, approveChange } from '$lib/stores/changes';
6
7 let showReviewMenu = $state(false);
8 let toast = $state<string | null>(null);
9
10 // In jj, approve is available when the viewed change is a pending descendant
11 // of main; in git, when there are uncommitted changes.
12 const canApprove = $derived(
13 $vcs === 'jj'
14 ? $changes.some((c) => c.changeId === $currentChange)
15 : $uncommittedCount.total > 0
16 );
17
18 function openReviewMenu() {
19 showReviewMenu = !showReviewMenu;
20 if (showReviewMenu) loadUncommittedFiles();
21 }
22
23 function showToast(message: string) {
24 toast = message;
25 setTimeout(() => {
26 toast = null;
27 }, 3000);
28 }
29
30 async function handleApprove() {
31 await submitReview('approved');
32 if ($vcs === 'jj') {
33 // Move the `main` bookmark forward to the change being reviewed.
34 try {
35 await approveChange($currentChange ?? undefined);
36 showToast('Approved — main updated');
37 } catch (err) {
38 showToast(String(err));
39 }
40 } else {
41 $showCommitDialog = true;
42 }
43 }
44
45 async function handleRequestChanges() {
46 await submitReview('changes_requested');
47 $showExportDialog = true;
48 }
49
50 async function setDiffMode(mode: 'full' | 'unstaged') {
51 $diffMode = mode;
52 await loadAllDiffs(mode);
53 }
54</script>
55
56<header class="bg-panel border-b border-border px-4 py-2 flex items-center gap-3">
57 <h1 class="text-sm font-semibold text-primary mr-4">Local Code Review</h1>
58
59 <span
60 class="text-[10px] uppercase tracking-wide px-1.5 py-0.5 rounded border {$vcs === 'jj' ? 'border-accent-blue/40 text-accent-blue' : 'border-border-strong text-muted'}"
61 title={$vcs === 'jj' ? 'Jujutsu repository' : 'Git repository'}
62 >
63 {$vcs}
64 </span>
65
66 <!-- View mode toggle -->
67 <div class="flex bg-surface rounded overflow-hidden border border-border">
68 <button
69 class="text-xs px-3 py-1 transition-colors {$viewMode === 'unified' ? 'bg-active text-white' : 'text-tertiary hover:text-primary'}"
70 onclick={() => ($viewMode = 'unified')}
71 >
72 Unified
73 </button>
74 <button
75 class="text-xs px-3 py-1 transition-colors {$viewMode === 'split' ? 'bg-active text-white' : 'text-tertiary hover:text-primary'}"
76 onclick={() => ($viewMode = 'split')}
77 >
78 Split
79 </button>
80 </div>
81
82 <!-- Diff scope toggle -->
83 <div class="flex bg-surface rounded overflow-hidden border border-border">
84 <button
85 class="text-xs px-3 py-1 transition-colors {$diffScope === 'uncommitted' ? 'bg-active text-white' : 'text-tertiary hover:text-primary'}"
86 onclick={() => setDiffScope('uncommitted')}
87 >
88 Uncommitted
89 </button>
90 <button
91 class="text-xs px-3 py-1 transition-colors {$diffScope === 'worktree' ? 'bg-active text-white' : 'text-tertiary hover:text-primary'}"
92 onclick={() => setDiffScope('worktree')}
93 title={$baseBranch ? `Comparing against ${$baseBranch}` : 'Show all branch changes'}
94 >
95 Worktree
96 </button>
97 </div>
98
99 <!-- Diff mode toggle -->
100 <div class="flex bg-surface rounded overflow-hidden border border-border">
101 <button
102 class="text-xs px-3 py-1 transition-colors {$diffMode === 'full' ? 'bg-active text-white' : 'text-tertiary hover:text-primary'}"
103 onclick={() => setDiffMode('full')}
104 >
105 Full Diff
106 </button>
107 <button
108 class="text-xs px-3 py-1 transition-colors {$diffMode === 'unstaged' ? 'bg-active text-white' : 'text-tertiary hover:text-primary'}"
109 onclick={() => setDiffMode('unstaged')}
110 >
111 Needs Review
112 </button>
113 </div>
114
115 <div class="flex-1"></div>
116
117 <!-- Diff stats -->
118 {#if $approvedCount.total > 0}
119 <span class="text-xs text-tertiary flex items-center gap-2">
120 <span><span class="text-accent-green">{$approvedCount.approved}</span>/{$approvedCount.total} approved</span>
121 <span class="text-faint">|</span>
122 <span class="text-accent-green">+{$diffStats.additions}</span>
123 <span class="text-accent-red">-{$diffStats.deletions}</span>
124 {#if $diffStats.unapprovedAdditions > 0 || $diffStats.unapprovedDeletions > 0}
125 <span class="text-faint">|</span>
126 <span class="text-accent-yellow">
127 {$diffStats.unapprovedAdditions + $diffStats.unapprovedDeletions} lines to review
128 </span>
129 {/if}
130 </span>
131 {/if}
132
133 <!-- Stats -->
134 {#if $stats.totalComments > 0}
135 <span class="text-xs text-tertiary">
136 {$stats.totalComments} comment{$stats.totalComments !== 1 ? 's' : ''}
137 {#if $stats.unresolvedThreads > 0}
138 <span class="text-accent-yellow">({$stats.unresolvedThreads} unresolved)</span>
139 {/if}
140 </span>
141 {/if}
142
143 <!-- Status badge -->
144 {#if $reviewStatus !== 'pending'}
145 <span
146 class="text-xs px-2 py-0.5 rounded-full {$reviewStatus === 'approved' ? 'bg-accent-green/20 text-accent-green' : 'bg-accent-yellow/20 text-accent-yellow'}"
147 >
148 {$reviewStatus === 'approved' ? 'Approved' : 'Changes Requested'}
149 </span>
150 {/if}
151
152 <!-- Theme toggle -->
153 <button
154 class="text-xs px-2 py-1.5 rounded border border-border-strong text-tertiary hover:text-primary hover:bg-hover transition-colors"
155 onclick={() => theme.toggle()}
156 title="Toggle theme"
157 >
158 {$theme === 'dark' ? '☀️' : '🌙'}
159 </button>
160
161 <!-- Export button -->
162 <button
163 class="text-xs px-3 py-1.5 rounded border border-border-strong text-secondary hover:bg-hover transition-colors"
164 onclick={() => ($showExportDialog = true)}
165 >
166 Export
167 </button>
168
169 <!-- Review actions -->
170 <div class="relative">
171 <button
172 class="text-xs px-3 py-1.5 rounded bg-green-600 text-white hover:bg-green-500 transition-colors"
173 onclick={openReviewMenu}
174 >
175 Review ▾
176 </button>
177
178 {#if showReviewMenu}
179 <!-- svelte-ignore a11y_click_events_have_key_events -->
180 <!-- svelte-ignore a11y_no_static_element_interactions -->
181 <div
182 class="fixed inset-0 z-10"
183 onclick={() => (showReviewMenu = false)}
184 ></div>
185 <div class="absolute right-0 top-full mt-1 bg-panel border border-border-strong rounded-lg shadow-xl z-20 py-1 min-w-[220px]">
186 <button
187 class="w-full text-left px-3 py-2 text-sm text-secondary hover:bg-hover transition-colors"
188 onclick={() => { handleRequestChanges(); showReviewMenu = false; }}
189 >
190 <span class="text-accent-yellow mr-2">↻</span> Request Changes (send to AI)
191 </button>
192 <button
193 class="w-full text-left px-3 py-2 text-sm transition-colors {!canApprove ? 'text-muted cursor-not-allowed' : 'text-secondary hover:bg-hover'}"
194 disabled={!canApprove}
195 onclick={() => { handleApprove(); showReviewMenu = false; }}
196 >
197 <span class="text-accent-green mr-2">✓</span> {$vcs === 'jj' ? 'Approve' : 'Approve & Commit'}
198 {#if !canApprove}
199 <span class="text-xs text-muted ml-1">(nothing pending)</span>
200 {/if}
201 </button>
202 </div>
203 {/if}
204 </div>
205
206 {#if toast}
207 <div class="fixed bottom-4 right-4 z-50 bg-panel border border-accent-green/40 text-primary text-sm px-4 py-2 rounded-lg shadow-xl">
208 {toast}
209 </div>
210 {/if}
211</header>