Skip to content

Commit c1e218c

Browse files
committed
Add CI status to PR listings
1 parent 510e436 commit c1e218c

File tree

5 files changed

+327
-83
lines changed

5 files changed

+327
-83
lines changed

README.md

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,31 @@
11
<h1>
22
<img src="src/browser/logo.svg" alt="pulldash logo" width="32" height="32" align="center">
3-
pulldash
3+
Pulldash
44
</h1>
55

6-
The fastest way to review pull requests—a native desktop app that makes massive PRs feel instant.
6+
The fastest way to review pull requests that makes massive PRs feel instant.
77

8-
- Keyboard-driven workflow: navigate, comment, and approve without touching your mouse
9-
- Handles giant diffs smoothly: virtualized rendering keeps you at 60fps even on 10k+ line changes
10-
- Uses your GitHub credentials: no app install on GitHub
8+
- Keybord-driven: navigate, comment, and approve without touching your mouse
9+
- Performant: giant diffs render smoothly, virtualized rendering keeps you at 60fps even on 10k+ line changes
10+
- Local or hosted: download the desktop app to avoid sending your credentials anywhere
1111

1212
## Try It
1313

1414
Head to [pulldash.com](https://pulldash.com) to explore pull-requests (no auth required).
1515

1616
[Download for Desktop](https://github.com/coder/pulldash/releases).
1717

18+
## Features
19+
20+
- Customize your PR list with search queries:
21+
1822
## Why Not GitHub's Web UI?
1923

20-
GitHub's PR interface works, but it wasn't built for speed:
24+
- Lack of native PR tracking
25+
26+
GitHub's PR interface is slow, especially for large PRs.
27+
28+
GitHub's PR interface is slow, especially for large PRs.
2129

2230
| Issue | GitHub Web | Pulldash |
2331
| ----------------------- | ----------------------------- | ----------------------------------- |
@@ -26,6 +34,8 @@ GitHub's PR interface works, but it wasn't built for speed:
2634
| **Multi-file review** | Constant page loads | ✓ Instant tab switching |
2735
| **Context switching** | Browser tabs everywhere | ✓ Dedicated app, focused experience |
2836

37+
GitHub supports [CORS](https://docs.github.com/en/rest/using-the-rest-api/using-cors-and-jsonp-to-make-cross-origin-requests) on their API, making Pulldash a simple UI.
38+
2939
## Keyboard Shortcuts
3040

3141
| Action | Shortcut |

src/browser/components/home.tsx

Lines changed: 167 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@ import {
1515
Users,
1616
RefreshCw,
1717
Github,
18+
Circle,
19+
CheckCircle2,
20+
XCircle,
21+
MessageSquare,
1822
} from "lucide-react";
1923
import { cn } from "../cn";
2024
import { Skeleton } from "../ui/skeleton";
@@ -55,7 +59,12 @@ interface SearchResult {
5559
}
5660

5761
// Filter mode type
58-
type FilterMode = "review-requested" | "authored" | "involves" | "all";
62+
type FilterMode =
63+
| "review-requested"
64+
| "reviewed"
65+
| "authored"
66+
| "involves"
67+
| "all";
5968

6069
// Special constant for "All Repos" global filter
6170
const ALL_REPOS_KEY = "__all_repos__";
@@ -130,6 +139,8 @@ function getModeFilter(mode: FilterMode): string {
130139
switch (mode) {
131140
case "review-requested":
132141
return "review-requested:@me";
142+
case "reviewed":
143+
return "reviewed-by:@me";
133144
case "authored":
134145
return "author:@me";
135146
case "involves":
@@ -234,6 +245,12 @@ const MODE_OPTIONS = [
234245
icon: Eye,
235246
description: "PRs where you're requested as reviewer",
236247
},
248+
{
249+
value: "reviewed",
250+
label: "Reviewed",
251+
icon: MessageSquare,
252+
description: "PRs you've already reviewed",
253+
},
237254
{
238255
value: "authored",
239256
label: "My PRs",
@@ -301,7 +318,15 @@ export function Home() {
301318
if (githubReady && !(isAnonymous && queriesRequireAuth)) {
302319
fetchPRList(searchQueries, page, perPage);
303320
}
304-
}, [fetchPRList, searchQueries, page, perPage, githubReady, isAnonymous, queriesRequireAuth]);
321+
}, [
322+
fetchPRList,
323+
searchQueries,
324+
page,
325+
perPage,
326+
githubReady,
327+
isAnonymous,
328+
queriesRequireAuth,
329+
]);
305330

306331
// Reset page when config changes
307332
useEffect(() => {
@@ -388,10 +413,17 @@ export function Home() {
388413

389414
// Track which repo dropdown is open
390415
const [openRepoDropdown, setOpenRepoDropdown] = useState<string | null>(null);
391-
const [repoDropdownPosition, setRepoDropdownPosition] = useState({ top: 0, left: 0 });
416+
const [repoDropdownPosition, setRepoDropdownPosition] = useState({
417+
top: 0,
418+
left: 0,
419+
});
392420
const [showAddRepo, setShowAddRepo] = useState(false);
393-
const [addRepoButtonRef, setAddRepoButtonRef] = useState<HTMLButtonElement | null>(null);
394-
const [addRepoDropdownPosition, setAddRepoDropdownPosition] = useState({ top: 0, right: 0 });
421+
const [addRepoButtonRef, setAddRepoButtonRef] =
422+
useState<HTMLButtonElement | null>(null);
423+
const [addRepoDropdownPosition, setAddRepoDropdownPosition] = useState({
424+
top: 0,
425+
right: 0,
426+
});
395427

396428
// Show loading/error state while GitHub client initializes
397429
if (!githubReady) {
@@ -561,20 +593,8 @@ export function Home() {
561593
})}
562594
</div>
563595

564-
{/* Query Preview & Add Repo - pushed to right */}
596+
{/* Add Repo - pushed to right */}
565597
<div className="flex items-center gap-2 shrink-0 ml-auto">
566-
{/* Query Preview */}
567-
{searchQueries.length > 0 && (
568-
<div
569-
className="text-xs text-muted-foreground font-mono truncate max-w-xs hidden lg:block shrink-0"
570-
title={searchQueries.join("\n")}
571-
>
572-
{searchQueries.length === 1
573-
? searchQueries[0]
574-
: `${searchQueries.length} queries`}
575-
</div>
576-
)}
577-
578598
{/* Add Repo Button */}
579599
<div className="relative shrink-0">
580600
<button
@@ -637,64 +657,66 @@ export function Home() {
637657
</div>
638658

639659
<div className="add-repo-dropdown max-h-64 overflow-auto">
640-
{/* All Repos option - always shown at top when not already added */}
641-
{!config.repos.some(isAllReposFilter) && !searchQuery && (
642-
<button
643-
onMouseDown={() => {
644-
handleAddRepo(ALL_REPOS_KEY);
645-
setShowAddRepo(false);
646-
}}
647-
className="w-full flex items-center gap-2 px-3 py-2.5 hover:bg-primary/10 transition-colors text-left border-b border-border bg-primary/5"
648-
>
649-
<div className="w-4 h-4 rounded bg-primary/20 flex items-center justify-center shrink-0">
650-
<Users className="w-3 h-3 text-primary" />
651-
</div>
652-
<div className="flex-1 min-w-0">
653-
<span className="font-medium text-xs">All Repos</span>
654-
<span className="text-[10px] text-muted-foreground ml-1.5">
655-
PRs across all repositories
656-
</span>
657-
</div>
658-
</button>
659-
)}
660-
{searchResults.length > 0 ? (
661-
searchResults.map((repo) => (
660+
{/* All Repos option - always shown at top when not already added */}
661+
{!config.repos.some(isAllReposFilter) && !searchQuery && (
662662
<button
663-
key={repo.id}
664663
onMouseDown={() => {
665-
handleAddRepo(repo.full_name);
664+
handleAddRepo(ALL_REPOS_KEY);
666665
setShowAddRepo(false);
667-
setSearchQuery("");
668666
}}
669-
className="w-full flex items-center gap-2 px-3 py-2 hover:bg-muted/50 transition-colors text-left border-b border-border/50 last:border-b-0"
667+
className="w-full flex items-center gap-2 px-3 py-2.5 hover:bg-primary/10 transition-colors text-left border-b border-border bg-primary/5"
670668
>
671-
{repo.owner && (
672-
<img
673-
src={repo.owner.avatar_url}
674-
alt={repo.owner.login}
675-
className="w-4 h-4 rounded shrink-0"
676-
/>
677-
)}
678-
<span className="font-medium text-xs truncate flex-1">
679-
{repo.full_name}
680-
</span>
681-
<span className="flex items-center gap-1 text-[10px] text-muted-foreground">
682-
<Star className="w-3 h-3" />
683-
{(repo.stargazers_count ?? 0).toLocaleString()}
684-
</span>
669+
<div className="w-4 h-4 rounded bg-primary/20 flex items-center justify-center shrink-0">
670+
<Users className="w-3 h-3 text-primary" />
671+
</div>
672+
<div className="flex-1 min-w-0">
673+
<span className="font-medium text-xs">
674+
All Repos
675+
</span>
676+
<span className="text-[10px] text-muted-foreground ml-1.5">
677+
PRs across all repositories
678+
</span>
679+
</div>
685680
</button>
686-
))
687-
) : searchQuery ? (
688-
<div className="px-3 py-4 text-xs text-muted-foreground text-center">
689-
{searching ? "Searching..." : "No repositories found"}
690-
</div>
691-
) : (
692-
<div className="px-3 py-4 text-xs text-muted-foreground text-center">
693-
Type to search for repositories
694-
</div>
695-
)}
681+
)}
682+
{searchResults.length > 0 ? (
683+
searchResults.map((repo) => (
684+
<button
685+
key={repo.id}
686+
onMouseDown={() => {
687+
handleAddRepo(repo.full_name);
688+
setShowAddRepo(false);
689+
setSearchQuery("");
690+
}}
691+
className="w-full flex items-center gap-2 px-3 py-2 hover:bg-muted/50 transition-colors text-left border-b border-border/50 last:border-b-0"
692+
>
693+
{repo.owner && (
694+
<img
695+
src={repo.owner.avatar_url}
696+
alt={repo.owner.login}
697+
className="w-4 h-4 rounded shrink-0"
698+
/>
699+
)}
700+
<span className="font-medium text-xs truncate flex-1">
701+
{repo.full_name}
702+
</span>
703+
<span className="flex items-center gap-1 text-[10px] text-muted-foreground">
704+
<Star className="w-3 h-3" />
705+
{(repo.stargazers_count ?? 0).toLocaleString()}
706+
</span>
707+
</button>
708+
))
709+
) : searchQuery ? (
710+
<div className="px-3 py-4 text-xs text-muted-foreground text-center">
711+
{searching ? "Searching..." : "No repositories found"}
712+
</div>
713+
) : (
714+
<div className="px-3 py-4 text-xs text-muted-foreground text-center">
715+
Type to search for repositories
716+
</div>
717+
)}
718+
</div>
696719
</div>
697-
</div>
698720
</>
699721
)}
700722
</div>
@@ -906,6 +928,81 @@ function PRListItem({ pr, onSelect }: PRListItemProps) {
906928
}
907929
};
908930

931+
// CI status indicator with details
932+
const CIStatusBadge = () => {
933+
if (!pr.ciStatus || pr.ciStatus === "none") return null;
934+
935+
const summary =
936+
pr.ciSummary ||
937+
(pr.ciStatus === "success"
938+
? "Passed"
939+
: pr.ciStatus === "failure"
940+
? "Failed"
941+
: "Running");
942+
943+
switch (pr.ciStatus) {
944+
case "success":
945+
return (
946+
<span
947+
title={
948+
pr.ciChecks
949+
?.map(
950+
(c) =>
951+
`${c.state === "success" ? "✓" : c.state === "failure" ? "✗" : "○"} ${c.name}`
952+
)
953+
.join("\n") || "CI passed"
954+
}
955+
className="shrink-0 inline-flex items-center gap-1 px-1.5 py-0.5 text-[10px] font-medium rounded bg-green-500/15 text-green-500 border border-green-500/30"
956+
>
957+
<CheckCircle2 className="w-3 h-3" />
958+
<span className="hidden sm:inline max-w-[100px] truncate">
959+
{summary}
960+
</span>
961+
</span>
962+
);
963+
case "failure":
964+
return (
965+
<span
966+
title={
967+
pr.ciChecks
968+
?.map(
969+
(c) =>
970+
`${c.state === "success" ? "✓" : c.state === "failure" ? "✗" : "○"} ${c.name}`
971+
)
972+
.join("\n") || "CI failed"
973+
}
974+
className="shrink-0 inline-flex items-center gap-1 px-1.5 py-0.5 text-[10px] font-medium rounded bg-red-500/15 text-red-500 border border-red-500/30"
975+
>
976+
<XCircle className="w-3 h-3" />
977+
<span className="hidden sm:inline max-w-[100px] truncate">
978+
{summary}
979+
</span>
980+
</span>
981+
);
982+
case "pending":
983+
return (
984+
<span
985+
title={
986+
pr.ciChecks
987+
?.map(
988+
(c) =>
989+
`${c.state === "success" ? "✓" : c.state === "failure" ? "✗" : "○"} ${c.name}`
990+
)
991+
.join("\n") || "CI running"
992+
}
993+
className="shrink-0 inline-flex items-center gap-1 px-1.5 py-0.5 text-[10px] font-medium rounded bg-yellow-500/15 text-yellow-500 border border-yellow-500/30"
994+
>
995+
<Circle className="w-3 h-3 animate-pulse" />
996+
<span className="hidden sm:inline max-w-[100px] truncate">
997+
{summary}
998+
</span>
999+
</span>
1000+
);
1001+
default:
1002+
return null;
1003+
}
1004+
};
1005+
9091006
return (
9101007
<button
9111008
onClick={handleClick}
@@ -931,6 +1028,7 @@ function PRListItem({ pr, onSelect }: PRListItemProps) {
9311028
<span className="font-medium hover:text-blue-400 break-words">
9321029
{pr.title}
9331030
</span>
1031+
<CIStatusBadge />
9341032
{pr.hasNewChanges && (
9351033
<span className="px-1.5 py-0.5 text-[10px] font-semibold rounded bg-blue-500/20 text-blue-400 border border-blue-500/30 shrink-0">
9361034
NEW

0 commit comments

Comments
 (0)