@@ -13,13 +13,13 @@ import { formatTimeAgo } from "../../components/ResourceListView.js";
1313import { createExecutor } from "../../utils/CommandExecutor.js" ;
1414import { DevboxDetailPage } from "../../components/DevboxDetailPage.js" ;
1515import { DevboxCreatePage } from "../../components/DevboxCreatePage.js" ;
16- import { DevboxActionsMenu } from "../../components/DevboxActionsMenu.js" ;
1716import { ResourceActionsMenu } from "../../components/ResourceActionsMenu.js" ;
1817import { ActionsPopup } from "../../components/ActionsPopup.js" ;
1918import { getDevboxUrl } from "../../utils/url.js" ;
2019import { useViewportHeight } from "../../hooks/useViewportHeight.js" ;
2120import { useExitOnCtrlC } from "../../hooks/useExitOnCtrlC.js" ;
2221import { colors } from "../../utils/theme.js" ;
22+ import { useDevboxStore } from "../../store/devboxStore.js" ;
2323
2424interface ListOptions {
2525 status ?: string ;
@@ -31,14 +31,14 @@ const MAX_CACHE_SIZE = 10; // Limit cache to 10 pages to prevent memory leaks
3131
3232const ListDevboxesUI = ( {
3333 status,
34- focusDevboxId,
3534 onBack,
3635 onExit,
36+ onNavigateToDetail,
3737} : {
3838 status ?: string ;
39- focusDevboxId ?: string ;
4039 onBack ?: ( ) => void ;
4140 onExit ?: ( ) => void ;
41+ onNavigateToDetail ?: ( devboxId : string ) => void ;
4242} ) => {
4343 const { exit : inkExit } = useApp ( ) ;
4444 const [ initialLoading , setInitialLoading ] = React . useState ( true ) ;
@@ -56,11 +56,13 @@ const ListDevboxesUI = ({
5656 const [ searchMode , setSearchMode ] = React . useState ( false ) ;
5757 const [ searchQuery , setSearchQuery ] = React . useState ( "" ) ;
5858 const [ totalCount , setTotalCount ] = React . useState ( 0 ) ;
59- const [ hasMore , setHasMore ] = React . useState ( false ) ;
6059 // eslint-disable-next-line @typescript-eslint/no-explicit-any
6160 const pageCache = React . useRef < Map < number , any [ ] > > ( new Map ( ) ) ;
6261 const lastIdCache = React . useRef < Map < number , string > > ( new Map ( ) ) ;
6362
63+ // Get devbox store setter to sync data for detail screen
64+ const setDevboxesInStore = useDevboxStore ( ( state ) => state . setDevboxes ) ;
65+
6466 // Calculate overhead for viewport height:
6567 // - Breadcrumb (3 lines + marginBottom): 4 lines
6668 // - Search bar (if visible, 1 line + marginBottom): 2 lines
@@ -334,17 +336,8 @@ const ListDevboxesUI = ({
334336 [ ] ,
335337 ) ;
336338
337- // Check if we need to focus on a specific devbox after returning from SSH
338- React . useEffect ( ( ) => {
339- if ( focusDevboxId && devboxes . length > 0 && ! initialLoading ) {
340- // Find the devbox in the current page
341- const devboxIndex = devboxes . findIndex ( ( d ) => d . id === focusDevboxId ) ;
342- if ( devboxIndex !== - 1 ) {
343- setSelectedIndex ( devboxIndex ) ;
344- setShowDetails ( true ) ;
345- }
346- }
347- } , [ devboxes , initialLoading , focusDevboxId ] ) ;
339+ // NOTE: focusDevboxId auto-navigation removed - now handled by Router in DevboxListScreen
340+ // The Router will navigate directly to devbox-detail screen instead of relying on internal state
348341
349342 // Clear cache when search query changes
350343 React . useEffect ( ( ) => {
@@ -446,16 +439,22 @@ const ListDevboxesUI = ({
446439 // Extract data immediately and create defensive copies
447440 // This breaks all reference chains to the SDK's internal objects
448441 if ( page . devboxes && Array . isArray ( page . devboxes ) ) {
449- // Copy ONLY the fields we need - don't hold entire SDK objects
442+ // Deep copy all fields to avoid SDK references
450443 page . devboxes . forEach ( ( d : any ) => {
451- pageDevboxes . push ( {
452- id : d . id ,
453- name : d . name ,
454- status : d . status ,
455- create_time_ms : d . create_time_ms ,
456- blueprint_id : d . blueprint_id ,
457- entitlements : d . entitlements ? { ...d . entitlements } : undefined ,
458- } ) ;
444+ const plain : any = { } ;
445+ for ( const key in d ) {
446+ const value = d [ key ] ;
447+ if ( value === null || value === undefined ) {
448+ plain [ key ] = value ;
449+ } else if ( Array . isArray ( value ) ) {
450+ plain [ key ] = [ ...value ] ;
451+ } else if ( typeof value === "object" ) {
452+ plain [ key ] = JSON . parse ( JSON . stringify ( value ) ) ;
453+ } else {
454+ plain [ key ] = value ;
455+ }
456+ }
457+ pageDevboxes . push ( plain ) ;
459458 } ) ;
460459 } else {
461460 console . error (
@@ -466,7 +465,6 @@ const ListDevboxesUI = ({
466465
467466 // Extract metadata before releasing page reference
468467 const totalCount = page . total_count || pageDevboxes . length ;
469- const hasMore = page . has_more || false ;
470468
471469 // CRITICAL: Explicitly null out page reference to help GC
472470 // The Page object holds references to client, response, and options
@@ -477,7 +475,6 @@ const ListDevboxesUI = ({
477475
478476 // Update pagination metadata
479477 setTotalCount ( totalCount ) ;
480- setHasMore ( hasMore ) ;
481478
482479 // Cache the page data and last ID
483480 if ( pageDevboxes . length > 0 ) {
@@ -499,6 +496,9 @@ const ListDevboxesUI = ({
499496 // Update devboxes for current page
500497 // React will handle efficient re-rendering - no need for manual comparison
501498 setDevboxes ( pageDevboxes ) ;
499+
500+ // Also update the store so DevboxDetailScreen can access the data
501+ setDevboxesInStore ( pageDevboxes ) ;
502502 } catch ( err ) {
503503 if ( isMounted ) {
504504 setError ( err as Error ) ;
@@ -632,7 +632,12 @@ const ListDevboxesUI = ({
632632 setCurrentPage ( currentPage - 1 ) ;
633633 setSelectedIndex ( 0 ) ;
634634 } else if ( key . return ) {
635- setShowDetails ( true ) ;
635+ // Use Router navigation if callback provided, otherwise use internal state
636+ if ( onNavigateToDetail && selectedDevbox ) {
637+ onNavigateToDetail ( selectedDevbox . id ) ;
638+ } else {
639+ setShowDetails ( true ) ;
640+ }
636641 } else if ( input === "a" ) {
637642 setShowPopup ( true ) ;
638643 setSelectedOperation ( 0 ) ;
@@ -726,13 +731,18 @@ const ListDevboxesUI = ({
726731 } )
727732 : allOperations ;
728733
729- // CRITICAL: Aggressive memory cleanup when switching views to prevent heap exhaustion
734+ // CRITICAL: Memory cleanup when switching views
735+ // Only clear LOCAL component state, NOT the store (store is needed by detail screen)
730736 React . useEffect ( ( ) => {
731737 if ( showDetails || showActions || showCreate ) {
732- // Immediately clear list data when navigating away to free memory
733- setDevboxes ( [ ] ) ;
738+ // Clear local list data only when using internal navigation
739+ // When using Router navigation (onNavigateToDetail), the component will unmount
740+ // so this cleanup is not needed
741+ if ( ! onNavigateToDetail ) {
742+ setDevboxes ( [ ] ) ;
743+ }
734744 }
735- } , [ showDetails , showActions , showCreate ] ) ;
745+ } , [ showDetails , showActions , showCreate , onNavigateToDetail ] ) ;
736746
737747 // Create view
738748 if ( showCreate ) {
@@ -741,7 +751,7 @@ const ListDevboxesUI = ({
741751 onBack = { ( ) => {
742752 setShowCreate ( false ) ;
743753 } }
744- onCreate = { ( devbox ) => {
754+ onCreate = { ( ) => {
745755 // Refresh the list after creation
746756 setShowCreate ( false ) ;
747757 // The list will auto-refresh via the polling effect
@@ -954,10 +964,7 @@ const ListDevboxesUI = ({
954964// Export the UI component for use in the main menu
955965export { ListDevboxesUI } ;
956966
957- export async function listDevboxes (
958- options : ListOptions ,
959- focusDevboxId ?: string ,
960- ) {
967+ export async function listDevboxes ( options : ListOptions ) {
961968 const executor = createExecutor ( options ) ;
962969
963970 await executor . executeList (
@@ -970,9 +977,7 @@ export async function listDevboxes(
970977 limit : DEFAULT_PAGE_SIZE ,
971978 } ) ;
972979 } ,
973- ( ) => (
974- < ListDevboxesUI status = { options . status } focusDevboxId = { focusDevboxId } />
975- ) ,
980+ ( ) => < ListDevboxesUI status = { options . status } /> ,
976981 DEFAULT_PAGE_SIZE ,
977982 ) ;
978983}
0 commit comments