Skip to content

Commit 8487a94

Browse files
feat: PR diff in diff editor, search+pagination for PR dashboard
1 parent 8fd4a69 commit 8487a94

3 files changed

Lines changed: 148 additions & 18 deletions

File tree

crates/tauri-app/frontend/src/App.tsx

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,16 @@ export function App() {
4949
return project ? project.path !== "." : false;
5050
};
5151

52+
// Check if active thread is linked to a PR
53+
const activePrNumber = (): number | null => {
54+
const tab = store.activeTab;
55+
if (!tab) return null;
56+
const project = store.projects.find((p) => p.threads.some((t) => t.id === tab));
57+
if (!project) return null;
58+
const prMap = store.projectPrMap[project.id];
59+
return prMap?.[tab] ?? null;
60+
};
61+
5262
// Compute diff cwd reactively based on active thread
5363
const diffCwd = () => {
5464
const tab = store.activeTab;
@@ -195,7 +205,7 @@ export function App() {
195205
<BrowserPanel threadId={store.activeTab!} />
196206
</Show>
197207
<Show when={store.diffPanelOpen && diffCwd()}>
198-
<DiffEditor cwd={diffCwd()} />
208+
<DiffEditor cwd={diffCwd()} prNumber={activePrNumber()} />
199209
</Show>
200210
</div>
201211
</Show>

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

Lines changed: 67 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ function dirPath(path: string): string {
2525
return parts.slice(0, -1).join("/") + "/";
2626
}
2727

28-
export function DiffEditor(props: { cwd: string }) {
28+
export function DiffEditor(props: { cwd: string; prNumber?: number | null }) {
2929
const { setStore } = appStore;
3030
const [files, setFiles] = createSignal<ChangedFile[]>([]);
3131
const [diffs, setDiffs] = createSignal<FileDiff[]>([]);
@@ -44,15 +44,18 @@ export function DiffEditor(props: { cwd: string }) {
4444

4545
async function loadAll() {
4646
const cwd = props.cwd;
47+
const prNum = props.prNumber;
4748
if (!cwd || cwd === ".") {
4849
setFiles([]);
4950
setDiffs([]);
5051
setLoading(false);
5152
return;
5253
}
5354

55+
const cacheKey = prNum ? `pr:${prNum}:${cwd}` : cwd;
56+
5457
// Check cache first
55-
const cached = diffCache.get(cwd);
58+
const cached = diffCache.get(cacheKey);
5659
if (cached) {
5760
setFiles(cached.files);
5861
setDiffs(cached.diffs);
@@ -63,24 +66,34 @@ export function DiffEditor(props: { cwd: string }) {
6366

6467
setLoading(true);
6568
setError(null);
66-
// Clear old data immediately for clean transition
6769
setFiles([]);
6870
setDiffs([]);
6971
setSelectedFile(null);
7072

7173
try {
72-
const [changedFiles, sessionDiffs] = await Promise.all([
73-
ipc.getChangedFiles(cwd),
74-
ipc.getSessionDiff(cwd),
75-
]);
76-
// Only update if cwd hasn't changed while we were fetching
74+
let changedFiles: any[];
75+
let sessionDiffs: any[];
76+
77+
if (prNum) {
78+
// PR mode: fetch PR diff instead of working directory diff
79+
const prDiffRaw = await ipc.getPrDiff(cwd, prNum);
80+
// Parse the raw diff into our FileDiff format
81+
const parsed = parsePrDiff(prDiffRaw);
82+
changedFiles = parsed.files;
83+
sessionDiffs = parsed.diffs;
84+
} else {
85+
[changedFiles, sessionDiffs] = await Promise.all([
86+
ipc.getChangedFiles(cwd),
87+
ipc.getSessionDiff(cwd),
88+
]);
89+
}
90+
7791
if (props.cwd === cwd) {
7892
setFiles(changedFiles);
7993
setDiffs(sessionDiffs);
8094
if (changedFiles.length > 0) setSelectedFile(changedFiles[0].path);
81-
// Cache for 30 seconds
82-
diffCache.set(cwd, { files: changedFiles, diffs: sessionDiffs });
83-
setTimeout(() => diffCache.delete(cwd), 30000);
95+
diffCache.set(cacheKey, { files: changedFiles, diffs: sessionDiffs });
96+
setTimeout(() => diffCache.delete(cacheKey), 30000);
8497
}
8598
} catch (e) {
8699
if (props.cwd === cwd) setError(String(e));
@@ -89,9 +102,49 @@ export function DiffEditor(props: { cwd: string }) {
89102
}
90103
}
91104

92-
// Reload when cwd changes (thread switch)
105+
/** Parse raw unified diff text into ChangedFile[] and FileDiff[] */
106+
function parsePrDiff(raw: string): { files: any[]; diffs: any[] } {
107+
const files: any[] = [];
108+
const diffs: any[] = [];
109+
if (!raw) return { files, diffs };
110+
111+
const fileSections = raw.split(/^diff --git /m).filter(Boolean);
112+
for (const section of fileSections) {
113+
const lines = section.split("\n");
114+
// Extract file path from "a/path b/path"
115+
const headerMatch = lines[0]?.match(/a\/(.+?) b\/(.+)/);
116+
const path = headerMatch ? headerMatch[2] : "unknown";
117+
118+
let insertions = 0, deletions = 0;
119+
const hunkLines: any[] = [];
120+
let hunkHeader = "";
121+
122+
for (const line of lines.slice(1)) {
123+
if (line.startsWith("@@")) {
124+
hunkHeader = line;
125+
} else if (line.startsWith("+") && !line.startsWith("+++")) {
126+
insertions++;
127+
hunkLines.push({ line_type: "add", content: line.slice(1), old_line: null, new_line: null });
128+
} else if (line.startsWith("-") && !line.startsWith("---")) {
129+
deletions++;
130+
hunkLines.push({ line_type: "delete", content: line.slice(1), old_line: null, new_line: null });
131+
} else if (!line.startsWith("\\") && !line.startsWith("index") && !line.startsWith("---") && !line.startsWith("+++") && !line.startsWith("new") && !line.startsWith("old") && !line.startsWith("deleted") && !line.startsWith("similarity")) {
132+
hunkLines.push({ line_type: "context", content: line.startsWith(" ") ? line.slice(1) : line, old_line: null, new_line: null });
133+
}
134+
}
135+
136+
const status = deletions > 0 && insertions > 0 ? "modified" : insertions > 0 ? "added" : "deleted";
137+
files.push({ path, status, insertions, deletions });
138+
diffs.push({ path, hunks: [{ header: hunkHeader, lines: hunkLines }] });
139+
}
140+
141+
return { files, diffs };
142+
}
143+
144+
// Reload when cwd or prNumber changes (thread switch)
93145
createEffect(() => {
94-
const _ = props.cwd; // track reactive prop
146+
const _ = props.cwd;
147+
const _pr = props.prNumber;
95148
loadAll();
96149
});
97150

@@ -130,7 +183,7 @@ export function DiffEditor(props: { cwd: string }) {
130183
<svg class="de-header-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
131184
<path d="M12 3v18" /><path d="M3 12h18" />
132185
</svg>
133-
<h3>Changes</h3>
186+
<h3>{props.prNumber ? `PR #${props.prNumber}` : "Changes"}</h3>
134187
<Show when={!loading() && files().length > 0}>
135188
<span class="de-stat-summary">
136189
<span class="de-stat-files">{files().length} file{files().length !== 1 ? "s" : ""}</span>

crates/tauri-app/frontend/src/components/github/PrDashboard.tsx

Lines changed: 70 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,23 @@ export function PrDashboard(props: Props) {
1313
const [prs, setPrs] = createSignal<PullRequest[]>([]);
1414
const [loading, setLoading] = createSignal(false);
1515
const [filter, setFilter] = createSignal<"open" | "closed" | "all">("open");
16+
const [search, setSearch] = createSignal("");
17+
const [visibleCount, setVisibleCount] = createSignal(10);
18+
19+
const filteredPrs = () => {
20+
const q = search().toLowerCase();
21+
if (!q) return prs();
22+
return prs().filter((pr) =>
23+
pr.title.toLowerCase().includes(q) ||
24+
pr.author.toLowerCase().includes(q) ||
25+
pr.branch.toLowerCase().includes(q) ||
26+
String(pr.number).includes(q) ||
27+
pr.labels.some((l) => l.toLowerCase().includes(q))
28+
);
29+
};
30+
31+
const visiblePrs = () => filteredPrs().slice(0, visibleCount());
32+
const hasMore = () => filteredPrs().length > visibleCount();
1633

1734
// Reload when repoPath changes (thread/project switch)
1835
createEffect(() => {
@@ -125,16 +142,25 @@ export function PrDashboard(props: Props) {
125142
</button>
126143
</div>
127144

145+
<Show when={prs().length > 5}>
146+
<input
147+
class="prd-search"
148+
placeholder="Search PRs by title, author, branch, #number…"
149+
value={search()}
150+
onInput={(e) => { setSearch(e.currentTarget.value); setVisibleCount(10); }}
151+
/>
152+
</Show>
153+
128154
<Show when={loading()}>
129155
<div class="prd-loading">Loading pull requests…</div>
130156
</Show>
131157

132-
<Show when={!loading() && prs().length === 0}>
133-
<div class="prd-empty">No {filter()} pull requests</div>
158+
<Show when={!loading() && filteredPrs().length === 0}>
159+
<div class="prd-empty">{search() ? `No PRs matching "${search()}"` : `No ${filter()} pull requests`}</div>
134160
</Show>
135161

136162
<div class="prd-list">
137-
<For each={prs()}>
163+
<For each={visiblePrs()}>
138164
{(pr) => {
139165
const badge = reviewBadge(pr.review_status);
140166
return (
@@ -177,6 +203,14 @@ export function PrDashboard(props: Props) {
177203
);
178204
}}
179205
</For>
206+
<Show when={hasMore()}>
207+
<button class="prd-show-more" onClick={() => setVisibleCount((c) => c + 10)}>
208+
Show {Math.min(10, filteredPrs().length - visibleCount())} more of {filteredPrs().length} PRs
209+
</button>
210+
</Show>
211+
<Show when={filteredPrs().length > 0}>
212+
<div class="prd-count">{filteredPrs().length} PR{filteredPrs().length !== 1 ? "s" : ""}{search() ? ` matching "${search()}"` : ""}</div>
213+
</Show>
180214
</div>
181215
</div>
182216
);
@@ -234,6 +268,39 @@ if (!document.getElementById("prd-styles")) {
234268
transition: color 0.1s, background 0.1s;
235269
}
236270
.prd-refresh:hover { color: var(--text-secondary); background: var(--bg-hover); }
271+
.prd-search {
272+
width: 100%;
273+
padding: 7px 10px;
274+
font-size: 12px;
275+
background: var(--bg-muted);
276+
border: 1px solid var(--border);
277+
border-radius: var(--radius-sm);
278+
color: var(--text);
279+
font-family: var(--font-body);
280+
outline: none;
281+
margin-bottom: 10px;
282+
}
283+
.prd-search:focus { border-color: var(--primary); box-shadow: 0 0 0 2px var(--primary-glow); }
284+
.prd-search::placeholder { color: var(--text-tertiary); }
285+
.prd-show-more {
286+
width: 100%;
287+
padding: 8px;
288+
font-size: 12px;
289+
font-weight: 500;
290+
color: var(--primary);
291+
background: rgba(107, 124, 255, 0.06);
292+
border: 1px solid rgba(107, 124, 255, 0.12);
293+
border-radius: var(--radius-sm);
294+
margin-top: 6px;
295+
transition: all 0.12s;
296+
}
297+
.prd-show-more:hover { background: rgba(107, 124, 255, 0.1); border-color: rgba(107, 124, 255, 0.2); }
298+
.prd-count {
299+
font-size: 10px;
300+
color: var(--text-tertiary);
301+
text-align: center;
302+
margin-top: 8px;
303+
}
237304
.prd-loading, .prd-empty {
238305
font-size: 12px;
239306
color: var(--text-tertiary);

0 commit comments

Comments
 (0)