@runloop/rl-cli 0.0.1 → 0.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -4,14 +4,13 @@ import { Box, Text, useInput } from 'ink';
4
4
  import TextInput from 'ink-text-input';
5
5
  import figures from 'figures';
6
6
  import { getClient } from '../utils/client.js';
7
- import { Header } from './Header.js';
8
7
  import { SpinnerComponent } from './Spinner.js';
9
8
  import { ErrorMessage } from './ErrorMessage.js';
10
9
  import { SuccessMessage } from './SuccessMessage.js';
11
10
  import { Breadcrumb } from './Breadcrumb.js';
12
11
  import { MetadataDisplay } from './MetadataDisplay.js';
13
12
  export const DevboxCreatePage = ({ onBack, onCreate }) => {
14
- const [currentField, setCurrentField] = React.useState('name');
13
+ const [currentField, setCurrentField] = React.useState('create');
15
14
  const [formData, setFormData] = React.useState({
16
15
  name: '',
17
16
  architecture: 'arm64',
@@ -33,6 +32,7 @@ export const DevboxCreatePage = ({ onBack, onCreate }) => {
33
32
  const [result, setResult] = React.useState(null);
34
33
  const [error, setError] = React.useState(null);
35
34
  const baseFields = [
35
+ { key: 'create', label: 'Devbox Create', type: 'action' },
36
36
  { key: 'name', label: 'Name', type: 'text' },
37
37
  { key: 'architecture', label: 'Architecture', type: 'select' },
38
38
  { key: 'resource_size', label: 'Resource Size', type: 'select' },
@@ -93,6 +93,11 @@ export const DevboxCreatePage = ({ onBack, onCreate }) => {
93
93
  handleCreate();
94
94
  return;
95
95
  }
96
+ // Handle Enter on create field
97
+ if (currentField === 'create' && key.return) {
98
+ handleCreate();
99
+ return;
100
+ }
96
101
  // Handle metadata section
97
102
  if (inMetadataSection) {
98
103
  const metadataKeys = Object.keys(formData.metadata);
@@ -312,29 +317,32 @@ export const DevboxCreatePage = ({ onBack, onCreate }) => {
312
317
  return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [
313
318
  { label: 'Devboxes' },
314
319
  { label: 'Create', active: true }
315
- ] }), _jsx(Header, { title: "Create Devbox" }), _jsx(SuccessMessage, { message: "Devbox created successfully!", details: `ID: ${result.id}\nName: ${result.name || '(none)'}\nStatus: ${result.status}` }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: "gray", dimColor: true, children: "Press [Enter], [q], or [esc] to return to list" }) })] }));
320
+ ] }), _jsx(SuccessMessage, { message: "Devbox created successfully!", details: `ID: ${result.id}\nName: ${result.name || '(none)'}\nStatus: ${result.status}` }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: "gray", dimColor: true, children: "Press [Enter], [q], or [esc] to return to list" }) })] }));
316
321
  }
317
322
  // Error screen
318
323
  if (error) {
319
324
  return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [
320
325
  { label: 'Devboxes' },
321
326
  { label: 'Create', active: true }
322
- ] }), _jsx(Header, { title: "Create Devbox" }), _jsx(ErrorMessage, { message: "Failed to create devbox", error: error }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: "gray", dimColor: true, children: "Press [Enter] or [r] to retry \u2022 [q] or [esc] to cancel" }) })] }));
327
+ ] }), _jsx(ErrorMessage, { message: "Failed to create devbox", error: error }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: "gray", dimColor: true, children: "Press [Enter] or [r] to retry \u2022 [q] or [esc] to cancel" }) })] }));
323
328
  }
324
329
  // Creating screen
325
330
  if (creating) {
326
331
  return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [
327
332
  { label: 'Devboxes' },
328
333
  { label: 'Create', active: true }
329
- ] }), _jsx(Header, { title: "Create Devbox" }), _jsx(SpinnerComponent, { message: "Creating devbox..." })] }));
334
+ ] }), _jsx(SpinnerComponent, { message: "Creating devbox..." })] }));
330
335
  }
331
336
  // Form screen
332
337
  return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [
333
338
  { label: 'Devboxes' },
334
339
  { label: 'Create', active: true }
335
- ] }), _jsx(Header, { title: "Create Devbox" }), _jsx(Box, { flexDirection: "column", marginBottom: 1, children: fields.map((field, index) => {
340
+ ] }), _jsx(Box, { flexDirection: "column", marginBottom: 1, children: fields.map((field, index) => {
336
341
  const isActive = currentField === field.key;
337
342
  const fieldData = formData[field.key];
343
+ if (field.type === 'action') {
344
+ return (_jsxs(Box, { marginBottom: 0, children: [_jsxs(Text, { color: isActive ? 'green' : 'gray', bold: isActive, children: [isActive ? figures.pointer : ' ', " ", field.label] }), isActive && (_jsxs(Text, { color: "gray", dimColor: true, children: [' ', "[Enter to create]"] }))] }, field.key));
345
+ }
338
346
  if (field.type === 'text') {
339
347
  return (_jsxs(Box, { marginBottom: 0, children: [_jsxs(Text, { color: isActive ? 'cyan' : 'gray', children: [isActive ? figures.pointer : ' ', " ", field.label, ":", ' '] }), isActive ? (_jsx(TextInput, { value: String(fieldData || ''), onChange: (value) => {
340
348
  setFormData({ ...formData, [field.key]: value });
@@ -366,5 +374,5 @@ export const DevboxCreatePage = ({ onBack, onCreate }) => {
366
374
  : `${figures.arrowUp}${figures.arrowDown} Navigate • [Enter] ${selectedMetadataIndex === 0 ? 'Add' : selectedMetadataIndex === maxIndex ? 'Done' : 'Edit'} • [d] Delete • [esc] Back` }) })] }, field.key));
367
375
  }
368
376
  return null;
369
- }) }), formData.resource_size === 'CUSTOM_SIZE' && validateCustomResources() && (_jsxs(Box, { borderStyle: "round", borderColor: "red", paddingX: 1, paddingY: 0, marginTop: 1, children: [_jsxs(Text, { color: "red", bold: true, children: [figures.cross, " Validation Error"] }), _jsx(Text, { color: "red", dimColor: true, children: validateCustomResources() })] })), _jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "green", paddingX: 1, paddingY: 0, marginTop: 1, children: [_jsxs(Text, { color: "green", bold: true, children: [figures.info, " Summary"] }), _jsxs(Text, { dimColor: true, children: ["Name: ", formData.name || '(auto-generated)'] }), _jsxs(Text, { dimColor: true, children: ["Architecture: ", formData.architecture] }), formData.resource_size && _jsxs(Text, { dimColor: true, children: ["Resources: ", formData.resource_size] }), formData.resource_size === 'CUSTOM_SIZE' && formData.custom_cpu && (_jsxs(Text, { dimColor: true, children: [" CPU: ", formData.custom_cpu, " cores"] })), formData.resource_size === 'CUSTOM_SIZE' && formData.custom_memory && (_jsxs(Text, { dimColor: true, children: [" Memory: ", formData.custom_memory, " GB"] })), formData.resource_size === 'CUSTOM_SIZE' && formData.custom_disk && (_jsxs(Text, { dimColor: true, children: [" Disk: ", formData.custom_disk, " GB"] })), _jsxs(Text, { dimColor: true, children: ["Keep Alive: ", formData.keep_alive, "s (", Math.floor(parseInt(formData.keep_alive || '0') / 60), "m)"] }), formData.blueprint_id && _jsxs(Text, { dimColor: true, children: ["Blueprint: ", formData.blueprint_id] }), formData.snapshot_id && _jsxs(Text, { dimColor: true, children: ["Snapshot: ", formData.snapshot_id] }), _jsxs(Text, { dimColor: true, children: ["Metadata: ", Object.keys(formData.metadata).length, " item(s)"] })] }), !inMetadataSection && (_jsxs(_Fragment, { children: [_jsx(Box, { borderStyle: "single", borderColor: "green", paddingX: 1, paddingY: 0, marginTop: 1, children: _jsxs(Text, { color: "green", bold: true, children: [figures.play, " Press [Ctrl+S] to create this devbox"] }) }), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: "gray", dimColor: true, children: [figures.arrowUp, figures.arrowDown, " Navigate \u2022 [Ctrl+S] Create \u2022 [q] Cancel"] }) })] }))] }));
377
+ }) }), formData.resource_size === 'CUSTOM_SIZE' && validateCustomResources() && (_jsxs(Box, { borderStyle: "round", borderColor: "red", paddingX: 1, paddingY: 0, marginTop: 1, children: [_jsxs(Text, { color: "red", bold: true, children: [figures.cross, " Validation Error"] }), _jsx(Text, { color: "red", dimColor: true, children: validateCustomResources() })] })), !inMetadataSection && (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: "gray", dimColor: true, children: [figures.arrowUp, figures.arrowDown, " Navigate \u2022 [Enter] Create \u2022 [q] Cancel"] }) }))] }));
370
378
  };
@@ -1,16 +1,13 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import React from 'react';
3
- import { Box, Text, useInput, useApp, useStdout } from 'ink';
4
- import TextInput from 'ink-text-input';
3
+ import { Box, Text, useInput, useStdout } from 'ink';
5
4
  import figures from 'figures';
6
- import { getClient } from '../utils/client.js';
7
5
  import { Header } from './Header.js';
8
- import { SpinnerComponent } from './Spinner.js';
9
- import { ErrorMessage } from './ErrorMessage.js';
10
- import { SuccessMessage } from './SuccessMessage.js';
11
6
  import { StatusBadge } from './StatusBadge.js';
12
7
  import { MetadataDisplay } from './MetadataDisplay.js';
13
8
  import { Breadcrumb } from './Breadcrumb.js';
9
+ import { DevboxActionsMenu } from './DevboxActionsMenu.js';
10
+ import { getDevboxUrl } from '../utils/url.js';
14
11
  // Format time ago in a succinct way
15
12
  const formatTimeAgo = (timestamp) => {
16
13
  const seconds = Math.floor((Date.now() - timestamp) / 1000);
@@ -31,34 +28,23 @@ const formatTimeAgo = (timestamp) => {
31
28
  const years = Math.floor(months / 12);
32
29
  return `${years}y ago`;
33
30
  };
34
- export const DevboxDetailPage = ({ devbox: initialDevbox, onBack }) => {
35
- const { exit } = useApp();
31
+ export const DevboxDetailPage = ({ devbox: initialDevbox, onBack, onSSHRequest }) => {
36
32
  const { stdout } = useStdout();
37
- const [loading, setLoading] = React.useState(false);
38
- const [selectedOperation, setSelectedOperation] = React.useState(0);
39
- const [executingOperation, setExecutingOperation] = React.useState(null);
40
- const [operationInput, setOperationInput] = React.useState('');
41
- const [operationResult, setOperationResult] = React.useState(null);
42
- const [operationError, setOperationError] = React.useState(null);
43
33
  const [showDetailedInfo, setShowDetailedInfo] = React.useState(false);
44
34
  const [detailScroll, setDetailScroll] = React.useState(0);
45
- const [logsWrapMode, setLogsWrapMode] = React.useState(true);
46
- const [logsScroll, setLogsScroll] = React.useState(0);
47
- const [copyStatus, setCopyStatus] = React.useState(null);
35
+ const [showActions, setShowActions] = React.useState(false);
36
+ const [selectedOperation, setSelectedOperation] = React.useState(0);
48
37
  const selectedDevbox = initialDevbox;
49
- // Memoize time-based values to prevent re-rendering on every tick
50
- const formattedCreateTime = React.useMemo(() => selectedDevbox.create_time_ms ? new Date(selectedDevbox.create_time_ms).toLocaleString() : '', [selectedDevbox.create_time_ms]);
51
- const createTimeAgo = React.useMemo(() => selectedDevbox.create_time_ms ? formatTimeAgo(selectedDevbox.create_time_ms) : '', [selectedDevbox.create_time_ms]);
52
38
  const allOperations = [
53
- { key: 'logs', label: 'View Logs', color: 'blue', icon: figures.info },
54
- { key: 'exec', label: 'Execute Command', color: 'green', icon: figures.play },
55
- { key: 'upload', label: 'Upload File', color: 'green', icon: figures.arrowUp },
56
- { key: 'snapshot', label: 'Create Snapshot', color: 'yellow', icon: figures.circleFilled },
57
- { key: 'ssh', label: 'SSH onto the box', color: 'cyan', icon: figures.arrowRight },
58
- { key: 'tunnel', label: 'Open Tunnel', color: 'magenta', icon: figures.pointerSmall },
59
- { key: 'suspend', label: 'Suspend Devbox', color: 'yellow', icon: figures.squareSmallFilled },
60
- { key: 'resume', label: 'Resume Devbox', color: 'green', icon: figures.play },
61
- { key: 'delete', label: 'Shutdown Devbox', color: 'red', icon: figures.cross },
39
+ { key: 'logs', label: 'View Logs', color: 'blue', icon: figures.info, shortcut: 'l' },
40
+ { key: 'exec', label: 'Execute Command', color: 'green', icon: figures.play, shortcut: 'e' },
41
+ { key: 'upload', label: 'Upload File', color: 'green', icon: figures.arrowUp, shortcut: 'u' },
42
+ { key: 'snapshot', label: 'Create Snapshot', color: 'yellow', icon: figures.circleFilled, shortcut: 'n' },
43
+ { key: 'ssh', label: 'SSH onto the box', color: 'cyan', icon: figures.arrowRight, shortcut: 's' },
44
+ { key: 'tunnel', label: 'Open Tunnel', color: 'magenta', icon: figures.pointerSmall, shortcut: 't' },
45
+ { key: 'suspend', label: 'Suspend Devbox', color: 'yellow', icon: figures.squareSmallFilled, shortcut: 'p' },
46
+ { key: 'resume', label: 'Resume Devbox', color: 'green', icon: figures.play, shortcut: 'r' },
47
+ { key: 'delete', label: 'Shutdown Devbox', color: 'red', icon: figures.cross, shortcut: 'd' },
62
48
  ];
63
49
  // Filter operations based on devbox status
64
50
  const operations = selectedDevbox ? allOperations.filter(op => {
@@ -78,120 +64,12 @@ export const DevboxDetailPage = ({ devbox: initialDevbox, onBack }) => {
78
64
  // Default for transitional states (provisioning, initializing)
79
65
  return op.key === 'logs' || op.key === 'delete';
80
66
  }) : allOperations;
81
- // Auto-execute operations that don't need input (delete, ssh, logs, suspend, resume)
82
- React.useEffect(() => {
83
- if ((executingOperation === 'delete' || executingOperation === 'ssh' || executingOperation === 'logs' || executingOperation === 'suspend' || executingOperation === 'resume') && !loading && selectedDevbox) {
84
- executeOperation();
85
- }
86
- }, [executingOperation]);
67
+ // Memoize time-based values to prevent re-rendering on every tick
68
+ const formattedCreateTime = React.useMemo(() => selectedDevbox.create_time_ms ? new Date(selectedDevbox.create_time_ms).toLocaleString() : '', [selectedDevbox.create_time_ms]);
69
+ const createTimeAgo = React.useMemo(() => selectedDevbox.create_time_ms ? formatTimeAgo(selectedDevbox.create_time_ms) : '', [selectedDevbox.create_time_ms]);
87
70
  useInput((input, key) => {
88
- // Handle operation input mode
89
- if (executingOperation && !operationResult && !operationError) {
90
- if (key.return && operationInput.trim()) {
91
- executeOperation();
92
- }
93
- else if (input === 'q' || key.escape) {
94
- console.clear();
95
- setExecutingOperation(null);
96
- setOperationInput('');
97
- }
98
- return;
99
- }
100
- // Handle operation result display
101
- if (operationResult || operationError) {
102
- if (input === 'q' || key.escape || key.return) {
103
- console.clear();
104
- setOperationResult(null);
105
- setOperationError(null);
106
- setExecutingOperation(null);
107
- setOperationInput('');
108
- setLogsWrapMode(true); // Reset wrap mode
109
- setLogsScroll(0); // Reset scroll
110
- setCopyStatus(null); // Reset copy status
111
- // Keep detail view open
112
- }
113
- else if ((key.upArrow || input === 'k') && operationResult && typeof operationResult === 'object' && operationResult.__customRender === 'logs') {
114
- // Scroll up in logs
115
- setLogsScroll(Math.max(0, logsScroll - 1));
116
- }
117
- else if ((key.downArrow || input === 'j') && operationResult && typeof operationResult === 'object' && operationResult.__customRender === 'logs') {
118
- // Scroll down in logs
119
- setLogsScroll(logsScroll + 1);
120
- }
121
- else if (key.pageUp && operationResult && typeof operationResult === 'object' && operationResult.__customRender === 'logs') {
122
- // Page up
123
- setLogsScroll(Math.max(0, logsScroll - 10));
124
- }
125
- else if (key.pageDown && operationResult && typeof operationResult === 'object' && operationResult.__customRender === 'logs') {
126
- // Page down
127
- setLogsScroll(logsScroll + 10);
128
- }
129
- else if (input === 'g' && operationResult && typeof operationResult === 'object' && operationResult.__customRender === 'logs') {
130
- // Jump to top
131
- setLogsScroll(0);
132
- }
133
- else if (input === 'G' && operationResult && typeof operationResult === 'object' && operationResult.__customRender === 'logs') {
134
- // Jump to bottom (last line)
135
- const logs = operationResult.__logs || [];
136
- const terminalHeight = stdout?.rows || 30;
137
- const viewportHeight = Math.max(10, terminalHeight - 10);
138
- const maxScroll = Math.max(0, logs.length - viewportHeight);
139
- setLogsScroll(maxScroll);
140
- }
141
- else if (input === 'w' && operationResult && typeof operationResult === 'object' && operationResult.__customRender === 'logs') {
142
- // Toggle wrap mode for logs
143
- setLogsWrapMode(!logsWrapMode);
144
- }
145
- else if (input === 'c' && operationResult && typeof operationResult === 'object' && operationResult.__customRender === 'logs') {
146
- // Copy logs to clipboard
147
- const logs = operationResult.__logs || [];
148
- const logsText = logs.map((log) => {
149
- const time = new Date(log.timestamp_ms).toLocaleString();
150
- const level = log.level || 'INFO';
151
- const source = log.source || 'exec';
152
- const message = log.message || '';
153
- const cmd = log.cmd ? `[${log.cmd}] ` : '';
154
- const exitCode = log.exit_code !== null && log.exit_code !== undefined ? `(${log.exit_code}) ` : '';
155
- return `${time} ${level}/${source} ${exitCode}${cmd}${message}`;
156
- }).join('\n');
157
- // Copy to clipboard using pbcopy (macOS), xclip (Linux), or clip (Windows)
158
- const copyToClipboard = async (text) => {
159
- const { spawn } = await import('child_process');
160
- const platform = process.platform;
161
- let command;
162
- let args;
163
- if (platform === 'darwin') {
164
- command = 'pbcopy';
165
- args = [];
166
- }
167
- else if (platform === 'win32') {
168
- command = 'clip';
169
- args = [];
170
- }
171
- else {
172
- command = 'xclip';
173
- args = ['-selection', 'clipboard'];
174
- }
175
- const proc = spawn(command, args);
176
- proc.stdin.write(text);
177
- proc.stdin.end();
178
- proc.on('exit', (code) => {
179
- if (code === 0) {
180
- setCopyStatus('Copied to clipboard!');
181
- setTimeout(() => setCopyStatus(null), 2000);
182
- }
183
- else {
184
- setCopyStatus('Failed to copy');
185
- setTimeout(() => setCopyStatus(null), 2000);
186
- }
187
- });
188
- proc.on('error', () => {
189
- setCopyStatus('Copy not supported');
190
- setTimeout(() => setCopyStatus(null), 2000);
191
- });
192
- };
193
- copyToClipboard(logsText);
194
- }
71
+ // Skip input handling when in actions view
72
+ if (showActions) {
195
73
  return;
196
74
  }
197
75
  // Handle detailed info mode
@@ -218,19 +96,37 @@ export const DevboxDetailPage = ({ devbox: initialDevbox, onBack }) => {
218
96
  }
219
97
  return;
220
98
  }
221
- // Operations selection mode
99
+ // Main view input handling
222
100
  if (input === 'q' || key.escape) {
223
101
  console.clear();
224
102
  onBack();
225
- setSelectedOperation(0);
226
103
  }
227
104
  else if (input === 'i') {
228
105
  setShowDetailedInfo(true);
229
106
  setDetailScroll(0);
230
107
  }
231
- else if (input === 'o') {
108
+ else if (key.upArrow && selectedOperation > 0) {
109
+ setSelectedOperation(selectedOperation - 1);
110
+ }
111
+ else if (key.downArrow && selectedOperation < operations.length - 1) {
112
+ setSelectedOperation(selectedOperation + 1);
113
+ }
114
+ else if (key.return || input === 'a') {
115
+ console.clear();
116
+ setShowActions(true);
117
+ }
118
+ else if (input) {
119
+ // Check if input matches any operation shortcut
120
+ const matchedOpIndex = operations.findIndex(op => op.shortcut === input);
121
+ if (matchedOpIndex !== -1) {
122
+ setSelectedOperation(matchedOpIndex);
123
+ console.clear();
124
+ setShowActions(true);
125
+ }
126
+ }
127
+ if (input === 'o') {
232
128
  // Open in browser
233
- const url = `https://platform.runloop.ai/devboxes/${selectedDevbox.id}`;
129
+ const url = getDevboxUrl(selectedDevbox.id);
234
130
  const openBrowser = async () => {
235
131
  const { exec } = await import('child_process');
236
132
  const platform = process.platform;
@@ -248,118 +144,7 @@ export const DevboxDetailPage = ({ devbox: initialDevbox, onBack }) => {
248
144
  };
249
145
  openBrowser();
250
146
  }
251
- else if (key.upArrow && selectedOperation > 0) {
252
- setSelectedOperation(selectedOperation - 1);
253
- }
254
- else if (key.downArrow && selectedOperation < operations.length - 1) {
255
- setSelectedOperation(selectedOperation + 1);
256
- }
257
- else if (key.return) {
258
- console.clear();
259
- const op = operations[selectedOperation].key;
260
- setExecutingOperation(op);
261
- }
262
147
  });
263
- const executeOperation = async () => {
264
- const client = getClient();
265
- const devbox = selectedDevbox;
266
- try {
267
- setLoading(true);
268
- switch (executingOperation) {
269
- case 'exec':
270
- const execResult = await client.devboxes.executeSync(devbox.id, {
271
- command: operationInput,
272
- });
273
- setOperationResult(execResult.stdout || execResult.stderr || 'Command executed');
274
- break;
275
- case 'upload':
276
- // For upload, operationInput should be file path
277
- const fs = await import('fs');
278
- const fileStream = fs.createReadStream(operationInput);
279
- const filename = operationInput.split('/').pop() || 'file';
280
- await client.devboxes.uploadFile(devbox.id, {
281
- path: filename,
282
- file: fileStream,
283
- });
284
- setOperationResult(`File ${filename} uploaded successfully`);
285
- break;
286
- case 'snapshot':
287
- const snapshot = await client.devboxes.snapshotDisk(devbox.id, {
288
- name: operationInput || `snapshot-${Date.now()}`,
289
- });
290
- setOperationResult(`Snapshot created: ${snapshot.id}`);
291
- break;
292
- case 'ssh':
293
- const sshKey = await client.devboxes.createSSHKey(devbox.id);
294
- // Save SSH key to persistent location
295
- const fsModule = await import('fs');
296
- const pathModule = await import('path');
297
- const osModule = await import('os');
298
- const sshDir = pathModule.join(osModule.homedir(), '.runloop', 'ssh_keys');
299
- fsModule.mkdirSync(sshDir, { recursive: true });
300
- const keyPath = pathModule.join(sshDir, `${devbox.id}.pem`);
301
- fsModule.writeFileSync(keyPath, sshKey.ssh_private_key, { mode: 0o600 });
302
- // Determine user from launch parameters
303
- const sshUser = devbox.launch_parameters?.user_parameters?.username || 'user';
304
- const proxyCommand = 'openssl s_client -quiet -verify_quiet -servername %h -connect ssh.runloop.ai:443 2>/dev/null';
305
- // Store SSH command details globally
306
- global.__sshCommand = {
307
- keyPath,
308
- proxyCommand,
309
- sshUser,
310
- url: sshKey.url,
311
- devboxName: devbox.name || devbox.id
312
- };
313
- // Exit Ink app to release terminal, SSH will be spawned after exit
314
- exit();
315
- break;
316
- case 'logs':
317
- const logsResult = await client.devboxes.logs.list(devbox.id);
318
- if (logsResult.logs.length === 0) {
319
- setOperationResult('No logs available for this devbox.');
320
- }
321
- else {
322
- // Store logs data for custom rendering - show all logs
323
- logsResult.__customRender = 'logs';
324
- logsResult.__logs = logsResult.logs; // Show all logs, not just last 50
325
- logsResult.__totalCount = logsResult.logs.length;
326
- setOperationResult(logsResult);
327
- }
328
- break;
329
- case 'tunnel':
330
- const port = parseInt(operationInput);
331
- if (isNaN(port) || port < 1 || port > 65535) {
332
- setOperationError(new Error('Invalid port number. Please enter a port between 1 and 65535.'));
333
- }
334
- else {
335
- const tunnel = await client.devboxes.createTunnel(devbox.id, { port });
336
- setOperationResult(`Tunnel created!\n\n` +
337
- `Local Port: ${port}\n` +
338
- `Public URL: ${tunnel.url}\n\n` +
339
- `You can now access port ${port} on the devbox via:\n${tunnel.url}`);
340
- }
341
- break;
342
- case 'suspend':
343
- await client.devboxes.suspend(devbox.id);
344
- setOperationResult(`Devbox ${devbox.id} suspended successfully`);
345
- break;
346
- case 'resume':
347
- await client.devboxes.resume(devbox.id);
348
- setOperationResult(`Devbox ${devbox.id} resumed successfully`);
349
- break;
350
- case 'delete':
351
- await client.devboxes.shutdown(devbox.id);
352
- setOperationResult(`Devbox ${devbox.id} shut down successfully`);
353
- break;
354
- }
355
- }
356
- catch (err) {
357
- setOperationError(err);
358
- }
359
- finally {
360
- setLoading(false);
361
- }
362
- };
363
148
  const uptime = selectedDevbox.create_time_ms
364
149
  ? Math.floor((Date.now() - selectedDevbox.create_time_ms) / 1000 / 60)
365
150
  : null;
@@ -489,108 +274,16 @@ export const DevboxDetailPage = ({ devbox: initialDevbox, onBack }) => {
489
274
  });
490
275
  return lines;
491
276
  };
492
- // Operation result display
493
- if (operationResult || operationError) {
494
- const operationLabel = operations.find((o) => o.key === executingOperation)?.label || 'Operation';
495
- // Check for custom logs rendering
496
- if (operationResult && typeof operationResult === 'object' && operationResult.__customRender === 'logs') {
497
- const logs = operationResult.__logs || [];
498
- const totalCount = operationResult.__totalCount || 0;
499
- // Calculate viewport for scrolling
500
- const terminalHeight = stdout?.rows || 30;
501
- const terminalWidth = stdout?.columns || 120;
502
- const viewportHeight = Math.max(10, terminalHeight - 10); // Reserve space for header/footer
503
- const maxScroll = Math.max(0, logs.length - viewportHeight);
504
- const actualScroll = Math.min(logsScroll, maxScroll);
505
- const visibleLogs = logs.slice(actualScroll, actualScroll + viewportHeight);
506
- const hasMore = actualScroll + viewportHeight < logs.length;
507
- const hasLess = actualScroll > 0;
508
- return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [
509
- { label: 'Devboxes' },
510
- { label: selectedDevbox?.name || selectedDevbox?.id || 'Devbox' },
511
- { label: 'Logs', active: true }
512
- ] }), _jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "gray", paddingX: 1, children: [visibleLogs.map((log, index) => {
513
- const time = new Date(log.timestamp_ms).toLocaleTimeString();
514
- const level = log.level ? log.level[0].toUpperCase() : 'I';
515
- const source = log.source ? log.source.substring(0, 8) : 'exec';
516
- const fullMessage = log.message || '';
517
- const cmd = log.cmd ? `[${log.cmd.substring(0, 40)}${log.cmd.length > 40 ? '...' : ''}] ` : '';
518
- const exitCode = log.exit_code !== null && log.exit_code !== undefined ? `(${log.exit_code}) ` : '';
519
- let levelColor = 'gray';
520
- if (level === 'E')
521
- levelColor = 'red';
522
- else if (level === 'W')
523
- levelColor = 'yellow';
524
- else if (level === 'I')
525
- levelColor = 'cyan';
526
- if (logsWrapMode) {
527
- // Wrap mode: show full message on same line, let terminal handle wrapping
528
- return (_jsxs(Box, { children: [_jsx(Text, { color: "gray", dimColor: true, children: time }), _jsx(Text, { children: " " }), _jsx(Text, { color: levelColor, bold: true, children: level }), _jsxs(Text, { color: "gray", dimColor: true, children: ["/", source] }), _jsx(Text, { children: " " }), exitCode && _jsx(Text, { color: "yellow", children: exitCode }), cmd && _jsx(Text, { color: "blue", dimColor: true, children: cmd }), _jsx(Text, { children: fullMessage })] }, index));
529
- }
530
- else {
531
- // No-wrap mode: calculate actual metadata width and truncate accordingly
532
- // Time (11) + space (1) + Level (1) + /source (1+8) + space (1) + exitCode.length + cmd.length + border/padding (6)
533
- const metadataWidth = 11 + 1 + 1 + 1 + 8 + 1 + exitCode.length + cmd.length + 6;
534
- const availableMessageWidth = Math.max(20, terminalWidth - metadataWidth);
535
- const truncatedMessage = fullMessage.length > availableMessageWidth
536
- ? fullMessage.substring(0, availableMessageWidth - 3) + '...'
537
- : fullMessage;
538
- return (_jsxs(Box, { children: [_jsx(Text, { color: "gray", dimColor: true, children: time }), _jsx(Text, { children: " " }), _jsx(Text, { color: levelColor, bold: true, children: level }), _jsxs(Text, { color: "gray", dimColor: true, children: ["/", source] }), _jsx(Text, { children: " " }), exitCode && _jsx(Text, { color: "yellow", children: exitCode }), cmd && _jsx(Text, { color: "blue", dimColor: true, children: cmd }), _jsx(Text, { children: truncatedMessage })] }, index));
539
- }
540
- }), hasLess && (_jsx(Box, { children: _jsxs(Text, { color: "cyan", children: [figures.arrowUp, " More above"] }) })), hasMore && (_jsx(Box, { children: _jsxs(Text, { color: "cyan", children: [figures.arrowDown, " More below"] }) }))] }), _jsxs(Box, { marginTop: 1, paddingX: 1, children: [_jsxs(Text, { color: "cyan", bold: true, children: [figures.hamburger, " ", totalCount] }), _jsx(Text, { color: "gray", dimColor: true, children: " total logs" }), _jsx(Text, { color: "gray", dimColor: true, children: " \u2022 " }), _jsxs(Text, { color: "gray", dimColor: true, children: ["Viewing ", actualScroll + 1, "-", Math.min(actualScroll + viewportHeight, logs.length), " of ", logs.length] }), _jsx(Text, { color: "gray", dimColor: true, children: " \u2022 " }), _jsx(Text, { color: logsWrapMode ? 'green' : 'gray', bold: logsWrapMode, children: logsWrapMode ? 'Wrap: ON' : 'Wrap: OFF' }), copyStatus && (_jsxs(_Fragment, { children: [_jsx(Text, { color: "gray", dimColor: true, children: " \u2022 " }), _jsx(Text, { color: "green", bold: true, children: copyStatus })] }))] }), _jsx(Box, { marginTop: 1, paddingX: 1, children: _jsxs(Text, { color: "gray", dimColor: true, children: [figures.arrowUp, figures.arrowDown, " Navigate \u2022 [g] Top \u2022 [G] Bottom \u2022 [w] Toggle Wrap \u2022 [c] Copy \u2022 [Enter], [q], or [esc] Back"] }) })] }));
541
- }
542
- return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [
543
- { label: 'Devboxes' },
544
- { label: selectedDevbox?.name || selectedDevbox?.id || 'Devbox' },
545
- { label: operationLabel, active: true }
546
- ] }), _jsx(Header, { title: "Operation Result" }), operationResult && _jsx(SuccessMessage, { message: operationResult }), operationError && _jsx(ErrorMessage, { message: "Operation failed", error: operationError }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: "gray", dimColor: true, children: "Press [Enter], [q], or [esc] to continue" }) })] }));
547
- }
548
- // Operation input mode
549
- if (executingOperation && selectedDevbox) {
550
- const needsInput = executingOperation === 'exec' ||
551
- executingOperation === 'upload' ||
552
- executingOperation === 'snapshot' ||
553
- executingOperation === 'tunnel';
554
- const operationLabel = operations.find((o) => o.key === executingOperation)?.label || 'Operation';
555
- if (loading) {
556
- return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [
557
- { label: 'Devboxes' },
558
- { label: selectedDevbox.name || selectedDevbox.id },
559
- { label: operationLabel, active: true }
560
- ] }), _jsx(Header, { title: "Executing Operation" }), _jsx(SpinnerComponent, { message: "Please wait..." })] }));
561
- }
562
- if (!needsInput) {
563
- // SSH, Logs, Suspend, Resume, and Delete operations are auto-executed via useEffect
564
- const messages = {
565
- ssh: 'Creating SSH key...',
566
- logs: 'Fetching logs...',
567
- suspend: 'Suspending devbox...',
568
- resume: 'Resuming devbox...',
569
- delete: 'Shutting down devbox...',
570
- };
571
- return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [
572
- { label: 'Devboxes' },
573
- { label: selectedDevbox.name || selectedDevbox.id },
574
- { label: operationLabel, active: true }
575
- ] }), _jsx(Header, { title: "Executing Operation" }), _jsx(SpinnerComponent, { message: messages[executingOperation] || 'Please wait...' })] }));
576
- }
577
- const prompts = {
578
- exec: 'Command to execute:',
579
- upload: 'File path to upload:',
580
- snapshot: 'Snapshot name (optional):',
581
- tunnel: 'Port number to expose:',
582
- };
583
- return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [
584
- { label: 'Devboxes' },
585
- { label: selectedDevbox.name || selectedDevbox.id },
586
- { label: operationLabel, active: true }
587
- ] }), _jsx(Header, { title: operationLabel }), _jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { color: "cyan", bold: true, children: selectedDevbox.name || selectedDevbox.id }) }), _jsx(Box, { children: _jsxs(Text, { color: "gray", children: [prompts[executingOperation], " "] }) }), _jsx(Box, { marginTop: 1, children: _jsx(TextInput, { value: operationInput, onChange: setOperationInput, placeholder: executingOperation === 'exec'
588
- ? 'ls -la'
589
- : executingOperation === 'upload'
590
- ? '/path/to/file'
591
- : executingOperation === 'tunnel'
592
- ? '8080'
593
- : 'my-snapshot' }) }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: "gray", dimColor: true, children: "Press [Enter] to execute \u2022 [q or esc] Cancel" }) })] })] }));
277
+ // Actions view - show the DevboxActionsMenu when an action is triggered
278
+ if (showActions) {
279
+ const selectedOp = operations[selectedOperation];
280
+ return (_jsx(DevboxActionsMenu, { devbox: selectedDevbox, onBack: () => {
281
+ setShowActions(false);
282
+ setSelectedOperation(0);
283
+ }, breadcrumbItems: [
284
+ { label: 'Devboxes' },
285
+ { label: selectedDevbox.name || selectedDevbox.id }
286
+ ], initialOperation: selectedOp?.key, skipOperationsMenu: true, onSSHRequest: onSSHRequest }));
594
287
  }
595
288
  // Detailed info mode - full screen
596
289
  if (showDetailedInfo) {
@@ -606,16 +299,16 @@ export const DevboxDetailPage = ({ devbox: initialDevbox, onBack }) => {
606
299
  { label: 'Devboxes' },
607
300
  { label: selectedDevbox.name || selectedDevbox.id },
608
301
  { label: 'Full Details', active: true }
609
- ] }), _jsx(Header, { title: `${selectedDevbox.name || selectedDevbox.id} - Complete Information` }), _jsx(Box, { flexDirection: "column", marginBottom: 1, children: _jsxs(Box, { marginBottom: 1, children: [_jsx(StatusBadge, { status: selectedDevbox.status }), _jsx(Text, { children: " " }), _jsx(Text, { color: "gray", dimColor: true, children: selectedDevbox.id })] }) }), _jsxs(Box, { flexDirection: "column", marginTop: 1, marginBottom: 1, borderStyle: "round", borderColor: "gray", paddingX: 2, paddingY: 1, children: [_jsx(Box, { flexDirection: "column", children: visibleLines }), hasLess && (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: "cyan", children: [figures.arrowUp, " More above"] }) })), hasMore && (_jsx(Box, { marginTop: hasLess ? 0 : 1, children: _jsxs(Text, { color: "cyan", children: [figures.arrowDown, " More below"] }) }))] }), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: "gray", dimColor: true, children: [figures.arrowUp, figures.arrowDown, " Scroll \u2022 [q or esc] Back to Operations \u2022 Line ", actualScroll + 1, "-", Math.min(actualScroll + viewportHeight, detailLines.length), " of ", detailLines.length] }) })] }));
302
+ ] }), _jsx(Header, { title: `${selectedDevbox.name || selectedDevbox.id} - Complete Information` }), _jsx(Box, { flexDirection: "column", marginBottom: 1, children: _jsxs(Box, { marginBottom: 1, children: [_jsx(StatusBadge, { status: selectedDevbox.status }), _jsx(Text, { children: " " }), _jsx(Text, { color: "gray", dimColor: true, children: selectedDevbox.id })] }) }), _jsxs(Box, { flexDirection: "column", marginTop: 1, marginBottom: 1, borderStyle: "round", borderColor: "gray", paddingX: 2, paddingY: 1, children: [_jsx(Box, { flexDirection: "column", children: visibleLines }), hasLess && (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: "cyan", children: [figures.arrowUp, " More above"] }) })), hasMore && (_jsx(Box, { marginTop: hasLess ? 0 : 1, children: _jsxs(Text, { color: "cyan", children: [figures.arrowDown, " More below"] }) }))] }), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: "gray", dimColor: true, children: [figures.arrowUp, figures.arrowDown, " Scroll \u2022 [q or esc] Back to Details \u2022 Line ", actualScroll + 1, "-", Math.min(actualScroll + viewportHeight, detailLines.length), " of ", detailLines.length] }) })] }));
610
303
  }
611
- // Operations selection mode (main detail view)
304
+ // Main detail view
612
305
  const lp = selectedDevbox.launch_parameters;
613
306
  const hasCapabilities = selectedDevbox.capabilities && selectedDevbox.capabilities.filter((c) => c !== 'unknown').length > 0;
614
307
  return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [
615
308
  { label: 'Devboxes' },
616
309
  { label: selectedDevbox.name || selectedDevbox.id, active: true }
617
- ] }), _jsx(Header, { title: "Devbox Details" }), _jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1, paddingY: 0, children: [_jsxs(Box, { children: [_jsx(Text, { color: "cyan", bold: true, children: selectedDevbox.name || selectedDevbox.id }), _jsx(Text, { children: " " }), _jsx(StatusBadge, { status: selectedDevbox.status }), _jsxs(Text, { color: "gray", dimColor: true, children: [" \u2022 ", selectedDevbox.id] })] }), _jsxs(Box, { children: [_jsx(Text, { color: "gray", dimColor: true, children: formattedCreateTime }), _jsxs(Text, { color: "gray", dimColor: true, children: [" (", createTimeAgo, ")"] })] }), uptime !== null && selectedDevbox.status === 'running' && (_jsxs(Box, { children: [_jsxs(Text, { color: "green", dimColor: true, children: ["Uptime: ", uptime < 60 ? `${uptime}m` : `${Math.floor(uptime / 60)}h ${uptime % 60}m`] }), lp?.keep_alive_time_seconds && (_jsxs(Text, { color: "gray", dimColor: true, children: [" \u2022 Keep-alive: ", Math.floor(lp.keep_alive_time_seconds / 60), "m"] }))] }))] }), _jsxs(Box, { flexDirection: "row", gap: 1, children: [(lp?.resource_size_request || lp?.custom_cpu_cores || lp?.custom_gb_memory || lp?.custom_disk_size || lp?.architecture) && (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "yellow", paddingX: 1, paddingY: 0, flexGrow: 1, children: [_jsxs(Text, { color: "yellow", bold: true, children: [figures.squareSmallFilled, " Resources"] }), _jsxs(Text, { dimColor: true, children: [lp?.resource_size_request && `${lp.resource_size_request}`, lp?.architecture && ` • ${lp.architecture}`, lp?.custom_cpu_cores && ` • ${lp.custom_cpu_cores}VCPU`, lp?.custom_gb_memory && ` • ${lp.custom_gb_memory}GB RAM`, lp?.custom_disk_size && ` • ${lp.custom_disk_size}GB DISC`] })] })), hasCapabilities && (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "blue", paddingX: 1, paddingY: 0, flexGrow: 1, children: [_jsxs(Text, { color: "blue", bold: true, children: [figures.tick, " Capabilities"] }), _jsx(Text, { dimColor: true, children: selectedDevbox.capabilities.filter((c) => c !== 'unknown').join(', ') })] })), (selectedDevbox.blueprint_id || selectedDevbox.snapshot_id) && (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "magenta", paddingX: 1, paddingY: 0, flexGrow: 1, children: [_jsxs(Text, { color: "magenta", bold: true, children: [figures.circleFilled, " Source"] }), _jsxs(Text, { dimColor: true, children: [selectedDevbox.blueprint_id && `BP: ${selectedDevbox.blueprint_id}`, selectedDevbox.snapshot_id && `Snap: ${selectedDevbox.snapshot_id}`] })] }))] }), selectedDevbox.metadata && Object.keys(selectedDevbox.metadata).length > 0 && (_jsx(Box, { borderStyle: "round", borderColor: "green", paddingX: 1, paddingY: 0, children: _jsx(MetadataDisplay, { metadata: selectedDevbox.metadata, showBorder: false }) })), selectedDevbox.failure_reason && (_jsxs(Box, { borderStyle: "round", borderColor: "red", paddingX: 1, paddingY: 0, children: [_jsxs(Text, { color: "red", bold: true, children: [figures.cross, " "] }), _jsx(Text, { color: "red", dimColor: true, children: selectedDevbox.failure_reason })] })), _jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { color: "cyan", bold: true, children: [figures.play, " Operations"] }), _jsx(Box, { flexDirection: "column", children: operations.map((op, index) => {
310
+ ] }), _jsx(Header, { title: "Devbox Details" }), _jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1, paddingY: 0, children: [_jsxs(Box, { children: [_jsx(Text, { color: "cyan", bold: true, children: selectedDevbox.name || selectedDevbox.id }), _jsx(Text, { children: " " }), _jsx(StatusBadge, { status: selectedDevbox.status }), _jsxs(Text, { color: "gray", dimColor: true, children: [" \u2022 ", selectedDevbox.id] })] }), _jsxs(Box, { children: [_jsx(Text, { color: "gray", dimColor: true, children: formattedCreateTime }), _jsxs(Text, { color: "gray", dimColor: true, children: [" (", createTimeAgo, ")"] })] }), uptime !== null && selectedDevbox.status === 'running' && (_jsxs(Box, { children: [_jsxs(Text, { color: "green", dimColor: true, children: ["Uptime: ", uptime < 60 ? `${uptime}m` : `${Math.floor(uptime / 60)}h ${uptime % 60}m`] }), lp?.keep_alive_time_seconds && (_jsxs(Text, { color: "gray", dimColor: true, children: [" \u2022 Keep-alive: ", Math.floor(lp.keep_alive_time_seconds / 60), "m"] }))] }))] }), _jsxs(Box, { flexDirection: "row", gap: 1, children: [(lp?.resource_size_request || lp?.custom_cpu_cores || lp?.custom_gb_memory || lp?.custom_disk_size || lp?.architecture) && (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "yellow", paddingX: 1, paddingY: 0, flexGrow: 1, children: [_jsxs(Text, { color: "yellow", bold: true, children: [figures.squareSmallFilled, " Resources"] }), _jsxs(Text, { dimColor: true, children: [lp?.resource_size_request && `${lp.resource_size_request}`, lp?.architecture && ` • ${lp.architecture}`, lp?.custom_cpu_cores && ` • ${lp.custom_cpu_cores}VCPU`, lp?.custom_gb_memory && ` • ${lp.custom_gb_memory}GB RAM`, lp?.custom_disk_size && ` • ${lp.custom_disk_size}GB DISC`] })] })), hasCapabilities && (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "blue", paddingX: 1, paddingY: 0, flexGrow: 1, children: [_jsxs(Text, { color: "blue", bold: true, children: [figures.tick, " Capabilities"] }), _jsx(Text, { dimColor: true, children: selectedDevbox.capabilities.filter((c) => c !== 'unknown').join(', ') })] })), (selectedDevbox.blueprint_id || selectedDevbox.snapshot_id) && (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "magenta", paddingX: 1, paddingY: 0, flexGrow: 1, children: [_jsxs(Text, { color: "magenta", bold: true, children: [figures.circleFilled, " Source"] }), _jsxs(Text, { dimColor: true, children: [selectedDevbox.blueprint_id && `BP: ${selectedDevbox.blueprint_id}`, selectedDevbox.snapshot_id && `Snap: ${selectedDevbox.snapshot_id}`] })] }))] }), selectedDevbox.metadata && Object.keys(selectedDevbox.metadata).length > 0 && (_jsx(Box, { borderStyle: "round", borderColor: "green", paddingX: 1, paddingY: 0, children: _jsx(MetadataDisplay, { metadata: selectedDevbox.metadata, showBorder: false }) })), selectedDevbox.failure_reason && (_jsxs(Box, { borderStyle: "round", borderColor: "red", paddingX: 1, paddingY: 0, children: [_jsxs(Text, { color: "red", bold: true, children: [figures.cross, " "] }), _jsx(Text, { color: "red", dimColor: true, children: selectedDevbox.failure_reason })] })), _jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsxs(Text, { color: "cyan", bold: true, children: [figures.play, " Actions"] }), _jsx(Box, { flexDirection: "column", children: operations.map((op, index) => {
618
311
  const isSelected = index === selectedOperation;
619
- return (_jsxs(Box, { children: [_jsxs(Text, { color: isSelected ? 'cyan' : 'gray', children: [isSelected ? figures.pointer : ' ', " "] }), _jsxs(Text, { color: isSelected ? op.color : 'gray', bold: isSelected, children: [op.icon, " ", op.label] })] }, op.key));
620
- }) })] }), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: "gray", dimColor: true, children: [figures.arrowUp, figures.arrowDown, " Navigate \u2022 [Enter] Select \u2022 [i] Full Details \u2022 [o] Browser \u2022 [q] Back"] }) })] }));
312
+ return (_jsxs(Box, { children: [_jsxs(Text, { color: isSelected ? 'cyan' : 'gray', children: [isSelected ? figures.pointer : ' ', " "] }), _jsxs(Text, { color: isSelected ? op.color : 'gray', bold: isSelected, children: [op.icon, " ", op.label] }), _jsxs(Text, { color: "gray", dimColor: true, children: [" [", op.shortcut, "]"] })] }, op.key));
313
+ }) })] }), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: "gray", dimColor: true, children: [figures.arrowUp, figures.arrowDown, " Navigate \u2022 [Enter] Execute \u2022 [i] Full Details \u2022 [o] Browser \u2022 [q] Back"] }) })] }));
621
314
  };