@runloop/rl-cli 1.9.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.
@@ -20,6 +20,8 @@ import { listBlueprints } from "../services/blueprintService.js";
20
20
  import { listSnapshots } from "../services/snapshotService.js";
21
21
  import { listNetworkPolicies } from "../services/networkPolicyService.js";
22
22
  import { listGatewayConfigs } from "../services/gatewayConfigService.js";
23
+ import { SecretCreatePage } from "./SecretCreatePage.js";
24
+ import { GatewayConfigCreatePage } from "./GatewayConfigCreatePage.js";
23
25
  const sourceTypes = ["blueprint", "snapshot"];
24
26
  const architectures = ["arm64", "x86_64"];
25
27
  const resourceSizes = [
@@ -31,6 +33,7 @@ const resourceSizes = [
31
33
  "XX_LARGE",
32
34
  "CUSTOM_SIZE",
33
35
  ];
36
+ const tunnelAuthModes = ["none", "open", "authenticated"];
34
37
  export const DevboxCreatePage = ({ onBack, onCreate, initialBlueprintId, initialSnapshotId, }) => {
35
38
  const [currentField, setCurrentField] = React.useState("create");
36
39
  const [formData, setFormData] = React.useState({
@@ -45,6 +48,7 @@ export const DevboxCreatePage = ({ onBack, onCreate, initialBlueprintId, initial
45
48
  blueprint_id: initialBlueprintId || "",
46
49
  snapshot_id: initialSnapshotId || "",
47
50
  network_policy_id: "",
51
+ tunnel_auth_mode: "none",
48
52
  gateways: [],
49
53
  });
50
54
  const [metadataKey, setMetadataKey] = React.useState("");
@@ -68,9 +72,16 @@ export const DevboxCreatePage = ({ onBack, onCreate, initialBlueprintId, initial
68
72
  const [showSecretPicker, setShowSecretPicker] = React.useState(false);
69
73
  const [inGatewaySection, setInGatewaySection] = React.useState(false);
70
74
  const [gatewayEnvPrefix, setGatewayEnvPrefix] = React.useState("");
71
- const [gatewayInputMode, setGatewayInputMode] = React.useState(null);
72
75
  const [selectedGatewayIndex, setSelectedGatewayIndex] = React.useState(0);
73
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);
74
85
  const baseFields = [
75
86
  { key: "create", label: "Devbox Create", type: "action" },
76
87
  { key: "name", label: "Name", type: "text", placeholder: "my-devbox" },
@@ -119,9 +130,15 @@ export const DevboxCreatePage = ({ onBack, onCreate, initialBlueprintId, initial
119
130
  type: "picker",
120
131
  placeholder: "Select a network policy...",
121
132
  },
133
+ {
134
+ key: "tunnel_auth_mode",
135
+ label: "Tunnel (optional)",
136
+ type: "select",
137
+ placeholder: "none",
138
+ },
122
139
  {
123
140
  key: "gateways",
124
- label: "Gateways (optional)",
141
+ label: "AI Gateway Configs (optional)",
125
142
  type: "gateways",
126
143
  placeholder: "Configure API credential proxying...",
127
144
  },
@@ -134,6 +151,7 @@ export const DevboxCreatePage = ({ onBack, onCreate, initialBlueprintId, initial
134
151
  // Select navigation handlers using shared hook
135
152
  const handleArchitectureNav = useFormSelectNavigation(formData.architecture, architectures, (value) => setFormData({ ...formData, architecture: value }), currentField === "architecture");
136
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");
137
155
  const handleSourceTypeNav = useFormSelectNavigation(sourceTypeToggle, sourceTypes, (value) => setSourceTypeToggle(value), currentField === "source");
138
156
  // Main form input handler - active when not in metadata section
139
157
  useInput((input, key) => {
@@ -228,6 +246,8 @@ export const DevboxCreatePage = ({ onBack, onCreate, initialBlueprintId, initial
228
246
  return;
229
247
  if (handleResourceSizeNav(input, key))
230
248
  return;
249
+ if (handleTunnelNav(input, key))
250
+ return;
231
251
  if (handleSourceTypeNav(input, key))
232
252
  return;
233
253
  // Navigation (up/down arrows and tab/shift+tab)
@@ -247,7 +267,9 @@ export const DevboxCreatePage = ({ onBack, onCreate, initialBlueprintId, initial
247
267
  !showSnapshotPicker &&
248
268
  !showNetworkPolicyPicker &&
249
269
  !showGatewayPicker &&
250
- !showSecretPicker,
270
+ !showSecretPicker &&
271
+ !showInlineSecretCreate &&
272
+ !showInlineGatewayConfigCreate,
251
273
  });
252
274
  // Handle blueprint selection
253
275
  const handleBlueprintSelect = React.useCallback((blueprints) => {
@@ -290,10 +312,21 @@ export const DevboxCreatePage = ({ onBack, onCreate, initialBlueprintId, initial
290
312
  const handleGatewaySelect = React.useCallback((configs) => {
291
313
  if (configs.length > 0) {
292
314
  const config = configs[0];
293
- setPendingGateway({ id: config.id, name: config.name || config.id });
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);
294
327
  setShowGatewayPicker(false);
295
- // Now show secret picker
296
- setShowSecretPicker(true);
328
+ // Move to env name field in the form
329
+ setGatewayFormField("envName");
297
330
  }
298
331
  else {
299
332
  setShowGatewayPicker(false);
@@ -301,26 +334,38 @@ export const DevboxCreatePage = ({ onBack, onCreate, initialBlueprintId, initial
301
334
  }, []);
302
335
  // Handle secret selection for gateway
303
336
  const handleSecretSelect = React.useCallback((secrets) => {
304
- if (secrets.length > 0 && pendingGateway && gatewayEnvPrefix) {
337
+ if (secrets.length > 0) {
305
338
  const secret = secrets[0];
306
- const newGateway = {
307
- envPrefix: gatewayEnvPrefix,
308
- gateway: pendingGateway.id,
309
- gatewayName: pendingGateway.name,
310
- secret: secret.id,
311
- secretName: secret.name || secret.id,
312
- };
313
- setFormData((prev) => ({
314
- ...prev,
315
- gateways: [...prev.gateways, newGateway],
316
- }));
339
+ setPendingSecret({ id: secret.id, name: secret.name || secret.id });
317
340
  }
318
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
319
362
  setPendingGateway(null);
363
+ setPendingSecret(null);
320
364
  setGatewayEnvPrefix("");
321
- setGatewayInputMode(null);
365
+ setGatewayFormActive(false);
366
+ setGatewayFormField("attach");
322
367
  setSelectedGatewayIndex(0);
323
- }, [pendingGateway, gatewayEnvPrefix]);
368
+ }, [pendingGateway, pendingSecret, gatewayEnvPrefix]);
324
369
  // Handle clearing source
325
370
  const handleClearSource = React.useCallback(() => {
326
371
  setFormData((prev) => ({ ...prev, blueprint_id: "", snapshot_id: "" }));
@@ -418,24 +463,70 @@ export const DevboxCreatePage = ({ onBack, onCreate, initialBlueprintId, initial
418
463
  }, { isActive: inMetadataSection });
419
464
  // Gateway section input handler - active when in gateway section
420
465
  useInput((input, key) => {
421
- const gatewayCount = formData.gateways.length;
422
- const maxIndex = gatewayCount + 1; // Add new + existing items + Done
423
- // Handle input mode (typing env prefix)
424
- if (gatewayInputMode === "envPrefix") {
425
- if (key.return && gatewayEnvPrefix.trim()) {
426
- // Open gateway picker
427
- setGatewayInputMode(null);
428
- setShowGatewayPicker(true);
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
429
488
  return;
430
489
  }
431
- else if (key.escape) {
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);
432
520
  setGatewayEnvPrefix("");
433
- setGatewayInputMode(null);
521
+ setGatewayFormActive(false);
522
+ setGatewayFormField("attach");
434
523
  return;
435
524
  }
436
525
  return;
437
526
  }
438
- // Navigation mode in gateway section
527
+ // === List navigation mode (existing gateways + attach/done) ===
528
+ const gatewayCount = formData.gateways.length;
529
+ const maxIndex = gatewayCount + 1; // Attach + existing items + Done
439
530
  if (key.upArrow && selectedGatewayIndex > 0) {
440
531
  setSelectedGatewayIndex(selectedGatewayIndex - 1);
441
532
  }
@@ -444,22 +535,23 @@ export const DevboxCreatePage = ({ onBack, onCreate, initialBlueprintId, initial
444
535
  }
445
536
  else if (key.return) {
446
537
  if (selectedGatewayIndex === 0) {
447
- // Add new gateway - start with env prefix input
538
+ // Open the attach form
539
+ setPendingGateway(null);
540
+ setPendingSecret(null);
448
541
  setGatewayEnvPrefix("");
449
- setGatewayInputMode("envPrefix");
542
+ setGatewayFormActive(true);
543
+ setGatewayFormField("gateway");
450
544
  }
451
545
  else if (selectedGatewayIndex === maxIndex) {
452
546
  // Done
453
547
  setInGatewaySection(false);
454
548
  setSelectedGatewayIndex(0);
455
- setGatewayEnvPrefix("");
456
- setGatewayInputMode(null);
457
549
  }
458
550
  }
459
551
  else if ((input === "d" || key.delete) &&
460
552
  selectedGatewayIndex >= 1 &&
461
553
  selectedGatewayIndex <= gatewayCount) {
462
- // Delete gateway at index
554
+ // Remove gateway at index
463
555
  const indexToDelete = selectedGatewayIndex - 1;
464
556
  const newGateways = [...formData.gateways];
465
557
  newGateways.splice(indexToDelete, 1);
@@ -472,10 +564,14 @@ export const DevboxCreatePage = ({ onBack, onCreate, initialBlueprintId, initial
472
564
  else if (key.escape || input === "q") {
473
565
  setInGatewaySection(false);
474
566
  setSelectedGatewayIndex(0);
475
- setGatewayEnvPrefix("");
476
- setGatewayInputMode(null);
477
567
  }
478
- }, { isActive: inGatewaySection && !showGatewayPicker && !showSecretPicker });
568
+ }, {
569
+ isActive: inGatewaySection &&
570
+ !showGatewayPicker &&
571
+ !showSecretPicker &&
572
+ !showInlineSecretCreate &&
573
+ !showInlineGatewayConfigCreate,
574
+ });
479
575
  // Validate custom resource configuration
480
576
  const validateCustomResources = () => {
481
577
  if (formData.resource_size !== "CUSTOM_SIZE") {
@@ -564,6 +660,12 @@ export const DevboxCreatePage = ({ onBack, onCreate, initialBlueprintId, initial
564
660
  }
565
661
  createParams.gateways = gateways;
566
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
+ }
567
669
  const devbox = await client.devboxes.create(createParams);
568
670
  setResult(devbox);
569
671
  }
@@ -698,15 +800,23 @@ export const DevboxCreatePage = ({ onBack, onCreate, initialBlueprintId, initial
698
800
  return "Deny All";
699
801
  return `Custom (${egress.allowed_hostnames?.length || 0})`;
700
802
  };
701
- const networkPolicyColumns = [
702
- createTextColumn("id", "ID", (policy) => policy.id, {
703
- width: 25,
704
- color: colors.idColor,
705
- }),
706
- createTextColumn("name", "Name", (policy) => policy.name || "", { width: 25 }),
707
- createTextColumn("egress", "Egress", (policy) => getEgressLabel(policy.egress), { width: 15 }),
708
- createTextColumn("created", "Created", (policy) => policy.create_time_ms ? formatTimeAgo(policy.create_time_ms) : "", { width: 18, color: colors.textDim }),
709
- ];
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
+ };
710
820
  return (_jsx(ResourcePicker, { config: {
711
821
  title: "Select Network Policy",
712
822
  fetchPage: async (params) => {
@@ -723,7 +833,7 @@ export const DevboxCreatePage = ({ onBack, onCreate, initialBlueprintId, initial
723
833
  },
724
834
  getItemId: (policy) => policy.id,
725
835
  getItemLabel: (policy) => policy.name || policy.id,
726
- columns: networkPolicyColumns,
836
+ columns: buildNetworkPolicyColumns,
727
837
  mode: "single",
728
838
  emptyMessage: "No network policies found",
729
839
  searchPlaceholder: "Search network policies...",
@@ -734,19 +844,57 @@ export const DevboxCreatePage = ({ onBack, onCreate, initialBlueprintId, initial
734
844
  ],
735
845
  }, onSelect: handleNetworkPolicySelect, onCancel: () => setShowNetworkPolicyPicker(false), initialSelected: formData.network_policy_id ? [formData.network_policy_id] : [] }));
736
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
+ }
737
871
  // Gateway config picker screen
738
872
  if (showGatewayPicker) {
739
- const gatewayColumns = [
740
- createTextColumn("id", "ID", (config) => config.id, {
741
- width: 25,
742
- color: colors.idColor,
743
- }),
744
- createTextColumn("name", "Name", (config) => config.name || "", { width: 25 }),
745
- createTextColumn("endpoint", "Endpoint", (config) => config.endpoint || "", { width: 30, color: colors.textDim }),
746
- createTextColumn("created", "Created", (config) => config.create_time_ms ? formatTimeAgo(config.create_time_ms) : "", { width: 18, color: colors.textDim }),
747
- ];
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
+ };
748
896
  return (_jsx(ResourcePicker, { config: {
749
- title: "Select Gateway Config",
897
+ title: "Select AI Gateway Config",
750
898
  fetchPage: async (params) => {
751
899
  const result = await listGatewayConfigs({
752
900
  limit: params.limit,
@@ -761,66 +909,111 @@ export const DevboxCreatePage = ({ onBack, onCreate, initialBlueprintId, initial
761
909
  },
762
910
  getItemId: (config) => config.id,
763
911
  getItemLabel: (config) => config.name || config.id,
764
- columns: gatewayColumns,
912
+ columns: buildGatewayColumns,
765
913
  mode: "single",
766
- emptyMessage: "No gateway configs found",
767
- searchPlaceholder: "Search gateway configs...",
914
+ emptyMessage: "No AI gateway configs found",
915
+ searchPlaceholder: "Search AI gateway configs...",
768
916
  breadcrumbItems: [
769
917
  { label: "Devboxes" },
770
918
  { label: "Create" },
771
- { label: `Gateway: ${gatewayEnvPrefix}`, active: true },
919
+ { label: "Attach AI Gateway Config" },
920
+ { label: "Select Config", active: true },
772
921
  ],
922
+ onCreateNew: () => {
923
+ setShowGatewayPicker(false);
924
+ setShowInlineGatewayConfigCreate(true);
925
+ },
926
+ createNewLabel: "Create AI gateway config",
773
927
  }, onSelect: handleGatewaySelect, onCancel: () => {
774
928
  setShowGatewayPicker(false);
775
- setGatewayEnvPrefix("");
776
- setGatewayInputMode(null);
929
+ // Return to the form at the gateway field
930
+ setGatewayFormField("gateway");
777
931
  }, initialSelected: [] }, "gateway-config-picker"));
778
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
+ }
779
950
  // Secret picker screen (for gateway)
780
951
  if (showSecretPicker) {
781
- const secretColumns = [
782
- createTextColumn("id", "ID", (secret) => secret.id, {
783
- width: 25,
784
- color: colors.idColor,
785
- }),
786
- createTextColumn("name", "Name", (secret) => secret.name || "", { width: 30 }),
787
- createTextColumn("created", "Created", (secret) => secret.create_time_ms ? formatTimeAgo(secret.create_time_ms) : "", { width: 18, color: colors.textDim }),
788
- ];
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
+ };
789
967
  return (_jsx(ResourcePicker, { config: {
790
968
  title: "Select Secret for Gateway",
791
969
  fetchPage: async (params) => {
792
970
  const client = getClient();
793
- // Secrets API doesn't support cursor pagination, just limit
971
+ // Secrets API doesn't support cursor pagination, so we fetch all
972
+ // and do client-side pagination by slicing to the requested page
794
973
  const page = await client.secrets.list({
795
- limit: params.limit,
974
+ limit: 1000,
796
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);
797
990
  return {
798
- items: (page.secrets || []).map((s) => ({
799
- id: s.id,
800
- name: s.name,
801
- create_time_ms: s.create_time_ms,
802
- })),
803
- hasMore: false, // Secrets API doesn't support pagination
804
- totalCount: page.total_count || 0,
991
+ items: sliced,
992
+ hasMore: startIdx + params.limit < allSecrets.length,
993
+ totalCount: allSecrets.length,
805
994
  };
806
995
  },
807
996
  getItemId: (secret) => secret.id,
808
997
  getItemLabel: (secret) => secret.name || secret.id,
809
- columns: secretColumns,
998
+ columns: buildSecretColumns,
810
999
  mode: "single",
811
1000
  emptyMessage: "No secrets found",
812
1001
  searchPlaceholder: "Search secrets...",
813
1002
  breadcrumbItems: [
814
1003
  { label: "Devboxes" },
815
1004
  { label: "Create" },
816
- { label: `Gateway: ${gatewayEnvPrefix}` },
1005
+ { label: "Attach AI Gateway Config" },
817
1006
  { label: "Select Secret", active: true },
818
1007
  ],
1008
+ onCreateNew: () => {
1009
+ setShowSecretPicker(false);
1010
+ setShowInlineSecretCreate(true);
1011
+ },
1012
+ createNewLabel: "Create secret",
819
1013
  }, onSelect: handleSecretSelect, onCancel: () => {
820
1014
  setShowSecretPicker(false);
821
- setPendingGateway(null);
822
- setGatewayEnvPrefix("");
823
- setGatewayInputMode(null);
1015
+ // Return to the form at the secret field
1016
+ setGatewayFormField("secret");
824
1017
  }, initialSelected: [] }, "secret-picker"));
825
1018
  }
826
1019
  // Form screen
@@ -835,7 +1028,20 @@ export const DevboxCreatePage = ({ onBack, onCreate, initialBlueprintId, initial
835
1028
  }
836
1029
  if (field.type === "select") {
837
1030
  const value = fieldData;
838
- 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));
839
1045
  }
840
1046
  if (field.type === "source") {
841
1047
  // Check if either blueprint or snapshot is selected
@@ -902,34 +1108,59 @@ export const DevboxCreatePage = ({ onBack, onCreate, initialBlueprintId, initial
902
1108
  if (field.type === "gateways") {
903
1109
  if (!inGatewaySection) {
904
1110
  // Collapsed view
905
- 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, " gateway(s)"] }), 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, " ", gw.envPrefix, ": ", gw.gatewayName, " \u2192", " ", gw.secretName] }, idx))) }))] }, field.key));
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));
906
1112
  }
907
1113
  // Expanded gateway section view
908
1114
  const gatewayCount = formData.gateways.length;
909
1115
  const maxGatewayIndex = gatewayCount + 1;
910
- 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, " Manage Gateway Configurations"] }), gatewayInputMode === "envPrefix" && (_jsxs(Box, { flexDirection: "column", marginTop: 1, borderStyle: "single", borderColor: colors.success, paddingX: 1, children: [_jsx(Text, { color: colors.success, bold: true, children: "Adding New Gateway" }), _jsxs(Box, { children: [_jsxs(Text, { color: colors.primary, children: ["Env Prefix (e.g., GWS_ANTHROPIC):", " "] }), _jsx(TextInput, { value: gatewayEnvPrefix || "", onChange: setGatewayEnvPrefix, placeholder: "GWS_ANTHROPIC" })] }), _jsx(Text, { color: colors.textDim, dimColor: true, children: "Press Enter to select gateway config" })] })), !gatewayInputMode && (_jsxs(_Fragment, { children: [_jsxs(Box, { marginTop: 1, children: [_jsxs(Text, { color: selectedGatewayIndex === 0
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
911
1142
  ? colors.primary
912
1143
  : colors.textDim, children: [selectedGatewayIndex === 0
913
1144
  ? figures.pointer
914
1145
  : " ", " "] }), _jsx(Text, { color: selectedGatewayIndex === 0
915
1146
  ? colors.success
916
- : colors.textDim, bold: selectedGatewayIndex === 0, children: "+ Add new gateway" })] }), gatewayCount > 0 && (_jsx(Box, { flexDirection: "column", marginTop: 1, children: formData.gateways.map((gw, index) => {
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) => {
917
1148
  const itemIndex = index + 1;
918
1149
  const isGatewaySelected = selectedGatewayIndex === itemIndex;
919
- return (_jsxs(Box, { children: [_jsxs(Text, { color: isGatewaySelected
920
- ? colors.primary
921
- : colors.textDim, children: [isGatewaySelected ? figures.pointer : " ", " "] }), _jsxs(Text, { color: isGatewaySelected
922
- ? colors.primary
923
- : colors.textDim, bold: isGatewaySelected, children: [gw.envPrefix, ": ", gw.gatewayName, " \u2192", " ", gw.secretName] })] }, gw.envPrefix));
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));
924
1157
  }) })), _jsxs(Box, { marginTop: 1, children: [_jsxs(Text, { color: selectedGatewayIndex === maxGatewayIndex
925
1158
  ? colors.primary
926
1159
  : colors.textDim, children: [selectedGatewayIndex === maxGatewayIndex
927
1160
  ? figures.pointer
928
1161
  : " ", " "] }), _jsxs(Text, { color: selectedGatewayIndex === maxGatewayIndex
929
1162
  ? colors.success
930
- : 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: gatewayInputMode
931
- ? `[Enter] Select gateway • [esc] Cancel`
932
- : `${figures.arrowUp}${figures.arrowDown} Navigate • [Enter] ${selectedGatewayIndex === 0 ? "Add" : selectedGatewayIndex === maxGatewayIndex ? "Done" : "Select"} • [d] Delete • [esc] Back` }) })] }, field.key));
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));
933
1164
  }
934
1165
  return null;
935
1166
  }) }), formData.resource_size === "CUSTOM_SIZE" &&