Skip to content

Commit 76b2b45

Browse files
committed
cp dines
1 parent 03e4f49 commit 76b2b45

2 files changed

Lines changed: 215 additions & 48 deletions

File tree

src/commands/snapshot/list.tsx

Lines changed: 209 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,14 @@ import { Box, Text, useInput, useApp } from "ink";
33
import figures from "figures";
44
import type { DiskSnapshotsCursorIDPage } from "@runloop/api-client/pagination";
55
import { getClient } from "../../utils/client.js";
6+
import { Header } from "../../components/Header.js";
67
import { SpinnerComponent } from "../../components/Spinner.js";
78
import { ErrorMessage } from "../../components/ErrorMessage.js";
9+
import { SuccessMessage } from "../../components/SuccessMessage.js";
810
import { Breadcrumb } from "../../components/Breadcrumb.js";
911
import { Table, createTextColumn } from "../../components/Table.js";
12+
import { ActionsPopup } from "../../components/ActionsPopup.js";
13+
import { Operation } from "../../components/OperationsMenu.js";
1014
import { formatTimeAgo } from "../../components/ResourceListView.js";
1115
import { createExecutor } from "../../utils/CommandExecutor.js";
1216
import { colors } from "../../utils/theme.js";
@@ -32,6 +36,14 @@ const ListSnapshotsUI = ({
3236
}) => {
3337
const { exit: inkExit } = useApp();
3438
const [selectedIndex, setSelectedIndex] = React.useState(0);
39+
const [showPopup, setShowPopup] = React.useState(false);
40+
const [selectedOperation, setSelectedOperation] = React.useState(0);
41+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
42+
const [selectedSnapshot, setSelectedSnapshot] = React.useState<any | null>(null);
43+
const [executingOperation, setExecutingOperation] = React.useState<string | null>(null);
44+
const [operationResult, setOperationResult] = React.useState<string | null>(null);
45+
const [operationError, setOperationError] = React.useState<Error | null>(null);
46+
const [operationLoading, setOperationLoading] = React.useState(false);
3547

3648
// Calculate overhead for viewport height
3749
const overhead = 13;
@@ -47,7 +59,7 @@ const ListSnapshotsUI = ({
4759
const nameWidth = Math.max(15, terminalWidth >= 120 ? 30 : 25);
4860
const devboxWidth = 15;
4961
const timeWidth = 20;
50-
const showDevboxId = terminalWidth >= 100 && !devboxId;
62+
const showDevboxIdColumn = terminalWidth >= 100 && !devboxId;
5163

5264
// Fetch function for pagination hook
5365
const fetchPage = React.useCallback(
@@ -117,9 +129,23 @@ const ListSnapshotsUI = ({
117129
pageSize: PAGE_SIZE,
118130
getItemId: (snapshot: any) => snapshot.id,
119131
pollInterval: 2000,
132+
pollingEnabled: !showPopup && !executingOperation,
120133
deps: [devboxId, PAGE_SIZE],
121134
});
122135

136+
// Operations for snapshots
137+
const operations: Operation[] = React.useMemo(
138+
() => [
139+
{
140+
key: "delete",
141+
label: "Delete Snapshot",
142+
color: colors.error,
143+
icon: figures.cross,
144+
},
145+
],
146+
[],
147+
);
148+
123149
// Build columns
124150
const columns = React.useMemo(
125151
() => [
@@ -146,7 +172,7 @@ const ListSnapshotsUI = ({
146172
color: colors.idColor,
147173
dimColor: false,
148174
bold: false,
149-
visible: showDevboxId,
175+
visible: showDevboxIdColumn,
150176
},
151177
),
152178
createTextColumn(
@@ -162,7 +188,7 @@ const ListSnapshotsUI = ({
162188
},
163189
),
164190
],
165-
[idWidth, nameWidth, devboxWidth, timeWidth, showDevboxId],
191+
[idWidth, nameWidth, devboxWidth, timeWidth, showDevboxIdColumn],
166192
);
167193

168194
// Handle Ctrl+C to exit
@@ -175,12 +201,72 @@ const ListSnapshotsUI = ({
175201
}
176202
}, [snapshots.length, selectedIndex]);
177203

204+
const selectedSnapshotItem = snapshots[selectedIndex];
205+
178206
// Calculate pagination info for display
179207
const totalPages = Math.max(1, Math.ceil(totalCount / PAGE_SIZE));
180208
const startIndex = currentPage * PAGE_SIZE;
181209
const endIndex = startIndex + snapshots.length;
182210

211+
const executeOperation = async () => {
212+
const client = getClient();
213+
const snapshot = selectedSnapshot;
214+
215+
if (!snapshot) return;
216+
217+
try {
218+
setOperationLoading(true);
219+
switch (executingOperation) {
220+
case "delete":
221+
await client.devboxes.deleteDiskSnapshot(snapshot.id);
222+
setOperationResult(`Snapshot ${snapshot.id} deleted successfully`);
223+
break;
224+
}
225+
} catch (err) {
226+
setOperationError(err as Error);
227+
} finally {
228+
setOperationLoading(false);
229+
}
230+
};
231+
183232
useInput((input, key) => {
233+
// Handle operation result display
234+
if (operationResult || operationError) {
235+
if (input === "q" || key.escape || key.return) {
236+
setOperationResult(null);
237+
setOperationError(null);
238+
setExecutingOperation(null);
239+
setSelectedSnapshot(null);
240+
}
241+
return;
242+
}
243+
244+
// Handle popup navigation
245+
if (showPopup) {
246+
if (key.upArrow && selectedOperation > 0) {
247+
setSelectedOperation(selectedOperation - 1);
248+
} else if (key.downArrow && selectedOperation < operations.length - 1) {
249+
setSelectedOperation(selectedOperation + 1);
250+
} else if (key.return) {
251+
setShowPopup(false);
252+
const operationKey = operations[selectedOperation].key;
253+
setSelectedSnapshot(selectedSnapshotItem);
254+
setExecutingOperation(operationKey);
255+
// Execute immediately after state update
256+
setTimeout(() => executeOperation(), 0);
257+
} else if (key.escape || input === "q") {
258+
setShowPopup(false);
259+
setSelectedOperation(0);
260+
} else if (input === "d") {
261+
// Delete hotkey
262+
setShowPopup(false);
263+
setSelectedSnapshot(selectedSnapshotItem);
264+
setExecutingOperation("delete");
265+
setTimeout(() => executeOperation(), 0);
266+
}
267+
return;
268+
}
269+
184270
const pageSnapshots = snapshots.length;
185271

186272
// Handle list view navigation
@@ -194,6 +280,9 @@ const ListSnapshotsUI = ({
194280
} else if ((input === "p" || key.leftArrow) && !loading && !navigating && hasPrev) {
195281
prevPage();
196282
setSelectedIndex(0);
283+
} else if (input === "a" && selectedSnapshotItem) {
284+
setShowPopup(true);
285+
setSelectedOperation(0);
197286
} else if (key.escape) {
198287
if (onBack) {
199288
onBack();
@@ -205,6 +294,57 @@ const ListSnapshotsUI = ({
205294
}
206295
});
207296

297+
// Operation result display
298+
if (operationResult || operationError) {
299+
const operationLabel =
300+
operations.find((o) => o.key === executingOperation)?.label || "Operation";
301+
return (
302+
<>
303+
<Breadcrumb
304+
items={[
305+
{ label: "Snapshots" },
306+
{ label: selectedSnapshot?.name || selectedSnapshot?.id || "Snapshot" },
307+
{ label: operationLabel, active: true },
308+
]}
309+
/>
310+
<Header title="Operation Result" />
311+
{operationResult && <SuccessMessage message={operationResult} />}
312+
{operationError && (
313+
<ErrorMessage message="Operation failed" error={operationError} />
314+
)}
315+
<Box marginTop={1}>
316+
<Text color={colors.textDim} dimColor>
317+
Press [Enter], [q], or [esc] to continue
318+
</Text>
319+
</Box>
320+
</>
321+
);
322+
}
323+
324+
// Operation loading state
325+
if (operationLoading && selectedSnapshot) {
326+
const operationLabel =
327+
operations.find((o) => o.key === executingOperation)?.label || "Operation";
328+
const messages: Record<string, string> = {
329+
delete: "Deleting snapshot...",
330+
};
331+
return (
332+
<>
333+
<Breadcrumb
334+
items={[
335+
{ label: "Snapshots" },
336+
{ label: selectedSnapshot.name || selectedSnapshot.id },
337+
{ label: operationLabel, active: true },
338+
]}
339+
/>
340+
<Header title="Executing Operation" />
341+
<SpinnerComponent
342+
message={messages[executingOperation as string] || "Please wait..."}
343+
/>
344+
</>
345+
);
346+
}
347+
208348
// Loading state
209349
if (loading && snapshots.length === 0) {
210350
return (
@@ -266,49 +406,71 @@ const ListSnapshotsUI = ({
266406
]}
267407
/>
268408

269-
{/* Table */}
270-
<Table
271-
data={snapshots}
272-
keyExtractor={(snapshot: any) => snapshot.id}
273-
selectedIndex={selectedIndex}
274-
title={`snapshots[${totalCount}]`}
275-
columns={columns}
276-
/>
409+
{/* Table - hide when popup is shown */}
410+
{!showPopup && (
411+
<Table
412+
data={snapshots}
413+
keyExtractor={(snapshot: any) => snapshot.id}
414+
selectedIndex={selectedIndex}
415+
title={`snapshots[${totalCount}]`}
416+
columns={columns}
417+
/>
418+
)}
277419

278-
{/* Statistics Bar */}
279-
<Box marginTop={1} paddingX={1}>
280-
<Text color={colors.primary} bold>
281-
{figures.hamburger} {totalCount}
282-
</Text>
283-
<Text color={colors.textDim} dimColor>
284-
{" "}
285-
total
286-
</Text>
287-
{totalPages > 1 && (
288-
<>
289-
<Text color={colors.textDim} dimColor>
290-
{" "}
291-
{" "}
292-
</Text>
293-
{navigating ? (
294-
<Text color={colors.warning}>
295-
{figures.pointer} Loading page {currentPage + 1}...
296-
</Text>
297-
) : (
420+
{/* Statistics Bar - hide when popup is shown */}
421+
{!showPopup && (
422+
<Box marginTop={1} paddingX={1}>
423+
<Text color={colors.primary} bold>
424+
{figures.hamburger} {totalCount}
425+
</Text>
426+
<Text color={colors.textDim} dimColor>
427+
{" "}
428+
total
429+
</Text>
430+
{totalPages > 1 && (
431+
<>
298432
<Text color={colors.textDim} dimColor>
299-
Page {currentPage + 1} of {totalPages}
433+
{" "}
434+
{" "}
300435
</Text>
301-
)}
302-
</>
303-
)}
304-
<Text color={colors.textDim} dimColor>
305-
{" "}
306-
{" "}
307-
</Text>
308-
<Text color={colors.textDim} dimColor>
309-
Showing {startIndex + 1}-{endIndex} of {totalCount}
310-
</Text>
311-
</Box>
436+
{navigating ? (
437+
<Text color={colors.warning}>
438+
{figures.pointer} Loading page {currentPage + 1}...
439+
</Text>
440+
) : (
441+
<Text color={colors.textDim} dimColor>
442+
Page {currentPage + 1} of {totalPages}
443+
</Text>
444+
)}
445+
</>
446+
)}
447+
<Text color={colors.textDim} dimColor>
448+
{" "}
449+
{" "}
450+
</Text>
451+
<Text color={colors.textDim} dimColor>
452+
Showing {startIndex + 1}-{endIndex} of {totalCount}
453+
</Text>
454+
</Box>
455+
)}
456+
457+
{/* Actions Popup */}
458+
{showPopup && selectedSnapshotItem && (
459+
<Box marginTop={2} justifyContent="center">
460+
<ActionsPopup
461+
devbox={selectedSnapshotItem}
462+
operations={operations.map((op) => ({
463+
key: op.key,
464+
label: op.label,
465+
color: op.color,
466+
icon: op.icon,
467+
shortcut: op.key === "delete" ? "d" : "",
468+
}))}
469+
selectedOperation={selectedOperation}
470+
onClose={() => setShowPopup(false)}
471+
/>
472+
</Box>
473+
)}
312474

313475
{/* Help Bar */}
314476
<Box marginTop={1} paddingX={1}>
@@ -323,6 +485,10 @@ const ListSnapshotsUI = ({
323485
{figures.arrowRight} Page
324486
</Text>
325487
)}
488+
<Text color={colors.textDim} dimColor>
489+
{" "}
490+
• [a] Actions
491+
</Text>
326492
<Text color={colors.textDim} dimColor>
327493
{" "}
328494
• [Esc] Back

src/screens/SSHSessionScreen.tsx

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import { colors } from "../utils/theme.js";
1515
import figures from "figures";
1616

1717
export function SSHSessionScreen() {
18-
const { params, navigate } = useNavigation();
18+
const { params, replace } = useNavigation();
1919

2020
// NOTE: Do NOT use useExitOnCtrlC here - SSH handles Ctrl+C itself
2121
// Using useInput would conflict with the subprocess's terminal control
@@ -76,15 +76,16 @@ export function SSHSessionScreen() {
7676
command="ssh"
7777
args={sshArgs}
7878
onExit={(_code) => {
79-
// Navigate back to previous screen when SSH exits
79+
// Replace current screen (don't add SSH to history stack)
80+
// Using replace() instead of navigate() prevents "escape goes back to SSH" bug
8081
setTimeout(() => {
81-
navigate(returnScreen, returnParams || {});
82+
replace(returnScreen, returnParams || {});
8283
}, 100);
8384
}}
8485
onError={(_error) => {
85-
// On error, navigate back as well
86+
// On error, replace current screen as well
8687
setTimeout(() => {
87-
navigate(returnScreen, returnParams || {});
88+
replace(returnScreen, returnParams || {});
8889
}, 100);
8990
}}
9091
/>

0 commit comments

Comments
 (0)