From 3b7850fe1a1b09061728c29e3ccd57f76252365c Mon Sep 17 00:00:00 2001 From: Joshua Coles Date: Tue, 28 Jul 2026 15:12:29 +0000 Subject: [PATCH] Per-PR timeline focus: click a card to filter activity, mark-PR-read, clear chip Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01DgBEW6qAwgn2fcQbA2f4ZQ --- server/db.ts | 11 ++++++- server/routes.ts | 3 +- web/src/App.tsx | 11 +++++-- web/src/api.ts | 5 ++++ web/src/components/PrCard.tsx | 21 ++++++++++++-- web/src/components/PrList.tsx | 17 +++++++++-- web/src/components/Timeline.tsx | 51 ++++++++++++++++++++++++--------- web/src/styles/app.css | 36 +++++++++++++++++++++++ 8 files changed, 134 insertions(+), 21 deletions(-) diff --git a/server/db.ts b/server/db.ts index 6bb4c5a..742e1fe 100644 --- a/server/db.ts +++ b/server/db.ts @@ -195,9 +195,18 @@ export class Db { return this.db.prepare('SELECT 1 FROM events WHERE id = ?').get(id) !== undefined; } - timeline(opts: { limit: number; before?: string; unreadOnly?: boolean }): TimelinePage { + timeline(opts: { + limit: number; + before?: string; + unreadOnly?: boolean; + prId?: string; + }): TimelinePage { const clauses = ["p.state != ''"]; const params: (string | number)[] = []; + if (opts.prId) { + clauses.push('e.pr_id = ?'); + params.push(opts.prId); + } if (opts.before) { clauses.push('(e.created_at < ? OR (e.created_at = ? AND e.id < ?))'); const [createdAt, id] = splitCursor(opts.before); diff --git a/server/routes.ts b/server/routes.ts index ce1dcfb..e8e254f 100644 --- a/server/routes.ts +++ b/server/routes.ts @@ -41,7 +41,8 @@ export function createApp({ db, triggerSync }: RouteDeps): Express { const limit = Math.min(Number(req.query.limit) || 50, 200); const before = typeof req.query.before === 'string' ? req.query.before : undefined; const unreadOnly = req.query.unread === '1'; - res.json(db.timeline({ limit, before, unreadOnly })); + const prId = typeof req.query.pr === 'string' ? req.query.pr : undefined; + res.json(db.timeline({ limit, before, unreadOnly, prId })); }); app.post('/api/timeline/:id/read', (req, res) => { diff --git a/web/src/App.tsx b/web/src/App.tsx index a14461c..3ee66d2 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -12,8 +12,15 @@ export default function App() { const sse = useSSE(); const stateQuery = useQuery({ queryKey: ['state'], queryFn: fetchState }); const [mobileView, setMobileView] = useState('prs'); + const [selectedPrId, setSelectedPrId] = useState(null); const state = stateQuery.data; + const selectedPr = state?.prs.find((p) => p.id === selectedPrId) ?? null; + + const selectPr = (id: string) => { + setSelectedPrId((cur) => (cur === id ? null : id)); + setMobileView('timeline'); + }; const lastSyncAt = sse.lastSync?.lastSyncAt ?? state?.lastSyncAt ?? null; const error = sse.lastSync ? sse.lastSync.error : (state?.lastSyncError ?? null); const unreadTotal = sse.lastSync?.unreadCount ?? state?.unreadTotal ?? 0; @@ -24,7 +31,7 @@ export default function App() {
{state ? ( - + ) : (
{stateQuery.isError ? 'Failed to load — is the server running?' : 'Loading…'} @@ -32,7 +39,7 @@ export default function App() { )}
- + setSelectedPrId(null)} />