Skip to content

Commit 8bf8fc6

Browse files
committed
cp dines
1 parent 0204532 commit 8bf8fc6

7 files changed

Lines changed: 289 additions & 151 deletions

File tree

src/cli.ts

Lines changed: 9 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -79,12 +79,9 @@ const devbox = program
7979
.description("Manage devboxes")
8080
.alias("d")
8181
.action(async () => {
82-
// Open interactive devbox list when no subcommand provided
83-
const { runInteractiveCommand } = await import(
84-
"./utils/interactiveCommand.js"
85-
);
86-
const { listDevboxes } = await import("./commands/devbox/list.js");
87-
await runInteractiveCommand(() => listDevboxes({ output: "interactive" }));
82+
// Open interactive devbox list using the Router architecture
83+
const { runMainMenu } = await import("./commands/menu.js");
84+
await runMainMenu("devbox-list");
8885
});
8986

9087
devbox
@@ -350,12 +347,9 @@ const snapshot = program
350347
.description("Manage devbox snapshots")
351348
.alias("snap")
352349
.action(async () => {
353-
// Open interactive snapshot list when no subcommand provided
354-
const { runInteractiveCommand } = await import(
355-
"./utils/interactiveCommand.js"
356-
);
357-
const { listSnapshots } = await import("./commands/snapshot/list.js");
358-
await runInteractiveCommand(() => listSnapshots({ output: "interactive" }));
350+
// Open interactive snapshot list using the Router architecture
351+
const { runMainMenu } = await import("./commands/menu.js");
352+
await runMainMenu("snapshot-list");
359353
});
360354

361355
snapshot
@@ -415,14 +409,9 @@ const blueprint = program
415409
.description("Manage blueprints")
416410
.alias("bp")
417411
.action(async () => {
418-
// Open interactive blueprint list when no subcommand provided
419-
const { runInteractiveCommand } = await import(
420-
"./utils/interactiveCommand.js"
421-
);
422-
const { listBlueprints } = await import("./commands/blueprint/list.js");
423-
await runInteractiveCommand(() =>
424-
listBlueprints({ output: "interactive" }),
425-
);
412+
// Open interactive blueprint list using the Router architecture
413+
const { runMainMenu } = await import("./commands/menu.js");
414+
await runMainMenu("blueprint-list");
426415
});
427416

428417
blueprint

src/commands/devbox/list.tsx

Lines changed: 44 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,13 @@ import { formatTimeAgo } from "../../components/ResourceListView.js";
1313
import { createExecutor } from "../../utils/CommandExecutor.js";
1414
import { DevboxDetailPage } from "../../components/DevboxDetailPage.js";
1515
import { DevboxCreatePage } from "../../components/DevboxCreatePage.js";
16-
import { DevboxActionsMenu } from "../../components/DevboxActionsMenu.js";
1716
import { ResourceActionsMenu } from "../../components/ResourceActionsMenu.js";
1817
import { ActionsPopup } from "../../components/ActionsPopup.js";
1918
import { getDevboxUrl } from "../../utils/url.js";
2019
import { useViewportHeight } from "../../hooks/useViewportHeight.js";
2120
import { useExitOnCtrlC } from "../../hooks/useExitOnCtrlC.js";
2221
import { colors } from "../../utils/theme.js";
22+
import { useDevboxStore } from "../../store/devboxStore.js";
2323

2424
interface ListOptions {
2525
status?: string;
@@ -31,14 +31,14 @@ const MAX_CACHE_SIZE = 10; // Limit cache to 10 pages to prevent memory leaks
3131

3232
const 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
955965
export { 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
}

src/components/DevboxDetailPage.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ import { getDevboxUrl } from "../utils/url.js";
1010
import { colors } from "../utils/theme.js";
1111
import { useViewportHeight } from "../hooks/useViewportHeight.js";
1212
import { useExitOnCtrlC } from "../hooks/useExitOnCtrlC.js";
13-
import { useNavigation, type ScreenName, type RouteParams } from "../store/navigationStore.js";
1413
import { getDevbox } from "../services/devboxService.js";
1514

1615
interface DevboxDetailPageProps {

src/screens/DevboxDetailScreen.tsx

Lines changed: 72 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -6,35 +6,90 @@ import React from "react";
66
import { useNavigation } from "../store/navigationStore.js";
77
import { useDevboxStore } from "../store/devboxStore.js";
88
import { DevboxDetailPage } from "../components/DevboxDetailPage.js";
9+
import { getDevbox } from "../services/devboxService.js";
10+
import { SpinnerComponent } from "../components/Spinner.js";
11+
import { ErrorMessage } from "../components/ErrorMessage.js";
12+
import { Breadcrumb } from "../components/Breadcrumb.js";
913

1014
interface DevboxDetailScreenProps {
1115
devboxId?: string;
1216
}
1317

14-
export function DevboxDetailScreen({
15-
devboxId,
16-
}: DevboxDetailScreenProps) {
18+
export function DevboxDetailScreen({ devboxId }: DevboxDetailScreenProps) {
1719
const { goBack } = useNavigation();
1820
const devboxes = useDevboxStore((state) => state.devboxes);
21+
const setDevboxesInStore = useDevboxStore((state) => state.setDevboxes);
1922

20-
// Find devbox in store first, otherwise we'd need to fetch it
21-
const devbox = devboxes.find((d) => d.id === devboxId);
23+
const [loading, setLoading] = React.useState(false);
24+
const [error, setError] = React.useState<Error | null>(null);
25+
const [fetchedDevbox, setFetchedDevbox] = React.useState<any>(null);
2226

23-
// Navigate back if devbox not found - must be in useEffect, not during render
27+
// Find devbox in store first
28+
const devboxFromStore = devboxes.find((d) => d.id === devboxId);
29+
30+
// Fetch devbox from API if not in store
2431
React.useEffect(() => {
25-
if (!devbox) {
26-
goBack();
32+
if (!devboxFromStore && devboxId && !loading && !fetchedDevbox) {
33+
setLoading(true);
34+
setError(null);
35+
36+
getDevbox(devboxId)
37+
.then((devbox) => {
38+
setFetchedDevbox(devbox);
39+
// Cache it in store for future access
40+
setDevboxesInStore([devbox]);
41+
setLoading(false);
42+
})
43+
.catch((err) => {
44+
setError(err as Error);
45+
setLoading(false);
46+
});
2747
}
28-
}, [devbox, goBack]);
48+
}, [devboxFromStore, devboxId, loading, fetchedDevbox, setDevboxesInStore]);
49+
50+
// Use devbox from store or fetched devbox
51+
const devbox = devboxFromStore || fetchedDevbox;
52+
53+
// Show loading state while fetching
54+
if (loading) {
55+
return (
56+
<>
57+
<Breadcrumb
58+
items={[{ label: "Devboxes" }, { label: "Loading...", active: true }]}
59+
/>
60+
<SpinnerComponent message="Loading devbox details..." />
61+
</>
62+
);
63+
}
64+
65+
// Show error state if fetch failed
66+
if (error) {
67+
return (
68+
<>
69+
<Breadcrumb
70+
items={[{ label: "Devboxes" }, { label: "Error", active: true }]}
71+
/>
72+
<ErrorMessage message="Failed to load devbox details" error={error} />
73+
</>
74+
);
75+
}
2976

30-
if (!devbox) {
31-
return null;
77+
// Show error if no devbox found and not loading
78+
if (!devbox && !loading) {
79+
return (
80+
<>
81+
<Breadcrumb
82+
items={[{ label: "Devboxes" }, { label: "Not Found", active: true }]}
83+
/>
84+
<ErrorMessage
85+
message={`Devbox ${devboxId || "unknown"} not found`}
86+
error={
87+
new Error("Devbox not found in cache and could not be fetched")
88+
}
89+
/>
90+
</>
91+
);
3292
}
3393

34-
return (
35-
<DevboxDetailPage
36-
devbox={devbox}
37-
onBack={goBack}
38-
/>
39-
);
94+
return <DevboxDetailPage devbox={devbox} onBack={goBack} />;
4095
}

0 commit comments

Comments
 (0)