Skip to content

Commit 66eaf1b

Browse files
feat: diff mode toggle (Both/PR/Local) for PR-linked threads
1 parent 64d1b16 commit 66eaf1b

1 file changed

Lines changed: 110 additions & 39 deletions

File tree

crates/tauri-app/frontend/src/components/diff/DiffEditor.tsx

Lines changed: 110 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -34,75 +34,105 @@ export function DiffEditor(props: { cwd: string; prNumber?: number | null }) {
3434
const [error, setError] = createSignal<string | null>(null);
3535
const [collapsedHunks, setCollapsedHunks] = createSignal<Set<string>>(new Set());
3636
const [gitPanelOpen, setGitPanelOpen] = createSignal(true);
37+
const [diffMode, setDiffMode] = createSignal<"both" | "pr" | "worktree">("both");
38+
39+
// Separate storage for PR diff and worktree diff
40+
const [prFiles, setPrFiles] = createSignal<ChangedFile[]>([]);
41+
const [prDiffs, setPrDiffs] = createSignal<FileDiff[]>([]);
42+
const [wtFiles, setWtFiles] = createSignal<ChangedFile[]>([]);
43+
const [wtDiffs, setWtDiffs] = createSignal<FileDiff[]>([]);
44+
45+
const hasPr = () => !!props.prNumber;
3746

3847
function close() {
3948
const tab = appStore.store.activeTab;
4049
if (tab) setStore("threadDiffOpen", tab, false);
4150
}
4251

43-
// Cache diffs per cwd to avoid re-fetching on thread switch
44-
const diffCache = new Map<string, { files: any[]; diffs: any[] }>();
45-
4652
async function loadAll() {
4753
const cwd = props.cwd;
4854
const prNum = props.prNumber;
4955
if (!cwd || cwd === ".") {
50-
setFiles([]);
51-
setDiffs([]);
52-
setLoading(false);
53-
return;
54-
}
55-
56-
const cacheKey = prNum ? `pr:${prNum}:${cwd}` : cwd;
57-
58-
// Check cache first
59-
const cached = diffCache.get(cacheKey);
60-
if (cached) {
61-
setFiles(cached.files);
62-
setDiffs(cached.diffs);
63-
if (cached.files.length > 0) setSelectedFile(cached.files[0].path);
56+
setFiles([]); setDiffs([]);
6457
setLoading(false);
6558
return;
6659
}
6760

6861
setLoading(true);
6962
setError(null);
70-
setFiles([]);
71-
setDiffs([]);
63+
setFiles([]); setDiffs([]);
64+
setPrFiles([]); setPrDiffs([]);
65+
setWtFiles([]); setWtDiffs([]);
7266
setSelectedFile(null);
7367

7468
try {
75-
let changedFiles: any[];
76-
let sessionDiffs: any[];
69+
// Always fetch worktree/local changes
70+
const [localFiles, localDiffs] = await Promise.all([
71+
ipc.getChangedFiles(cwd).catch(() => []),
72+
ipc.getSessionDiff(cwd).catch(() => []),
73+
]);
74+
if (props.cwd === cwd) {
75+
setWtFiles(localFiles as any);
76+
setWtDiffs(localDiffs as any);
77+
}
7778

79+
// If PR linked, also fetch PR diff
7880
if (prNum) {
79-
// PR mode: fetch PR diff instead of working directory diff
80-
const prDiffRaw = await ipc.getPrDiff(cwd, prNum);
81-
// Parse the raw diff into our FileDiff format
82-
const parsed = parsePrDiff(prDiffRaw);
83-
changedFiles = parsed.files;
84-
sessionDiffs = parsed.diffs;
85-
} else {
86-
[changedFiles, sessionDiffs] = await Promise.all([
87-
ipc.getChangedFiles(cwd),
88-
ipc.getSessionDiff(cwd),
89-
]);
81+
try {
82+
const prDiffRaw = await ipc.getPrDiff(cwd, prNum);
83+
const parsed = parsePrDiff(prDiffRaw);
84+
if (props.cwd === cwd) {
85+
setPrFiles(parsed.files);
86+
setPrDiffs(parsed.diffs);
87+
}
88+
} catch {}
9089
}
9190

92-
if (props.cwd === cwd) {
93-
setFiles(changedFiles);
94-
setDiffs(sessionDiffs);
95-
if (changedFiles.length > 0) setSelectedFile(changedFiles[0].path);
96-
diffCache.set(cacheKey, { files: changedFiles, diffs: sessionDiffs });
97-
setTimeout(() => diffCache.delete(cacheKey), 30000);
98-
}
91+
// Apply current mode
92+
if (props.cwd === cwd) applyMode();
9993
} catch (e) {
10094
if (props.cwd === cwd) setError(String(e));
10195
} finally {
10296
if (props.cwd === cwd) setLoading(false);
10397
}
10498
}
10599

100+
function applyMode() {
101+
const mode = diffMode();
102+
let mergedFiles: any[] = [];
103+
let mergedDiffs: any[] = [];
104+
105+
if (mode === "pr" && hasPr()) {
106+
mergedFiles = prFiles();
107+
mergedDiffs = prDiffs();
108+
} else if (mode === "worktree") {
109+
mergedFiles = wtFiles();
110+
mergedDiffs = wtDiffs();
111+
} else {
112+
// "both" — merge PR + worktree, deduplicating by path (worktree wins)
113+
const fileMap = new Map<string, any>();
114+
const diffMap = new Map<string, any>();
115+
for (const f of prFiles()) { fileMap.set(f.path, { ...f, source: "pr" }); }
116+
for (const d of prDiffs()) { diffMap.set(d.path, d); }
117+
for (const f of wtFiles()) { fileMap.set(f.path, { ...f, source: "worktree" }); }
118+
for (const d of wtDiffs()) { diffMap.set(d.path, d); }
119+
mergedFiles = Array.from(fileMap.values());
120+
mergedDiffs = Array.from(diffMap.values());
121+
}
122+
123+
setFiles(mergedFiles);
124+
setDiffs(mergedDiffs);
125+
if (mergedFiles.length > 0 && !selectedFile()) {
126+
setSelectedFile(mergedFiles[0].path);
127+
}
128+
}
129+
130+
// Re-apply mode when diffMode changes (without re-fetching)
131+
createEffect(() => {
132+
const _ = diffMode();
133+
if (!loading()) applyMode();
134+
});
135+
106136
/** Parse raw unified diff text into ChangedFile[] and FileDiff[] */
107137
function parsePrDiff(raw: string): { files: any[]; diffs: any[] } {
108138
const files: any[] = [];
@@ -197,6 +227,25 @@ export function DiffEditor(props: { cwd: string; prNumber?: number | null }) {
197227
</span>
198228
</Show>
199229
</div>
230+
<Show when={hasPr()}>
231+
<div class="de-mode-toggle">
232+
<button
233+
class="de-mode-btn"
234+
classList={{ "de-mode-btn--active": diffMode() === "both" }}
235+
onClick={() => setDiffMode("both")}
236+
>Both</button>
237+
<button
238+
class="de-mode-btn"
239+
classList={{ "de-mode-btn--active": diffMode() === "pr" }}
240+
onClick={() => setDiffMode("pr")}
241+
>PR</button>
242+
<button
243+
class="de-mode-btn"
244+
classList={{ "de-mode-btn--active": diffMode() === "worktree" }}
245+
onClick={() => setDiffMode("worktree")}
246+
>Local</button>
247+
</div>
248+
</Show>
200249
<div class="de-header-actions">
201250
<button class="de-icon-btn" onClick={loadAll} title="Refresh">
202251
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
@@ -389,6 +438,28 @@ const DIFF_STYLES = `
389438
font-weight: 500;
390439
}
391440
441+
.de-mode-toggle {
442+
display: flex;
443+
gap: 1px;
444+
background: var(--bg-muted);
445+
border-radius: var(--radius-sm);
446+
padding: 2px;
447+
margin-left: auto;
448+
}
449+
.de-mode-btn {
450+
font-size: 10px;
451+
font-weight: 600;
452+
padding: 3px 8px;
453+
border-radius: 4px;
454+
color: var(--text-tertiary);
455+
transition: all 0.1s;
456+
}
457+
.de-mode-btn:hover { color: var(--text-secondary); }
458+
.de-mode-btn--active {
459+
background: var(--bg-accent);
460+
color: var(--text);
461+
box-shadow: 0 1px 3px rgba(0,0,0,0.15);
462+
}
392463
.de-header-actions { display: flex; align-items: center; gap: 4px; }
393464
.de-icon-btn {
394465
color: var(--text-tertiary);

0 commit comments

Comments
 (0)