@@ -4,18 +4,18 @@ import type { DevframeRpcClient } from 'devframe/client'
44import type { Commit } from '../../index'
55import type { GraphRow } from '../lib/commit-graph'
66import { RefreshCw } from 'lucide-react'
7- import { useCallback , useMemo , useState } from 'react'
7+ import { useCallback , useEffect , useMemo , useRef , useState } from 'react'
88import { computeGraph } from '../lib/commit-graph'
9+ import { useRpc } from './rpc-provider'
910import { Badge } from './ui/badge'
1011import { Button } from './ui/button'
1112import { ScrollArea } from './ui/scroll-area'
1213import { Skeleton } from './ui/skeleton'
13- import { useRpcResource } from './use-rpc-resource'
1414
1515const 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
2020function 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
100100export 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}
0 commit comments