@runloop/rl-cli 1.8.0 → 1.10.0

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.
Files changed (66) hide show
  1. package/README.md +21 -7
  2. package/dist/cli.js +0 -0
  3. package/dist/commands/blueprint/delete.js +21 -0
  4. package/dist/commands/blueprint/list.js +226 -174
  5. package/dist/commands/blueprint/prune.js +13 -28
  6. package/dist/commands/devbox/create.js +41 -0
  7. package/dist/commands/devbox/list.js +142 -110
  8. package/dist/commands/devbox/rsync.js +69 -41
  9. package/dist/commands/devbox/scp.js +180 -39
  10. package/dist/commands/devbox/tunnel.js +4 -19
  11. package/dist/commands/gateway-config/create.js +53 -0
  12. package/dist/commands/gateway-config/delete.js +21 -0
  13. package/dist/commands/gateway-config/get.js +18 -0
  14. package/dist/commands/gateway-config/list.js +493 -0
  15. package/dist/commands/gateway-config/update.js +70 -0
  16. package/dist/commands/snapshot/list.js +11 -2
  17. package/dist/commands/snapshot/prune.js +265 -0
  18. package/dist/components/BenchmarkMenu.js +23 -3
  19. package/dist/components/DetailedInfoView.js +20 -0
  20. package/dist/components/DevboxActionsMenu.js +26 -62
  21. package/dist/components/DevboxCreatePage.js +763 -15
  22. package/dist/components/DevboxDetailPage.js +73 -24
  23. package/dist/components/GatewayConfigCreatePage.js +272 -0
  24. package/dist/components/LogsViewer.js +6 -40
  25. package/dist/components/ResourceDetailPage.js +143 -160
  26. package/dist/components/ResourceListView.js +3 -33
  27. package/dist/components/ResourcePicker.js +234 -0
  28. package/dist/components/SecretCreatePage.js +71 -27
  29. package/dist/components/SettingsMenu.js +12 -2
  30. package/dist/components/StateHistory.js +1 -20
  31. package/dist/components/StatusBadge.js +9 -2
  32. package/dist/components/StreamingLogsViewer.js +8 -42
  33. package/dist/components/form/FormTextInput.js +4 -2
  34. package/dist/components/resourceDetailTypes.js +18 -0
  35. package/dist/hooks/useInputHandler.js +103 -0
  36. package/dist/router/Router.js +79 -2
  37. package/dist/screens/BenchmarkDetailScreen.js +163 -0
  38. package/dist/screens/BenchmarkJobCreateScreen.js +524 -0
  39. package/dist/screens/BenchmarkJobDetailScreen.js +614 -0
  40. package/dist/screens/BenchmarkJobListScreen.js +479 -0
  41. package/dist/screens/BenchmarkListScreen.js +266 -0
  42. package/dist/screens/BenchmarkMenuScreen.js +6 -0
  43. package/dist/screens/BenchmarkRunDetailScreen.js +258 -22
  44. package/dist/screens/BenchmarkRunListScreen.js +21 -1
  45. package/dist/screens/BlueprintDetailScreen.js +5 -1
  46. package/dist/screens/DevboxCreateScreen.js +2 -2
  47. package/dist/screens/GatewayConfigDetailScreen.js +236 -0
  48. package/dist/screens/GatewayConfigListScreen.js +7 -0
  49. package/dist/screens/ScenarioRunDetailScreen.js +6 -0
  50. package/dist/screens/SecretDetailScreen.js +26 -2
  51. package/dist/screens/SettingsMenuScreen.js +3 -0
  52. package/dist/screens/SnapshotDetailScreen.js +6 -0
  53. package/dist/services/agentService.js +42 -0
  54. package/dist/services/benchmarkJobService.js +122 -0
  55. package/dist/services/benchmarkService.js +47 -0
  56. package/dist/services/gatewayConfigService.js +153 -0
  57. package/dist/services/scenarioService.js +34 -0
  58. package/dist/store/benchmarkJobStore.js +66 -0
  59. package/dist/store/benchmarkStore.js +63 -0
  60. package/dist/store/gatewayConfigStore.js +83 -0
  61. package/dist/utils/browser.js +22 -0
  62. package/dist/utils/clipboard.js +41 -0
  63. package/dist/utils/commands.js +105 -9
  64. package/dist/utils/gatewayConfigValidation.js +58 -0
  65. package/dist/utils/time.js +121 -0
  66. package/package.json +43 -43
@@ -10,9 +10,19 @@ import { SuccessMessage } from "./SuccessMessage.js";
10
10
  import { Breadcrumb } from "./Breadcrumb.js";
11
11
  import { NavigationTips } from "./NavigationTips.js";
12
12
  import { MetadataDisplay } from "./MetadataDisplay.js";
13
+ import { ResourcePicker, createTextColumn } from "./ResourcePicker.js";
14
+ import { formatTimeAgo } from "./ResourceListView.js";
15
+ import { getStatusDisplay } from "./StatusBadge.js";
13
16
  import { FormTextInput, FormSelect, FormActionButton, useFormSelectNavigation, } from "./form/index.js";
14
17
  import { colors } from "../utils/theme.js";
15
18
  import { useExitOnCtrlC } from "../hooks/useExitOnCtrlC.js";
19
+ import { listBlueprints } from "../services/blueprintService.js";
20
+ import { listSnapshots } from "../services/snapshotService.js";
21
+ import { listNetworkPolicies } from "../services/networkPolicyService.js";
22
+ import { listGatewayConfigs } from "../services/gatewayConfigService.js";
23
+ import { SecretCreatePage } from "./SecretCreatePage.js";
24
+ import { GatewayConfigCreatePage } from "./GatewayConfigCreatePage.js";
25
+ const sourceTypes = ["blueprint", "snapshot"];
16
26
  const architectures = ["arm64", "x86_64"];
17
27
  const resourceSizes = [
18
28
  "X_SMALL",
@@ -23,6 +33,7 @@ const resourceSizes = [
23
33
  "XX_LARGE",
24
34
  "CUSTOM_SIZE",
25
35
  ];
36
+ const tunnelAuthModes = ["none", "open", "authenticated"];
26
37
  export const DevboxCreatePage = ({ onBack, onCreate, initialBlueprintId, initialSnapshotId, }) => {
27
38
  const [currentField, setCurrentField] = React.useState("create");
28
39
  const [formData, setFormData] = React.useState({
@@ -37,6 +48,8 @@ export const DevboxCreatePage = ({ onBack, onCreate, initialBlueprintId, initial
37
48
  blueprint_id: initialBlueprintId || "",
38
49
  snapshot_id: initialSnapshotId || "",
39
50
  network_policy_id: "",
51
+ tunnel_auth_mode: "none",
52
+ gateways: [],
40
53
  });
41
54
  const [metadataKey, setMetadataKey] = React.useState("");
42
55
  const [metadataValue, setMetadataValue] = React.useState("");
@@ -46,6 +59,29 @@ export const DevboxCreatePage = ({ onBack, onCreate, initialBlueprintId, initial
46
59
  const [creating, setCreating] = React.useState(false);
47
60
  const [result, setResult] = React.useState(null);
48
61
  const [error, setError] = React.useState(null);
62
+ // Source picker states (toggle between blueprint/snapshot)
63
+ const [sourceTypeToggle, setSourceTypeToggle] = React.useState(initialSnapshotId ? "snapshot" : "blueprint");
64
+ const [showBlueprintPicker, setShowBlueprintPicker] = React.useState(false);
65
+ const [showSnapshotPicker, setShowSnapshotPicker] = React.useState(false);
66
+ const [showNetworkPolicyPicker, setShowNetworkPolicyPicker] = React.useState(false);
67
+ const [selectedBlueprintName, setSelectedBlueprintName] = React.useState("");
68
+ const [selectedSnapshotName, setSelectedSnapshotName] = React.useState("");
69
+ const [selectedNetworkPolicyName, setSelectedNetworkPolicyName] = React.useState("");
70
+ // Gateway picker states
71
+ const [showGatewayPicker, setShowGatewayPicker] = React.useState(false);
72
+ const [showSecretPicker, setShowSecretPicker] = React.useState(false);
73
+ const [inGatewaySection, setInGatewaySection] = React.useState(false);
74
+ const [gatewayEnvPrefix, setGatewayEnvPrefix] = React.useState("");
75
+ const [selectedGatewayIndex, setSelectedGatewayIndex] = React.useState(0);
76
+ const [pendingGateway, setPendingGateway] = React.useState(null);
77
+ const [pendingSecret, setPendingSecret] = React.useState(null);
78
+ const [showInlineSecretCreate, setShowInlineSecretCreate] = React.useState(false);
79
+ const [showInlineGatewayConfigCreate, setShowInlineGatewayConfigCreate] = React.useState(false);
80
+ // Gateway attach form: when active, shows a mini-form to configure a gateway
81
+ const [gatewayFormActive, setGatewayFormActive] = React.useState(false);
82
+ const [gatewayFormField, setGatewayFormField] = React.useState("attach");
83
+ const gatewayFormFields = ["attach", "gateway", "envName", "secret"];
84
+ const gatewayFormFieldIndex = gatewayFormFields.indexOf(gatewayFormField);
49
85
  const baseFields = [
50
86
  { key: "create", label: "Devbox Create", type: "action" },
51
87
  { key: "name", label: "Name", type: "text", placeholder: "my-devbox" },
@@ -83,22 +119,28 @@ export const DevboxCreatePage = ({ onBack, onCreate, initialBlueprintId, initial
83
119
  placeholder: "3600",
84
120
  },
85
121
  {
86
- key: "blueprint_id",
87
- label: "Blueprint ID (optional)",
88
- type: "text",
89
- placeholder: "bpt_xxx",
122
+ key: "source",
123
+ label: "Source (optional)",
124
+ type: "source",
125
+ placeholder: "Select Blueprint or Snapshot...",
90
126
  },
91
127
  {
92
- key: "snapshot_id",
93
- label: "Snapshot ID (optional)",
94
- type: "text",
95
- placeholder: "snp_xxx",
128
+ key: "network_policy_id",
129
+ label: "Network Policy (optional)",
130
+ type: "picker",
131
+ placeholder: "Select a network policy...",
96
132
  },
97
133
  {
98
- key: "network_policy_id",
99
- label: "Network Policy ID (optional)",
100
- type: "text",
101
- placeholder: "np_xxx",
134
+ key: "tunnel_auth_mode",
135
+ label: "Tunnel (optional)",
136
+ type: "select",
137
+ placeholder: "none",
138
+ },
139
+ {
140
+ key: "gateways",
141
+ label: "AI Gateway Configs (optional)",
142
+ type: "gateways",
143
+ placeholder: "Configure API credential proxying...",
102
144
  },
103
145
  { key: "metadata", label: "Metadata (optional)", type: "metadata" },
104
146
  ];
@@ -109,6 +151,8 @@ export const DevboxCreatePage = ({ onBack, onCreate, initialBlueprintId, initial
109
151
  // Select navigation handlers using shared hook
110
152
  const handleArchitectureNav = useFormSelectNavigation(formData.architecture, architectures, (value) => setFormData({ ...formData, architecture: value }), currentField === "architecture");
111
153
  const handleResourceSizeNav = useFormSelectNavigation(formData.resource_size || "SMALL", resourceSizes, (value) => setFormData({ ...formData, resource_size: value }), currentField === "resource_size");
154
+ const handleTunnelNav = useFormSelectNavigation(formData.tunnel_auth_mode, tunnelAuthModes, (value) => setFormData({ ...formData, tunnel_auth_mode: value }), currentField === "tunnel_auth_mode");
155
+ const handleSourceTypeNav = useFormSelectNavigation(sourceTypeToggle, sourceTypes, (value) => setSourceTypeToggle(value), currentField === "source");
112
156
  // Main form input handler - active when not in metadata section
113
157
  useInput((input, key) => {
114
158
  // Handle result screen
@@ -155,6 +199,43 @@ export const DevboxCreatePage = ({ onBack, onCreate, initialBlueprintId, initial
155
199
  setSelectedMetadataIndex(0);
156
200
  return;
157
201
  }
202
+ // Enter key on gateways field to enter gateway section
203
+ if (currentField === "gateways" && key.return) {
204
+ setInGatewaySection(true);
205
+ setSelectedGatewayIndex(0);
206
+ return;
207
+ }
208
+ // Enter key on source field to open the appropriate picker
209
+ if (currentField === "source" && key.return) {
210
+ // If something is already selected, open that type's picker to change it
211
+ const hasBlueprint = !!(selectedBlueprintName || formData.blueprint_id);
212
+ const hasSnapshot = !!(selectedSnapshotName || formData.snapshot_id);
213
+ if (hasBlueprint) {
214
+ setShowBlueprintPicker(true);
215
+ }
216
+ else if (hasSnapshot) {
217
+ setShowSnapshotPicker(true);
218
+ }
219
+ else {
220
+ // Nothing selected, use the toggle value
221
+ if (sourceTypeToggle === "blueprint") {
222
+ setShowBlueprintPicker(true);
223
+ }
224
+ else {
225
+ setShowSnapshotPicker(true);
226
+ }
227
+ }
228
+ return;
229
+ }
230
+ // Delete key on source field to clear selection
231
+ if (currentField === "source" && (input === "d" || key.delete)) {
232
+ handleClearSource();
233
+ return;
234
+ }
235
+ if (currentField === "network_policy_id" && key.return) {
236
+ setShowNetworkPolicyPicker(true);
237
+ return;
238
+ }
158
239
  // Handle Enter on any field to submit
159
240
  if (key.return) {
160
241
  handleCreate();
@@ -165,6 +246,10 @@ export const DevboxCreatePage = ({ onBack, onCreate, initialBlueprintId, initial
165
246
  return;
166
247
  if (handleResourceSizeNav(input, key))
167
248
  return;
249
+ if (handleTunnelNav(input, key))
250
+ return;
251
+ if (handleSourceTypeNav(input, key))
252
+ return;
168
253
  // Navigation (up/down arrows and tab/shift+tab)
169
254
  if ((key.upArrow || (key.tab && key.shift)) && currentFieldIndex > 0) {
170
255
  setCurrentField(fields[currentFieldIndex - 1].key);
@@ -175,7 +260,118 @@ export const DevboxCreatePage = ({ onBack, onCreate, initialBlueprintId, initial
175
260
  setCurrentField(fields[currentFieldIndex + 1].key);
176
261
  return;
177
262
  }
178
- }, { isActive: !inMetadataSection });
263
+ }, {
264
+ isActive: !inMetadataSection &&
265
+ !inGatewaySection &&
266
+ !showBlueprintPicker &&
267
+ !showSnapshotPicker &&
268
+ !showNetworkPolicyPicker &&
269
+ !showGatewayPicker &&
270
+ !showSecretPicker &&
271
+ !showInlineSecretCreate &&
272
+ !showInlineGatewayConfigCreate,
273
+ });
274
+ // Handle blueprint selection
275
+ const handleBlueprintSelect = React.useCallback((blueprints) => {
276
+ if (blueprints.length > 0) {
277
+ const blueprint = blueprints[0];
278
+ setFormData((prev) => ({
279
+ ...prev,
280
+ blueprint_id: blueprint.id,
281
+ snapshot_id: "",
282
+ }));
283
+ setSelectedBlueprintName(blueprint.name || blueprint.id);
284
+ setSelectedSnapshotName("");
285
+ }
286
+ setShowBlueprintPicker(false);
287
+ }, []);
288
+ // Handle snapshot selection
289
+ const handleSnapshotSelect = React.useCallback((snapshots) => {
290
+ if (snapshots.length > 0) {
291
+ const snapshot = snapshots[0];
292
+ setFormData((prev) => ({
293
+ ...prev,
294
+ snapshot_id: snapshot.id,
295
+ blueprint_id: "",
296
+ }));
297
+ setSelectedSnapshotName(snapshot.name || snapshot.id);
298
+ setSelectedBlueprintName("");
299
+ }
300
+ setShowSnapshotPicker(false);
301
+ }, []);
302
+ // Handle network policy selection
303
+ const handleNetworkPolicySelect = React.useCallback((policies) => {
304
+ if (policies.length > 0) {
305
+ const policy = policies[0];
306
+ setFormData((prev) => ({ ...prev, network_policy_id: policy.id }));
307
+ setSelectedNetworkPolicyName(policy.name || policy.id);
308
+ }
309
+ setShowNetworkPolicyPicker(false);
310
+ }, []);
311
+ // Handle gateway config selection
312
+ const handleGatewaySelect = React.useCallback((configs) => {
313
+ if (configs.length > 0) {
314
+ const config = configs[0];
315
+ const configName = config.name || config.id;
316
+ setPendingGateway({
317
+ id: config.id,
318
+ name: configName,
319
+ endpoint: config.endpoint || "",
320
+ });
321
+ // Auto-fill ENV name from config name (uppercase, underscores, no GWS_ prefix)
322
+ const autoEnvName = configName
323
+ .toUpperCase()
324
+ .replace(/[^A-Z0-9]+/g, "_")
325
+ .replace(/^_|_$/g, "");
326
+ setGatewayEnvPrefix(autoEnvName);
327
+ setShowGatewayPicker(false);
328
+ // Move to env name field in the form
329
+ setGatewayFormField("envName");
330
+ }
331
+ else {
332
+ setShowGatewayPicker(false);
333
+ }
334
+ }, []);
335
+ // Handle secret selection for gateway
336
+ const handleSecretSelect = React.useCallback((secrets) => {
337
+ if (secrets.length > 0) {
338
+ const secret = secrets[0];
339
+ setPendingSecret({ id: secret.id, name: secret.name || secret.id });
340
+ }
341
+ setShowSecretPicker(false);
342
+ // Return to the form at the attach button
343
+ setGatewayFormField("attach");
344
+ }, []);
345
+ // Attach the configured gateway to the devbox
346
+ const handleAttachGateway = React.useCallback(() => {
347
+ if (!pendingGateway || !pendingSecret || !gatewayEnvPrefix.trim())
348
+ return;
349
+ const newGateway = {
350
+ envPrefix: gatewayEnvPrefix.trim(),
351
+ gateway: pendingGateway.id,
352
+ gatewayName: pendingGateway.name,
353
+ gatewayEndpoint: pendingGateway.endpoint,
354
+ secret: pendingSecret.id,
355
+ secretName: pendingSecret.name,
356
+ };
357
+ setFormData((prev) => ({
358
+ ...prev,
359
+ gateways: [...prev.gateways, newGateway],
360
+ }));
361
+ // Reset form
362
+ setPendingGateway(null);
363
+ setPendingSecret(null);
364
+ setGatewayEnvPrefix("");
365
+ setGatewayFormActive(false);
366
+ setGatewayFormField("attach");
367
+ setSelectedGatewayIndex(0);
368
+ }, [pendingGateway, pendingSecret, gatewayEnvPrefix]);
369
+ // Handle clearing source
370
+ const handleClearSource = React.useCallback(() => {
371
+ setFormData((prev) => ({ ...prev, blueprint_id: "", snapshot_id: "" }));
372
+ setSelectedBlueprintName("");
373
+ setSelectedSnapshotName("");
374
+ }, []);
179
375
  // Metadata section input handler - active when in metadata section
180
376
  useInput((input, key) => {
181
377
  const metadataKeys = Object.keys(formData.metadata);
@@ -265,6 +461,117 @@ export const DevboxCreatePage = ({ onBack, onCreate, initialBlueprintId, initial
265
461
  setMetadataInputMode(null);
266
462
  }
267
463
  }, { isActive: inMetadataSection });
464
+ // Gateway section input handler - active when in gateway section
465
+ useInput((input, key) => {
466
+ // === Gateway attach form mode ===
467
+ if (gatewayFormActive) {
468
+ // envName field is a text input - only handle navigation keys
469
+ if (gatewayFormField === "envName") {
470
+ if (key.upArrow || (key.tab && key.shift)) {
471
+ setGatewayFormField("gateway");
472
+ return;
473
+ }
474
+ if (key.downArrow || (key.tab && !key.shift)) {
475
+ setGatewayFormField("secret");
476
+ return;
477
+ }
478
+ if (key.escape) {
479
+ // Cancel the form
480
+ setPendingGateway(null);
481
+ setPendingSecret(null);
482
+ setGatewayEnvPrefix("");
483
+ setGatewayFormActive(false);
484
+ setGatewayFormField("attach");
485
+ return;
486
+ }
487
+ // Let TextInput handle other keys
488
+ return;
489
+ }
490
+ // Navigation between form fields
491
+ if (key.upArrow || (key.tab && key.shift)) {
492
+ const prevIdx = Math.max(0, gatewayFormFieldIndex - 1);
493
+ setGatewayFormField(gatewayFormFields[prevIdx]);
494
+ return;
495
+ }
496
+ if (key.downArrow || (key.tab && !key.shift)) {
497
+ const nextIdx = Math.min(gatewayFormFields.length - 1, gatewayFormFieldIndex + 1);
498
+ setGatewayFormField(gatewayFormFields[nextIdx]);
499
+ return;
500
+ }
501
+ // Enter on specific fields
502
+ if (key.return) {
503
+ if (gatewayFormField === "gateway") {
504
+ setShowGatewayPicker(true);
505
+ return;
506
+ }
507
+ if (gatewayFormField === "secret") {
508
+ setShowSecretPicker(true);
509
+ return;
510
+ }
511
+ if (gatewayFormField === "attach") {
512
+ handleAttachGateway();
513
+ return;
514
+ }
515
+ }
516
+ if (key.escape || input === "q") {
517
+ // Cancel the form
518
+ setPendingGateway(null);
519
+ setPendingSecret(null);
520
+ setGatewayEnvPrefix("");
521
+ setGatewayFormActive(false);
522
+ setGatewayFormField("attach");
523
+ return;
524
+ }
525
+ return;
526
+ }
527
+ // === List navigation mode (existing gateways + attach/done) ===
528
+ const gatewayCount = formData.gateways.length;
529
+ const maxIndex = gatewayCount + 1; // Attach + existing items + Done
530
+ if (key.upArrow && selectedGatewayIndex > 0) {
531
+ setSelectedGatewayIndex(selectedGatewayIndex - 1);
532
+ }
533
+ else if (key.downArrow && selectedGatewayIndex < maxIndex) {
534
+ setSelectedGatewayIndex(selectedGatewayIndex + 1);
535
+ }
536
+ else if (key.return) {
537
+ if (selectedGatewayIndex === 0) {
538
+ // Open the attach form
539
+ setPendingGateway(null);
540
+ setPendingSecret(null);
541
+ setGatewayEnvPrefix("");
542
+ setGatewayFormActive(true);
543
+ setGatewayFormField("gateway");
544
+ }
545
+ else if (selectedGatewayIndex === maxIndex) {
546
+ // Done
547
+ setInGatewaySection(false);
548
+ setSelectedGatewayIndex(0);
549
+ }
550
+ }
551
+ else if ((input === "d" || key.delete) &&
552
+ selectedGatewayIndex >= 1 &&
553
+ selectedGatewayIndex <= gatewayCount) {
554
+ // Remove gateway at index
555
+ const indexToDelete = selectedGatewayIndex - 1;
556
+ const newGateways = [...formData.gateways];
557
+ newGateways.splice(indexToDelete, 1);
558
+ setFormData({ ...formData, gateways: newGateways });
559
+ const newLength = newGateways.length;
560
+ if (selectedGatewayIndex > newLength) {
561
+ setSelectedGatewayIndex(Math.max(0, newLength));
562
+ }
563
+ }
564
+ else if (key.escape || input === "q") {
565
+ setInGatewaySection(false);
566
+ setSelectedGatewayIndex(0);
567
+ }
568
+ }, {
569
+ isActive: inGatewaySection &&
570
+ !showGatewayPicker &&
571
+ !showSecretPicker &&
572
+ !showInlineSecretCreate &&
573
+ !showInlineGatewayConfigCreate,
574
+ });
268
575
  // Validate custom resource configuration
269
576
  const validateCustomResources = () => {
270
577
  if (formData.resource_size !== "CUSTOM_SIZE") {
@@ -342,6 +649,23 @@ export const DevboxCreatePage = ({ onBack, onCreate, initialBlueprintId, initial
342
649
  if (Object.keys(launchParameters).length > 0) {
343
650
  createParams.launch_parameters = launchParameters;
344
651
  }
652
+ // Add gateway specifications
653
+ if (formData.gateways.length > 0) {
654
+ const gateways = {};
655
+ for (const gw of formData.gateways) {
656
+ gateways[gw.envPrefix] = {
657
+ gateway: gw.gateway,
658
+ secret: gw.secret,
659
+ };
660
+ }
661
+ createParams.gateways = gateways;
662
+ }
663
+ // Add tunnel configuration if not "none"
664
+ if (formData.tunnel_auth_mode !== "none") {
665
+ createParams.tunnel = {
666
+ auth_mode: formData.tunnel_auth_mode,
667
+ };
668
+ }
345
669
  const devbox = await client.devboxes.create(createParams);
346
670
  setResult(devbox);
347
671
  }
@@ -367,6 +691,331 @@ export const DevboxCreatePage = ({ onBack, onCreate, initialBlueprintId, initial
367
691
  if (creating) {
368
692
  return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [{ label: "Devboxes" }, { label: "Create", active: true }] }), _jsx(SpinnerComponent, { message: "Creating devbox..." })] }));
369
693
  }
694
+ // Blueprint picker screen
695
+ if (showBlueprintPicker) {
696
+ const blueprintColumns = [
697
+ {
698
+ key: "statusIcon",
699
+ label: "",
700
+ width: 2,
701
+ render: (blueprint, _index, isSelected) => {
702
+ const statusDisplay = getStatusDisplay(blueprint.status || "");
703
+ return (_jsxs(Text, { color: isSelected ? "white" : statusDisplay.color, bold: true, inverse: isSelected, wrap: "truncate", children: [statusDisplay.icon, " "] }));
704
+ },
705
+ },
706
+ createTextColumn("id", "ID", (blueprint) => blueprint.id, {
707
+ width: 25,
708
+ color: colors.idColor,
709
+ }),
710
+ createTextColumn("name", "Name", (blueprint) => blueprint.name || "", { width: 30 }),
711
+ {
712
+ key: "status",
713
+ label: "Status",
714
+ width: 12,
715
+ render: (blueprint, _index, isSelected) => {
716
+ const statusDisplay = getStatusDisplay(blueprint.status || "");
717
+ const padded = statusDisplay.text.slice(0, 12).padEnd(12, " ");
718
+ return (_jsx(Text, { color: isSelected ? "white" : statusDisplay.color, bold: true, inverse: isSelected, wrap: "truncate", children: padded }));
719
+ },
720
+ },
721
+ createTextColumn("created", "Created", (blueprint) => blueprint.create_time_ms
722
+ ? formatTimeAgo(blueprint.create_time_ms)
723
+ : "", { width: 18, color: colors.textDim }),
724
+ ];
725
+ // Filter out failed blueprints
726
+ const failedStatuses = ["failure", "build_failed", "failed"];
727
+ return (_jsx(ResourcePicker, { config: {
728
+ title: "Select Blueprint",
729
+ fetchPage: async (params) => {
730
+ const result = await listBlueprints({
731
+ limit: params.limit,
732
+ startingAfter: params.startingAt,
733
+ search: params.search,
734
+ });
735
+ // Filter out failed blueprints
736
+ const validBlueprints = result.blueprints.filter((bp) => !failedStatuses.includes(bp.status || ""));
737
+ return {
738
+ items: validBlueprints,
739
+ hasMore: result.hasMore,
740
+ totalCount: validBlueprints.length,
741
+ };
742
+ },
743
+ getItemId: (blueprint) => blueprint.id,
744
+ getItemLabel: (blueprint) => blueprint.name || blueprint.id,
745
+ columns: blueprintColumns,
746
+ mode: "single",
747
+ emptyMessage: "No blueprints found (failed blueprints are hidden)",
748
+ searchPlaceholder: "Search blueprints...",
749
+ breadcrumbItems: [
750
+ { label: "Devboxes" },
751
+ { label: "Create" },
752
+ { label: "Select Blueprint", active: true },
753
+ ],
754
+ }, onSelect: handleBlueprintSelect, onCancel: () => setShowBlueprintPicker(false), initialSelected: formData.blueprint_id ? [formData.blueprint_id] : [] }));
755
+ }
756
+ // Snapshot picker screen
757
+ if (showSnapshotPicker) {
758
+ const snapshotColumns = [
759
+ createTextColumn("id", "ID", (snapshot) => snapshot.id, {
760
+ width: 25,
761
+ color: colors.idColor,
762
+ }),
763
+ createTextColumn("name", "Name", (snapshot) => snapshot.name || "", { width: 30 }),
764
+ createTextColumn("status", "Status", (snapshot) => snapshot.status || "", { width: 12 }),
765
+ createTextColumn("created", "Created", (snapshot) => snapshot.create_time_ms ? formatTimeAgo(snapshot.create_time_ms) : "", { width: 18, color: colors.textDim }),
766
+ ];
767
+ return (_jsx(ResourcePicker, { config: {
768
+ title: "Select Snapshot",
769
+ fetchPage: async (params) => {
770
+ const result = await listSnapshots({
771
+ limit: params.limit,
772
+ startingAfter: params.startingAt,
773
+ });
774
+ return {
775
+ items: result.snapshots,
776
+ hasMore: result.hasMore,
777
+ totalCount: result.totalCount,
778
+ };
779
+ },
780
+ getItemId: (snapshot) => snapshot.id,
781
+ getItemLabel: (snapshot) => snapshot.name || snapshot.id,
782
+ columns: snapshotColumns,
783
+ mode: "single",
784
+ emptyMessage: "No snapshots found",
785
+ searchPlaceholder: "Search snapshots...",
786
+ breadcrumbItems: [
787
+ { label: "Devboxes" },
788
+ { label: "Create" },
789
+ { label: "Select Snapshot", active: true },
790
+ ],
791
+ }, onSelect: handleSnapshotSelect, onCancel: () => setShowSnapshotPicker(false), initialSelected: formData.snapshot_id ? [formData.snapshot_id] : [] }));
792
+ }
793
+ // Network policy picker screen
794
+ if (showNetworkPolicyPicker) {
795
+ // Helper to get egress type label
796
+ const getEgressLabel = (egress) => {
797
+ if (egress.allow_all)
798
+ return "Allow All";
799
+ if (egress.allowed_hostnames?.length === 0)
800
+ return "Deny All";
801
+ return `Custom (${egress.allowed_hostnames?.length || 0})`;
802
+ };
803
+ const buildNetworkPolicyColumns = (tw) => {
804
+ const fixedWidth = 6;
805
+ const idWidth = 25;
806
+ const egressWidth = 15;
807
+ const timeWidth = 18;
808
+ const baseWidth = fixedWidth + idWidth + egressWidth + timeWidth;
809
+ const nameWidth = Math.min(80, Math.max(15, tw - baseWidth));
810
+ return [
811
+ createTextColumn("id", "ID", (policy) => policy.id, {
812
+ width: idWidth + 1,
813
+ color: colors.idColor,
814
+ }),
815
+ createTextColumn("name", "Name", (policy) => policy.name || "", { width: nameWidth }),
816
+ createTextColumn("egress", "Egress", (policy) => getEgressLabel(policy.egress), { width: egressWidth }),
817
+ createTextColumn("created", "Created", (policy) => policy.create_time_ms ? formatTimeAgo(policy.create_time_ms) : "", { width: timeWidth, color: colors.textDim }),
818
+ ];
819
+ };
820
+ return (_jsx(ResourcePicker, { config: {
821
+ title: "Select Network Policy",
822
+ fetchPage: async (params) => {
823
+ const result = await listNetworkPolicies({
824
+ limit: params.limit,
825
+ startingAfter: params.startingAt,
826
+ search: params.search,
827
+ });
828
+ return {
829
+ items: result.networkPolicies,
830
+ hasMore: result.hasMore,
831
+ totalCount: result.totalCount,
832
+ };
833
+ },
834
+ getItemId: (policy) => policy.id,
835
+ getItemLabel: (policy) => policy.name || policy.id,
836
+ columns: buildNetworkPolicyColumns,
837
+ mode: "single",
838
+ emptyMessage: "No network policies found",
839
+ searchPlaceholder: "Search network policies...",
840
+ breadcrumbItems: [
841
+ { label: "Devboxes" },
842
+ { label: "Create" },
843
+ { label: "Select Network Policy", active: true },
844
+ ],
845
+ }, onSelect: handleNetworkPolicySelect, onCancel: () => setShowNetworkPolicyPicker(false), initialSelected: formData.network_policy_id ? [formData.network_policy_id] : [] }));
846
+ }
847
+ // Inline gateway config creation screen (from gateway attach flow)
848
+ if (showInlineGatewayConfigCreate) {
849
+ return (_jsx(GatewayConfigCreatePage, { onBack: () => {
850
+ setShowInlineGatewayConfigCreate(false);
851
+ // Return to gateway picker
852
+ setShowGatewayPicker(true);
853
+ }, onCreate: (config) => {
854
+ setShowInlineGatewayConfigCreate(false);
855
+ // Auto-select the newly created gateway config
856
+ const configName = config.name || config.id;
857
+ setPendingGateway({
858
+ id: config.id,
859
+ name: configName,
860
+ endpoint: config.endpoint || "",
861
+ });
862
+ const autoEnvName = configName
863
+ .toUpperCase()
864
+ .replace(/[^A-Z0-9]+/g, "_")
865
+ .replace(/^_|_$/g, "");
866
+ setGatewayEnvPrefix(autoEnvName);
867
+ setShowGatewayPicker(false);
868
+ setGatewayFormField("envName");
869
+ } }));
870
+ }
871
+ // Gateway config picker screen
872
+ if (showGatewayPicker) {
873
+ const buildGatewayColumns = (tw) => {
874
+ const fixedWidth = 6;
875
+ const idWidth = 25;
876
+ const timeWidth = 20;
877
+ const showEndpoint = tw >= 100;
878
+ const endpointWidth = Math.max(20, tw >= 140 ? 40 : 25);
879
+ const baseWidth = fixedWidth + idWidth + timeWidth;
880
+ const optionalWidth = showEndpoint ? endpointWidth : 0;
881
+ const nameWidth = Math.min(80, Math.max(15, tw - baseWidth - optionalWidth));
882
+ return [
883
+ createTextColumn("id", "ID", (config) => config.id, {
884
+ width: idWidth + 1,
885
+ color: colors.idColor,
886
+ }),
887
+ createTextColumn("name", "Name", (config) => config.name || "", { width: nameWidth }),
888
+ ...(showEndpoint
889
+ ? [
890
+ createTextColumn("endpoint", "Endpoint", (config) => config.endpoint || "", { width: endpointWidth, color: colors.textDim }),
891
+ ]
892
+ : []),
893
+ createTextColumn("created", "Created", (config) => config.create_time_ms ? formatTimeAgo(config.create_time_ms) : "", { width: timeWidth, color: colors.textDim }),
894
+ ];
895
+ };
896
+ return (_jsx(ResourcePicker, { config: {
897
+ title: "Select AI Gateway Config",
898
+ fetchPage: async (params) => {
899
+ const result = await listGatewayConfigs({
900
+ limit: params.limit,
901
+ startingAfter: params.startingAt,
902
+ search: params.search,
903
+ });
904
+ return {
905
+ items: result.gatewayConfigs,
906
+ hasMore: result.hasMore,
907
+ totalCount: result.totalCount,
908
+ };
909
+ },
910
+ getItemId: (config) => config.id,
911
+ getItemLabel: (config) => config.name || config.id,
912
+ columns: buildGatewayColumns,
913
+ mode: "single",
914
+ emptyMessage: "No AI gateway configs found",
915
+ searchPlaceholder: "Search AI gateway configs...",
916
+ breadcrumbItems: [
917
+ { label: "Devboxes" },
918
+ { label: "Create" },
919
+ { label: "Attach AI Gateway Config" },
920
+ { label: "Select Config", active: true },
921
+ ],
922
+ onCreateNew: () => {
923
+ setShowGatewayPicker(false);
924
+ setShowInlineGatewayConfigCreate(true);
925
+ },
926
+ createNewLabel: "Create AI gateway config",
927
+ }, onSelect: handleGatewaySelect, onCancel: () => {
928
+ setShowGatewayPicker(false);
929
+ // Return to the form at the gateway field
930
+ setGatewayFormField("gateway");
931
+ }, initialSelected: [] }, "gateway-config-picker"));
932
+ }
933
+ // Inline secret creation screen (from gateway flow)
934
+ if (showInlineSecretCreate) {
935
+ return (_jsx(SecretCreatePage, { onBack: () => {
936
+ setShowInlineSecretCreate(false);
937
+ // Return to secret picker
938
+ setShowSecretPicker(true);
939
+ }, onCreate: (secret) => {
940
+ setShowInlineSecretCreate(false);
941
+ // Store as pending secret and return to the attach form
942
+ setPendingSecret({
943
+ id: secret.id,
944
+ name: secret.name || secret.id,
945
+ });
946
+ setShowSecretPicker(false);
947
+ setGatewayFormField("attach");
948
+ } }));
949
+ }
950
+ // Secret picker screen (for gateway)
951
+ if (showSecretPicker) {
952
+ const buildSecretColumns = (tw) => {
953
+ const fixedWidth = 6;
954
+ const idWidth = 30;
955
+ const timeWidth = 20;
956
+ const baseWidth = fixedWidth + idWidth + timeWidth;
957
+ const nameWidth = Math.min(80, Math.max(15, tw - baseWidth));
958
+ return [
959
+ createTextColumn("id", "ID", (secret) => secret.id, {
960
+ width: idWidth + 1,
961
+ color: colors.idColor,
962
+ }),
963
+ createTextColumn("name", "Name", (secret) => secret.name || "", { width: nameWidth }),
964
+ createTextColumn("created", "Created", (secret) => secret.create_time_ms ? formatTimeAgo(secret.create_time_ms) : "", { width: timeWidth, color: colors.textDim }),
965
+ ];
966
+ };
967
+ return (_jsx(ResourcePicker, { config: {
968
+ title: "Select Secret for Gateway",
969
+ fetchPage: async (params) => {
970
+ const client = getClient();
971
+ // Secrets API doesn't support cursor pagination, so we fetch all
972
+ // and do client-side pagination by slicing to the requested page
973
+ const page = await client.secrets.list({
974
+ limit: 1000,
975
+ });
976
+ const allSecrets = (page.secrets || []).map((s) => ({
977
+ id: s.id,
978
+ name: s.name,
979
+ create_time_ms: s.create_time_ms,
980
+ }));
981
+ // Client-side cursor pagination
982
+ let startIdx = 0;
983
+ if (params.startingAt) {
984
+ const cursorIdx = allSecrets.findIndex((s) => s.id === params.startingAt);
985
+ if (cursorIdx >= 0) {
986
+ startIdx = cursorIdx + 1;
987
+ }
988
+ }
989
+ const sliced = allSecrets.slice(startIdx, startIdx + params.limit);
990
+ return {
991
+ items: sliced,
992
+ hasMore: startIdx + params.limit < allSecrets.length,
993
+ totalCount: allSecrets.length,
994
+ };
995
+ },
996
+ getItemId: (secret) => secret.id,
997
+ getItemLabel: (secret) => secret.name || secret.id,
998
+ columns: buildSecretColumns,
999
+ mode: "single",
1000
+ emptyMessage: "No secrets found",
1001
+ searchPlaceholder: "Search secrets...",
1002
+ breadcrumbItems: [
1003
+ { label: "Devboxes" },
1004
+ { label: "Create" },
1005
+ { label: "Attach AI Gateway Config" },
1006
+ { label: "Select Secret", active: true },
1007
+ ],
1008
+ onCreateNew: () => {
1009
+ setShowSecretPicker(false);
1010
+ setShowInlineSecretCreate(true);
1011
+ },
1012
+ createNewLabel: "Create secret",
1013
+ }, onSelect: handleSecretSelect, onCancel: () => {
1014
+ setShowSecretPicker(false);
1015
+ // Return to the form at the secret field
1016
+ setGatewayFormField("secret");
1017
+ }, initialSelected: [] }, "secret-picker"));
1018
+ }
370
1019
  // Form screen
371
1020
  return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [{ label: "Devboxes" }, { label: "Create", active: true }] }), _jsx(Box, { flexDirection: "column", marginBottom: 1, children: fields.map((field) => {
372
1021
  const isActive = currentField === field.key;
@@ -379,7 +1028,49 @@ export const DevboxCreatePage = ({ onBack, onCreate, initialBlueprintId, initial
379
1028
  }
380
1029
  if (field.type === "select") {
381
1030
  const value = fieldData;
382
- return (_jsx(FormSelect, { label: field.label, value: value || "", options: field.key === "architecture" ? architectures : resourceSizes, onChange: (newValue) => setFormData({ ...formData, [field.key]: newValue }), isActive: isActive }, field.key));
1031
+ let options;
1032
+ if (field.key === "architecture") {
1033
+ options = architectures;
1034
+ }
1035
+ else if (field.key === "resource_size") {
1036
+ options = resourceSizes;
1037
+ }
1038
+ else if (field.key === "tunnel_auth_mode") {
1039
+ options = tunnelAuthModes;
1040
+ }
1041
+ else {
1042
+ options = [];
1043
+ }
1044
+ return (_jsx(FormSelect, { label: field.label, value: value || "", options: options, onChange: (newValue) => setFormData({ ...formData, [field.key]: newValue }), isActive: isActive }, field.key));
1045
+ }
1046
+ if (field.type === "source") {
1047
+ // Check if either blueprint or snapshot is selected
1048
+ const selectedBlueprintValue = selectedBlueprintName || formData.blueprint_id;
1049
+ const selectedSnapshotValue = selectedSnapshotName || formData.snapshot_id;
1050
+ const hasBlueprint = !!selectedBlueprintValue;
1051
+ const hasSnapshot = !!selectedSnapshotValue;
1052
+ const hasSelection = hasBlueprint || hasSnapshot;
1053
+ // If something is selected, show it clearly with its type
1054
+ if (hasSelection) {
1055
+ const selectedType = hasBlueprint ? "Blueprint" : "Snapshot";
1056
+ const selectedValue = hasBlueprint
1057
+ ? selectedBlueprintValue
1058
+ : selectedSnapshotValue;
1059
+ return (_jsxs(Box, { marginBottom: 0, children: [_jsxs(Text, { color: isActive ? colors.primary : colors.textDim, children: [isActive ? figures.pointer : " ", " ", field.label, ":", " "] }), _jsxs(Text, { color: colors.success, children: [selectedType, ": "] }), _jsx(Text, { color: colors.idColor, children: selectedValue }), isActive && (_jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "[Enter to change, d to clear]"] }))] }, field.key));
1060
+ }
1061
+ // Nothing selected - show toggle to choose type
1062
+ return (_jsxs(Box, { marginBottom: 0, children: [_jsxs(Text, { color: isActive ? colors.primary : colors.textDim, children: [isActive ? figures.pointer : " ", " ", field.label, ":", " "] }), _jsxs(Text, { color: isActive ? colors.text : colors.textDim, children: [isActive ? figures.arrowLeft : "", " "] }), _jsx(Text, { color: sourceTypeToggle === "blueprint"
1063
+ ? colors.primary
1064
+ : colors.textDim, bold: sourceTypeToggle === "blueprint", children: "Blueprint" }), _jsx(Text, { color: colors.textDim, children: " / " }), _jsx(Text, { color: sourceTypeToggle === "snapshot"
1065
+ ? colors.primary
1066
+ : colors.textDim, bold: sourceTypeToggle === "snapshot", children: "Snapshot" }), _jsxs(Text, { color: isActive ? colors.text : colors.textDim, children: [" ", isActive ? figures.arrowRight : ""] }), isActive && (_jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "[Enter to select]"] }))] }, field.key));
1067
+ }
1068
+ if (field.type === "picker") {
1069
+ const value = fieldData;
1070
+ const displayName = field.key === "network_policy_id"
1071
+ ? selectedNetworkPolicyName || value
1072
+ : value;
1073
+ return (_jsxs(Box, { marginBottom: 0, children: [_jsxs(Text, { color: isActive ? colors.primary : colors.textDim, children: [isActive ? figures.pointer : " ", " ", field.label, ":", " "] }), displayName ? (_jsx(Text, { color: colors.idColor, children: displayName })) : (_jsx(Text, { color: colors.textDim, dimColor: true, children: field.placeholder || "(none)" })), isActive && (_jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "[Enter to ", displayName ? "change" : "select", "]"] }))] }, field.key));
383
1074
  }
384
1075
  if (field.type === "metadata") {
385
1076
  if (!inMetadataSection) {
@@ -414,9 +1105,66 @@ export const DevboxCreatePage = ({ onBack, onCreate, initialBlueprintId, initial
414
1105
  ? `[Tab] Switch field • [Enter] ${metadataInputMode === "key" ? "Next" : "Save"} • [esc] Cancel`
415
1106
  : `${figures.arrowUp}${figures.arrowDown} Navigate • [Enter] ${selectedMetadataIndex === 0 ? "Add" : selectedMetadataIndex === maxIndex ? "Done" : "Edit"} • [d] Delete • [esc] Back` }) })] }, field.key));
416
1107
  }
1108
+ if (field.type === "gateways") {
1109
+ if (!inGatewaySection) {
1110
+ // Collapsed view
1111
+ return (_jsxs(Box, { flexDirection: "column", marginBottom: 0, children: [_jsxs(Box, { children: [_jsxs(Text, { color: isActive ? colors.primary : colors.textDim, children: [isActive ? figures.pointer : " ", " ", field.label, ":", " "] }), _jsxs(Text, { color: colors.text, children: [formData.gateways.length, " configured"] }), isActive && (_jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "[Enter to manage]"] }))] }), formData.gateways.length > 0 && (_jsx(Box, { marginLeft: 2, flexDirection: "column", children: formData.gateways.map((gw, idx) => (_jsxs(Text, { color: colors.textDim, dimColor: true, children: [figures.pointer, " ENV: ", gw.envPrefix, " | Config:", " ", gw.gatewayName, " (", gw.gatewayEndpoint, ") | Secret:", " ", gw.secretName] }, idx))) }))] }, field.key));
1112
+ }
1113
+ // Expanded gateway section view
1114
+ const gatewayCount = formData.gateways.length;
1115
+ const maxGatewayIndex = gatewayCount + 1;
1116
+ const canAttach = !!pendingGateway && !!pendingSecret && !!gatewayEnvPrefix.trim();
1117
+ return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: colors.primary, paddingX: 1, paddingY: 1, marginBottom: 1, children: [_jsxs(Text, { color: colors.primary, bold: true, children: [figures.hamburger, " Configure AI Gateway Configs for Devbox"] }), gatewayFormActive && (_jsxs(Box, { flexDirection: "column", marginTop: 1, borderStyle: "single", borderColor: colors.success, paddingX: 1, children: [_jsx(Text, { color: colors.success, bold: true, children: "Attach AI Gateway Config" }), _jsxs(Box, { marginTop: 1, children: [_jsxs(Text, { color: gatewayFormField === "attach"
1118
+ ? canAttach
1119
+ ? colors.success
1120
+ : colors.primary
1121
+ : colors.textDim, children: [gatewayFormField === "attach"
1122
+ ? figures.pointer
1123
+ : " ", " "] }), _jsx(Text, { color: gatewayFormField === "attach"
1124
+ ? canAttach
1125
+ ? colors.success
1126
+ : colors.primary
1127
+ : colors.textDim, bold: gatewayFormField === "attach", children: canAttach
1128
+ ? `${figures.tick} Attach Gateway`
1129
+ : `${figures.ellipsis} Attach Gateway (fill fields below)` })] }), _jsxs(Box, { marginTop: 1, children: [_jsxs(Text, { color: gatewayFormField === "gateway"
1130
+ ? colors.primary
1131
+ : colors.textDim, children: [gatewayFormField === "gateway" ? figures.pointer : " ", " ", "AI Gateway Config:", " "] }), pendingGateway ? (_jsxs(Text, { color: colors.success, children: [pendingGateway.name, " ", _jsxs(Text, { color: colors.textDim, dimColor: true, children: ["(", pendingGateway.endpoint, ")"] })] })) : (_jsx(Text, { color: colors.textDim, dimColor: true, children: "(none selected)" })), gatewayFormField === "gateway" && (_jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "[Enter to select]"] }))] }), _jsxs(Box, { children: [_jsxs(Text, { color: gatewayFormField === "envName"
1132
+ ? colors.primary
1133
+ : colors.textDim, children: [gatewayFormField === "envName" ? figures.pointer : " ", " ", "ENV Name:", " "] }), gatewayFormField === "envName" ? (_jsx(TextInput, { value: gatewayEnvPrefix || "", onChange: setGatewayEnvPrefix, placeholder: "ANTHROPIC" })) : (_jsx(Text, { color: gatewayEnvPrefix ? colors.text : colors.textDim, dimColor: !gatewayEnvPrefix, children: gatewayEnvPrefix || "(auto-filled from config)" }))] }), _jsxs(Box, { children: [_jsxs(Text, { color: gatewayFormField === "secret"
1134
+ ? colors.primary
1135
+ : colors.textDim, children: [gatewayFormField === "secret" ? figures.pointer : " ", " ", "Secret:", " "] }), pendingSecret ? (_jsx(Text, { color: colors.success, children: pendingSecret.name })) : (_jsx(Text, { color: colors.textDim, dimColor: true, children: "(none selected)" })), gatewayFormField === "secret" && (_jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "[Enter to select]"] }))] }), _jsx(Box, { marginTop: 1, borderStyle: "single", borderColor: colors.border, paddingX: 1, children: _jsx(Text, { color: colors.textDim, dimColor: true, children: gatewayFormField === "envName"
1136
+ ? `Type to edit • ${figures.arrowUp}${figures.arrowDown} Navigate • [esc] Cancel`
1137
+ : gatewayFormField === "attach"
1138
+ ? canAttach
1139
+ ? `[Enter] Attach • ${figures.arrowUp}${figures.arrowDown} Navigate • [esc] Cancel`
1140
+ : `${figures.arrowUp}${figures.arrowDown} Navigate • [esc] Cancel`
1141
+ : `[Enter] Select • ${figures.arrowUp}${figures.arrowDown} Navigate • [esc] Cancel` }) })] })), !gatewayFormActive && (_jsxs(_Fragment, { children: [_jsxs(Box, { marginTop: 1, children: [_jsxs(Text, { color: selectedGatewayIndex === 0
1142
+ ? colors.primary
1143
+ : colors.textDim, children: [selectedGatewayIndex === 0
1144
+ ? figures.pointer
1145
+ : " ", " "] }), _jsx(Text, { color: selectedGatewayIndex === 0
1146
+ ? colors.success
1147
+ : colors.textDim, bold: selectedGatewayIndex === 0, children: "+ Attach AI gateway config" })] }), gatewayCount > 0 && (_jsx(Box, { flexDirection: "column", marginTop: 1, children: formData.gateways.map((gw, index) => {
1148
+ const itemIndex = index + 1;
1149
+ const isGatewaySelected = selectedGatewayIndex === itemIndex;
1150
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { children: [_jsxs(Text, { color: isGatewaySelected
1151
+ ? colors.primary
1152
+ : colors.textDim, children: [isGatewaySelected
1153
+ ? figures.pointer
1154
+ : " ", " "] }), _jsxs(Text, { color: isGatewaySelected
1155
+ ? colors.primary
1156
+ : colors.textDim, bold: isGatewaySelected, children: ["ENV: ", gw.envPrefix] })] }), _jsxs(Box, { marginLeft: 3, flexDirection: "column", children: [_jsxs(Text, { color: colors.textDim, dimColor: true, children: ["Gateway Config: ", gw.gatewayName, " (", gw.gateway, ")"] }), _jsxs(Text, { color: colors.textDim, dimColor: true, children: ["Endpoint: ", gw.gatewayEndpoint] }), _jsxs(Text, { color: colors.textDim, dimColor: true, children: ["Secret: ", gw.secretName, " (", gw.secret, ")"] })] })] }, gw.envPrefix));
1157
+ }) })), _jsxs(Box, { marginTop: 1, children: [_jsxs(Text, { color: selectedGatewayIndex === maxGatewayIndex
1158
+ ? colors.primary
1159
+ : colors.textDim, children: [selectedGatewayIndex === maxGatewayIndex
1160
+ ? figures.pointer
1161
+ : " ", " "] }), _jsxs(Text, { color: selectedGatewayIndex === maxGatewayIndex
1162
+ ? colors.success
1163
+ : colors.textDim, bold: selectedGatewayIndex === maxGatewayIndex, children: [figures.tick, " Done"] })] }), _jsx(Box, { marginTop: 1, borderStyle: "single", borderColor: colors.border, paddingX: 1, children: _jsx(Text, { color: colors.textDim, dimColor: true, children: `${figures.arrowUp}${figures.arrowDown} Navigate • [Enter] ${selectedGatewayIndex === 0 ? "Attach" : selectedGatewayIndex === maxGatewayIndex ? "Done" : "Select"} • [d] Remove • [esc] Back` }) })] }))] }, field.key));
1164
+ }
417
1165
  return null;
418
1166
  }) }), formData.resource_size === "CUSTOM_SIZE" &&
419
- validateCustomResources() && (_jsxs(Box, { borderStyle: "round", borderColor: colors.error, paddingX: 1, paddingY: 0, marginTop: 1, children: [_jsxs(Text, { color: colors.error, bold: true, children: [figures.cross, " Validation Error"] }), _jsx(Text, { color: colors.error, dimColor: true, children: validateCustomResources() })] })), !inMetadataSection && (_jsx(NavigationTips, { showArrows: true, tips: [
1167
+ validateCustomResources() && (_jsxs(Box, { borderStyle: "round", borderColor: colors.error, paddingX: 1, paddingY: 0, marginTop: 1, children: [_jsxs(Text, { color: colors.error, bold: true, children: [figures.cross, " Validation Error"] }), _jsx(Text, { color: colors.error, dimColor: true, children: validateCustomResources() })] })), !inMetadataSection && !inGatewaySection && (_jsx(NavigationTips, { showArrows: true, tips: [
420
1168
  { key: "Enter", label: "Create" },
421
1169
  { key: "q", label: "Cancel" },
422
1170
  ] }))] }));