Skip to content

Commit a04f690

Browse files
committed
fix(plugin-git): make commit load-more pagination reliable
1 parent 49604aa commit a04f690

5 files changed

Lines changed: 113 additions & 40 deletions

File tree

plugins/git/src/client/components/branches-panel.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,11 @@ import { useRpcResource } from './use-rpc-resource'
1212

1313
function BranchRow({ branch }: { branch: Branch }) {
1414
return (
15-
<li className="border-border/60 flex items-center gap-2 border-b py-2 last:border-0">
15+
<li className="border-border/60 flex items-center gap-2 border-b py-1.5 last:border-0">
1616
<GitBranch className={`size-4 shrink-0 ${branch.current ? 'text-primary' : 'text-muted-foreground'}`} />
1717
<div className="min-w-0 flex-1">
1818
<div className="flex items-center gap-2">
19-
<span className={`truncate font-mono text-sm ${branch.current ? 'font-semibold' : ''}`}>
19+
<span className={`truncate font-mono text-xs ${branch.current ? 'font-semibold' : ''}`}>
2020
{branch.name}
2121
</span>
2222
{branch.current && (
@@ -58,8 +58,8 @@ export function BranchesPanel() {
5858
<span className="text-muted-foreground text-xs">
5959
{data?.isRepo ? `${data.branches.length} branches` : ' '}
6060
</span>
61-
<Button variant="ghost" size="icon" onClick={refresh} disabled={loading} aria-label="Refresh branches">
62-
<RefreshCw className={loading ? 'animate-spin' : ''} />
61+
<Button variant="ghost" size="icon" className="size-7" onClick={refresh} disabled={loading} aria-label="Refresh branches">
62+
<RefreshCw className={`size-3.5 ${loading ? 'animate-spin' : ''}`} />
6363
</Button>
6464
</div>
6565

plugins/git/src/client/components/dashboard.tsx

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -29,22 +29,22 @@ function ConnectionBadge() {
2929
function ThemeToggle() {
3030
const { theme, toggle } = useTheme()
3131
return (
32-
<Button variant="ghost" size="icon" onClick={toggle} aria-label="Toggle light/dark theme">
33-
{theme === 'dark' ? <Sun /> : <Moon />}
32+
<Button variant="ghost" size="icon" className="size-7" onClick={toggle} aria-label="Toggle light/dark theme">
33+
{theme === 'dark' ? <Sun className="size-3.5" /> : <Moon className="size-3.5" />}
3434
</Button>
3535
)
3636
}
3737

3838
export function Dashboard() {
3939
return (
4040
<RpcProvider>
41-
<main className="mx-auto flex max-w-3xl flex-col gap-6 px-6 py-10">
41+
<main className="mx-auto flex max-w-2xl flex-col gap-4 px-4 py-6">
4242
<header className="flex items-center justify-between gap-3">
4343
<div className="flex items-center gap-2">
4444
<GitGraph className="text-primary size-6" />
4545
<div>
46-
<h1 className="text-lg leading-none font-semibold">Git Dashboard</h1>
47-
<p className="text-muted-foreground text-xs">
46+
<h1 className="text-base leading-none font-semibold">Git Dashboard</h1>
47+
<p className="text-muted-foreground text-[11px]">
4848
devframe + Next.js · type-safe RPC into the host repository
4949
</p>
5050
</div>
@@ -75,8 +75,8 @@ export function Dashboard() {
7575
</TabsTrigger>
7676
</TabsList>
7777

78-
<Card className="mt-2">
79-
<CardContent>
78+
<Card className="mt-1">
79+
<CardContent className="px-4">
8080
<TabsContent value="status"><StatusPanel /></TabsContent>
8181
<TabsContent value="commits"><LogPanel /></TabsContent>
8282
<TabsContent value="branches"><BranchesPanel /></TabsContent>

plugins/git/src/client/components/diff-panel.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -93,8 +93,8 @@ export function DiffPanel() {
9393
</span>
9494
</span>
9595
)}
96-
<Button variant="ghost" size="icon" onClick={refresh} disabled={loading} aria-label="Refresh diff">
97-
<RefreshCw className={loading ? 'animate-spin' : ''} />
96+
<Button variant="ghost" size="icon" className="size-7" onClick={refresh} disabled={loading} aria-label="Refresh diff">
97+
<RefreshCw className={`size-3.5 ${loading ? 'animate-spin' : ''}`} />
9898
</Button>
9999
</div>
100100
</div>

plugins/git/src/client/components/log-panel.tsx

Lines changed: 97 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,18 @@ import type { DevframeRpcClient } from 'devframe/client'
44
import type { Commit } from '../../index'
55
import type { GraphRow } from '../lib/commit-graph'
66
import { RefreshCw } from 'lucide-react'
7-
import { useCallback, useMemo, useState } from 'react'
7+
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
88
import { computeGraph } from '../lib/commit-graph'
9+
import { useRpc } from './rpc-provider'
910
import { Badge } from './ui/badge'
1011
import { Button } from './ui/button'
1112
import { ScrollArea } from './ui/scroll-area'
1213
import { Skeleton } from './ui/skeleton'
13-
import { useRpcResource } from './use-rpc-resource'
1414

1515
const PAGE = 30
16-
const ROW_H = 54
17-
const COL_W = 14
18-
const NODE_R = 4.5
16+
const ROW_H = 42
17+
const COL_W = 12
18+
const NODE_R = 4
1919

2020
function relativeTime(epoch: number): string {
2121
const diff = Date.now() - epoch
@@ -98,59 +98,132 @@ function CommitRow({ commit, row, gutter }: { commit: Commit, row: GraphRow, gut
9898
}
9999

100100
export function LogPanel() {
101-
const [limit, setLimit] = useState(PAGE)
102-
const loader = useCallback(
103-
(rpc: DevframeRpcClient) => rpc.call('git:log', { limit }),
104-
[limit],
105-
)
106-
const { data, loading, refresh } = useRpcResource(loader)
101+
const { rpc } = useRpc()
102+
const [isRepo, setIsRepo] = useState<boolean | null>(null)
103+
const [commits, setCommits] = useState<Commit[]>([])
104+
const [skip, setSkip] = useState(0)
105+
const [hasMore, setHasMore] = useState(false)
106+
const [loading, setLoading] = useState(false)
107+
const [error, setError] = useState<string | null>(null)
108+
const commitsRef = useRef<Commit[]>([])
109+
110+
useEffect(() => {
111+
commitsRef.current = commits
112+
}, [commits])
113+
114+
const loadPage = useCallback(async (
115+
client: DevframeRpcClient,
116+
nextSkip: number,
117+
mode: 'replace' | 'append',
118+
) => {
119+
setLoading(true)
120+
setError(null)
121+
try {
122+
const page = await client.call('git:log', { limit: PAGE, skip: nextSkip })
123+
setIsRepo(page.isRepo)
124+
if (mode === 'replace') {
125+
setCommits(page.commits)
126+
setSkip(page.commits.length)
127+
}
128+
else {
129+
const seen = new Set(commitsRef.current.map(c => c.hash))
130+
const unique = page.commits.filter((c) => {
131+
if (seen.has(c.hash))
132+
return false
133+
seen.add(c.hash)
134+
return true
135+
})
136+
if (unique.length === 0) {
137+
// Static fallback snapshots can return the same page for any args.
138+
setHasMore(false)
139+
return
140+
}
141+
setCommits(prev => [...prev, ...unique])
142+
setSkip(prev => prev + unique.length)
143+
}
144+
setHasMore(page.hasMore)
145+
}
146+
catch (e) {
147+
setError(e instanceof Error ? e.message : String(e))
148+
}
149+
finally {
150+
setLoading(false)
151+
}
152+
}, [])
153+
154+
useEffect(() => {
155+
if (!rpc)
156+
return
157+
void loadPage(rpc, 0, 'replace')
158+
}, [rpc, loadPage])
159+
160+
const refresh = useCallback(async () => {
161+
if (!rpc)
162+
return
163+
await loadPage(rpc, 0, 'replace')
164+
}, [rpc, loadPage])
165+
166+
const loadMore = useCallback(async () => {
167+
if (!rpc)
168+
return
169+
await loadPage(rpc, skip, 'append')
170+
}, [rpc, skip, loadPage])
107171

108172
const graph = useMemo(
109-
() => computeGraph(data?.commits ?? []),
110-
[data?.commits],
173+
() => computeGraph(commits),
174+
[commits],
111175
)
112176
const gutter = Math.max(graph.columns, 1) * COL_W + COL_W / 2
177+
const liveBackend = rpc?.connectionMeta.backend === 'websocket'
113178

114179
return (
115180
<div className="space-y-3">
116181
<div className="flex items-center justify-between">
117182
<span className="text-muted-foreground text-xs">
118-
{data?.isRepo ? `${data.commits.length} commits` : ' '}
183+
{isRepo ? `${commits.length} commits` : ' '}
119184
</span>
120-
<Button variant="ghost" size="icon" onClick={refresh} disabled={loading} aria-label="Refresh log">
121-
<RefreshCw className={loading ? 'animate-spin' : ''} />
185+
<Button variant="ghost" size="icon" className="size-7" onClick={refresh} disabled={loading} aria-label="Refresh log">
186+
<RefreshCw className={`size-3.5 ${loading ? 'animate-spin' : ''}`} />
122187
</Button>
123188
</div>
124189

125-
{!data && (
190+
{!rpc && (
126191
<div className="space-y-2">
127192
{Array.from({ length: 5 }).map((_, i) => <Skeleton key={i} className="h-10 w-full" />)}
128193
</div>
129194
)}
130195

131-
{data && !data.isRepo && (
196+
{error && (
197+
<p className="text-destructive text-sm">{error}</p>
198+
)}
199+
200+
{isRepo === false && (
132201
<p className="text-muted-foreground text-sm">The working directory is not a git repository.</p>
133202
)}
134203

135-
{data?.isRepo && data.commits.length === 0 && (
204+
{isRepo === true && commits.length === 0 && (
136205
<p className="text-muted-foreground text-sm">No commits yet.</p>
137206
)}
138207

139-
{data?.isRepo && data.commits.length > 0 && (
140-
<ScrollArea className="h-96 pr-3">
208+
{isRepo === true && commits.length > 0 && (
209+
<ScrollArea className="h-80 pr-3">
141210
<ul>
142-
{data.commits.map((commit, i) => (
211+
{commits.map((commit, i) => (
143212
<CommitRow key={commit.hash} commit={commit} row={graph.rows[i]} gutter={gutter} />
144213
))}
145214
</ul>
146215
</ScrollArea>
147216
)}
148217

149-
{data?.hasMore && (
150-
<Button variant="outline" size="sm" className="w-full" onClick={() => setLimit(l => l + PAGE)} disabled={loading}>
218+
{isRepo === true && hasMore && (
219+
<Button variant="outline" size="sm" className="w-full" onClick={loadMore} disabled={loading || !liveBackend}>
151220
Load more
152221
</Button>
153222
)}
223+
224+
{isRepo === true && hasMore && !liveBackend && (
225+
<p className="text-muted-foreground text-xs">Load more is available in live mode.</p>
226+
)}
154227
</div>
155228
)
156229
}

plugins/git/src/client/components/status-panel.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,7 @@ export function StatusPanel() {
155155
)
156156

157157
return (
158-
<div className="space-y-4">
158+
<div className="space-y-3">
159159
<div className="flex items-center justify-between gap-2">
160160
<div className="flex flex-wrap items-center gap-2">
161161
{data?.isRepo
@@ -194,8 +194,8 @@ export function StatusPanel() {
194194
)
195195
: <Skeleton className="h-5 w-40" />}
196196
</div>
197-
<Button variant="ghost" size="icon" onClick={refresh} disabled={loading || busy} aria-label="Refresh status">
198-
<RefreshCw className={loading ? 'animate-spin' : ''} />
197+
<Button variant="ghost" size="icon" className="size-7" onClick={refresh} disabled={loading || busy} aria-label="Refresh status">
198+
<RefreshCw className={`size-3.5 ${loading ? 'animate-spin' : ''}`} />
199199
</Button>
200200
</div>
201201

0 commit comments

Comments
 (0)