@@ -45,7 +45,6 @@ interface ListOptions {
4545 output ?: string ;
4646}
4747
48- const MAX_FETCH = 100 ;
4948const DEFAULT_PAGE_SIZE = 10 ;
5049
5150const ListDevboxesUI : React . FC < { status ?: string } > = ( { status } ) => {
@@ -65,6 +64,10 @@ const ListDevboxesUI: React.FC<{ status?: string }> = ({ status }) => {
6564 const [ refreshIcon , setRefreshIcon ] = React . useState ( 0 ) ;
6665 const [ searchMode , setSearchMode ] = React . useState ( false ) ;
6766 const [ searchQuery , setSearchQuery ] = React . useState ( '' ) ;
67+ const [ totalCount , setTotalCount ] = React . useState ( 0 ) ;
68+ const [ hasMore , setHasMore ] = React . useState ( false ) ;
69+ const pageCache = React . useRef < Map < number , any [ ] > > ( new Map ( ) ) ;
70+ const lastIdCache = React . useRef < Map < number , string > > ( new Map ( ) ) ;
6871
6972 // Calculate responsive dimensions
7073 const terminalWidth = stdout ?. columns || 120 ;
@@ -121,24 +124,63 @@ const ListDevboxesUI: React.FC<{ status?: string }> = ({ status }) => {
121124 if ( isInitialLoad ) {
122125 setRefreshing ( true ) ;
123126 }
127+
128+ // Check if we have cached data for this page
129+ if ( ! isInitialLoad && pageCache . current . has ( currentPage ) ) {
130+ setDevboxes ( pageCache . current . get ( currentPage ) || [ ] ) ;
131+ setLoading ( false ) ;
132+ return ;
133+ }
134+
124135 const client = getClient ( ) ;
125- const allDevboxes : any [ ] = [ ] ;
136+ const pageDevboxes : any [ ] = [ ] ;
126137
138+ // Get starting_after cursor from previous page's last ID
139+ const startingAfter = currentPage > 0 ? lastIdCache . current . get ( currentPage - 1 ) : undefined ;
140+
141+ // Build query params
142+ const queryParams : any = {
143+ limit : PAGE_SIZE ,
144+ } ;
145+ if ( startingAfter ) {
146+ queryParams . starting_after = startingAfter ;
147+ }
148+ if ( status ) {
149+ queryParams . status = status as 'provisioning' | 'initializing' | 'running' | 'suspending' | 'suspended' | 'resuming' | 'failure' | 'shutdown' ;
150+ }
151+
152+ // Fetch only the current page
153+ const page = await client . devboxes . list ( queryParams ) ;
154+
155+ // Collect items from the page - only get PAGE_SIZE items, don't auto-paginate
127156 let count = 0 ;
128- for await ( const devbox of client . devboxes . list ( ) ) {
129- if ( ! status || devbox . status === status ) {
130- allDevboxes . push ( devbox ) ;
131- }
157+ for await ( const devbox of page ) {
158+ pageDevboxes . push ( devbox ) ;
132159 count ++ ;
133- if ( count >= MAX_FETCH ) {
160+ // Break after getting PAGE_SIZE items to prevent auto-pagination
161+ if ( count >= PAGE_SIZE ) {
134162 break ;
135163 }
136164 }
137165
138- // Only update if data actually changed
166+ // Update pagination metadata from the page object
167+ // These properties are on the page object itself
168+ const total = ( page as any ) . total_count || pageDevboxes . length ;
169+ const more = ( page as any ) . has_more || false ;
170+
171+ setTotalCount ( total ) ;
172+ setHasMore ( more ) ;
173+
174+ // Cache the page data and last ID
175+ if ( pageDevboxes . length > 0 ) {
176+ pageCache . current . set ( currentPage , pageDevboxes ) ;
177+ lastIdCache . current . set ( currentPage , pageDevboxes [ pageDevboxes . length - 1 ] . id ) ;
178+ }
179+
180+ // Update devboxes for current page
139181 setDevboxes ( ( prev ) => {
140- const hasChanged = JSON . stringify ( prev ) !== JSON . stringify ( allDevboxes ) ;
141- return hasChanged ? allDevboxes : prev ;
182+ const hasChanged = JSON . stringify ( prev ) !== JSON . stringify ( pageDevboxes ) ;
183+ return hasChanged ? pageDevboxes : prev ;
142184 } ) ;
143185 } catch ( err ) {
144186 setError ( err as Error ) ;
@@ -156,12 +198,15 @@ const ListDevboxesUI: React.FC<{ status?: string }> = ({ status }) => {
156198 // Poll every 3 seconds (increased from 2), but only when in list view
157199 const interval = setInterval ( ( ) => {
158200 if ( ! showDetails && ! showCreate && ! showActions ) {
201+ // Clear cache on refresh to get latest data
202+ pageCache . current . clear ( ) ;
203+ lastIdCache . current . clear ( ) ;
159204 list ( false ) ;
160205 }
161206 } , 3000 ) ;
162207
163208 return ( ) => clearInterval ( interval ) ;
164- } , [ showDetails , showCreate , showActions ] ) ;
209+ } , [ showDetails , showCreate , showActions , currentPage ] ) ;
165210
166211 // Animate refresh icon only when in list view
167212 React . useEffect ( ( ) => {
@@ -282,7 +327,7 @@ const ListDevboxesUI: React.FC<{ status?: string }> = ({ status }) => {
282327 }
283328 } ) ;
284329
285- // Filter devboxes based on search query
330+ // Filter devboxes based on search query (client-side only for current page)
286331 const filteredDevboxes = React . useMemo ( ( ) => {
287332 if ( ! searchQuery . trim ( ) ) return devboxes ;
288333
@@ -301,12 +346,15 @@ const ListDevboxesUI: React.FC<{ status?: string }> = ({ status }) => {
301346 [ 'stopped' , 'suspended' ] . includes ( d . status )
302347 ) . length ;
303348
304- const totalPages = Math . ceil ( filteredDevboxes . length / PAGE_SIZE ) ;
305- const startIndex = currentPage * PAGE_SIZE ;
306- const endIndex = Math . min ( startIndex + PAGE_SIZE , filteredDevboxes . length ) ;
307- const currentDevboxes = filteredDevboxes . slice ( startIndex , endIndex ) ;
349+ // Current page is already fetched, no need to slice
350+ const currentDevboxes = filteredDevboxes ;
308351 const selectedDevbox = currentDevboxes [ selectedIndex ] ;
309352
353+ // Calculate pagination info
354+ const totalPages = Math . ceil ( totalCount / PAGE_SIZE ) ;
355+ const startIndex = currentPage * PAGE_SIZE ;
356+ const endIndex = startIndex + currentDevboxes . length ;
357+
310358 // Filter operations based on devbox status
311359 const operations = selectedDevbox ? allOperations . filter ( op => {
312360 const status = selectedDevbox . status ;
@@ -384,7 +432,7 @@ const ListDevboxesUI: React.FC<{ status?: string }> = ({ status }) => {
384432 data = { currentDevboxes }
385433 keyExtractor = { ( devbox : any ) => devbox . id }
386434 selectedIndex = { selectedIndex }
387- title = { `devboxes[${ devboxes . length } ]` }
435+ title = { `devboxes[${ totalCount } ]` }
388436 columns = { [
389437 {
390438 key : 'statusIcon' ,
@@ -524,7 +572,7 @@ const ListDevboxesUI: React.FC<{ status?: string }> = ({ status }) => {
524572 data = { currentDevboxes }
525573 keyExtractor = { ( devbox : any ) => devbox . id }
526574 selectedIndex = { selectedIndex }
527- title = { `devboxes[${ devboxes . length } ]` }
575+ title = { `devboxes[${ totalCount } ]` }
528576 columns = { [
529577 {
530578 key : 'statusIcon' ,
@@ -605,8 +653,7 @@ const ListDevboxesUI: React.FC<{ status?: string }> = ({ status }) => {
605653 { /* Statistics Bar */ }
606654 < Box marginTop = { 1 } paddingX = { 1 } >
607655 < Text color = "cyan" bold >
608- { figures . hamburger } { devboxes . length }
609- { devboxes . length >= MAX_FETCH && '+' }
656+ { figures . hamburger } { totalCount }
610657 </ Text >
611658 < Text color = "gray" dimColor > total</ Text >
612659 { totalPages > 1 && (
@@ -619,8 +666,11 @@ const ListDevboxesUI: React.FC<{ status?: string }> = ({ status }) => {
619666 ) }
620667 < Text color = "gray" dimColor > • </ Text >
621668 < Text color = "gray" dimColor >
622- Showing { startIndex + 1 } -{ endIndex } of { devboxes . length }
669+ Showing { startIndex + 1 } -{ endIndex } of { totalCount }
623670 </ Text >
671+ { hasMore && (
672+ < Text color = "gray" dimColor > (more available)</ Text >
673+ ) }
624674 < Text > </ Text >
625675 { refreshing ? (
626676 < Text color = "cyan" >
0 commit comments