@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.
@@ -1,4 +1,4 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  /**
3
3
  * DevboxDetailPage - Detail page for devboxes
4
4
  * Uses the generic ResourceDetailPage component with devbox-specific customizations
@@ -85,7 +85,9 @@ export const DevboxDetailPage = ({ devbox: initialDevbox, onBack, }) => {
85
85
  ];
86
86
  // Filter operations based on devbox status
87
87
  const getFilteredOperations = (devbox) => {
88
- return allOperations.filter((op) => {
88
+ const hasTunnel = !!(devbox.tunnel && devbox.tunnel.tunnel_key);
89
+ return allOperations
90
+ .filter((op) => {
89
91
  const status = devbox.status;
90
92
  // When suspended: logs and resume
91
93
  if (status === "suspended") {
@@ -103,6 +105,20 @@ export const DevboxDetailPage = ({ devbox: initialDevbox, onBack, }) => {
103
105
  }
104
106
  // Default for transitional states (provisioning, initializing)
105
107
  return op.key === "logs" || op.key === "delete";
108
+ })
109
+ .map((op) => {
110
+ // Dynamic tunnel label based on whether tunnel is active
111
+ if (op.key === "tunnel") {
112
+ return hasTunnel
113
+ ? {
114
+ ...op,
115
+ label: "Tunnel (Active)",
116
+ color: colors.success,
117
+ icon: figures.tick,
118
+ }
119
+ : op;
120
+ }
121
+ return op;
106
122
  });
107
123
  };
108
124
  // Build detail sections for the devbox
@@ -228,6 +244,22 @@ export const DevboxDetailPage = ({ devbox: initialDevbox, onBack, }) => {
228
244
  },
229
245
  });
230
246
  }
247
+ // Tunnel status - always show when running
248
+ if (devbox.tunnel && devbox.tunnel.tunnel_key) {
249
+ const tunnelKey = devbox.tunnel.tunnel_key;
250
+ const authMode = devbox.tunnel.auth_mode;
251
+ const tunnelUrl = `https://{port}-${tunnelKey}.tunnel.runloop.ai`;
252
+ detailFields.push({
253
+ label: "Tunnel",
254
+ value: (_jsxs(_Fragment, { children: [_jsxs(Text, { color: colors.success, bold: true, children: [figures.tick, " Active"] }), _jsx(Text, { color: colors.textDim, children: " \u2022 " }), _jsx(Text, { color: colors.success, children: tunnelUrl }), authMode === "authenticated" && (_jsx(Text, { color: colors.warning, children: " (authenticated)" }))] })),
255
+ });
256
+ }
257
+ else if (devbox.status === "running") {
258
+ detailFields.push({
259
+ label: "Tunnel",
260
+ value: (_jsxs(Text, { color: colors.textDim, dimColor: true, children: [figures.cross, " Off"] })),
261
+ });
262
+ }
231
263
  // Initiator
232
264
  if (devbox.initiator_id) {
233
265
  detailFields.push({
@@ -355,6 +387,18 @@ export const DevboxDetailPage = ({ devbox: initialDevbox, onBack, }) => {
355
387
  }
356
388
  lines.push(_jsx(Text, { children: " " }, "launch-space"));
357
389
  }
390
+ // Tunnel Information
391
+ if (devbox.tunnel && devbox.tunnel.tunnel_key) {
392
+ lines.push(_jsx(Text, { color: colors.warning, bold: true, children: "Tunnel" }, "tunnel-title"));
393
+ lines.push(_jsxs(Text, { dimColor: true, children: [" ", "Tunnel Key: ", devbox.tunnel.tunnel_key] }, "tunnel-key"));
394
+ lines.push(_jsxs(Text, { dimColor: true, children: [" ", "Auth Mode: ", devbox.tunnel.auth_mode] }, "tunnel-auth"));
395
+ const tunnelUrl = `https://{port}-${devbox.tunnel.tunnel_key}.tunnel.runloop.ai`;
396
+ lines.push(_jsxs(Text, { color: colors.success, children: [" ", "Tunnel URL: ", tunnelUrl] }, "tunnel-url"));
397
+ if (devbox.tunnel.auth_token) {
398
+ lines.push(_jsxs(Text, { color: colors.warning, children: [" ", "Auth Token: ", devbox.tunnel.auth_token] }, "tunnel-token"));
399
+ }
400
+ lines.push(_jsx(Text, { children: " " }, "tunnel-space"));
401
+ }
358
402
  // Source
359
403
  if (devbox.blueprint_id || devbox.snapshot_id) {
360
404
  lines.push(_jsx(Text, { color: colors.warning, bold: true, children: "Source" }, "source-title"));
@@ -11,6 +11,7 @@ import { NavigationTips } from "./NavigationTips.js";
11
11
  import { FormTextInput, FormSelect, FormActionButton, useFormSelectNavigation, } from "./form/index.js";
12
12
  import { colors } from "../utils/theme.js";
13
13
  import { useExitOnCtrlC } from "../hooks/useExitOnCtrlC.js";
14
+ import { validateGatewayConfig } from "../utils/gatewayConfigValidation.js";
14
15
  const authTypes = ["bearer", "header"];
15
16
  export const GatewayConfigCreatePage = ({ onBack, onCreate, initialConfig, }) => {
16
17
  const isEditing = !!initialConfig?.id;
@@ -36,13 +37,20 @@ export const GatewayConfigCreatePage = ({ onBack, onCreate, initialConfig, }) =>
36
37
  const fields = [
37
38
  {
38
39
  key: "create",
39
- label: isEditing ? "Update Gateway Config" : "Create Gateway Config",
40
+ label: isEditing
41
+ ? "Update AI Gateway Config"
42
+ : "Create AI Gateway Config",
40
43
  type: "action",
41
44
  },
42
- { key: "name", label: "Name", type: "text", placeholder: "my-gateway" },
45
+ {
46
+ key: "name",
47
+ label: "Name (required)",
48
+ type: "text",
49
+ placeholder: "my-gateway",
50
+ },
43
51
  {
44
52
  key: "endpoint",
45
- label: "Endpoint URL",
53
+ label: "Endpoint URL (required)",
46
54
  type: "text",
47
55
  placeholder: "https://api.example.com",
48
56
  },
@@ -156,35 +164,34 @@ export const GatewayConfigCreatePage = ({ onBack, onCreate, initialConfig, }) =>
156
164
  }
157
165
  }, { isActive: true });
158
166
  const handleCreate = async () => {
159
- // Validate required fields
160
- if (!formData.name.trim()) {
161
- setError(new Error("Name is required"));
162
- return;
163
- }
164
- if (!formData.endpoint.trim()) {
165
- setError(new Error("Endpoint URL is required"));
166
- return;
167
- }
168
- if (formData.auth_type === "header" && !formData.auth_key.trim()) {
169
- setError(new Error("Auth header key is required for header auth type"));
167
+ // Validate using shared validation
168
+ const validation = validateGatewayConfig({
169
+ name: formData.name,
170
+ endpoint: formData.endpoint,
171
+ authType: formData.auth_type,
172
+ authKey: formData.auth_key,
173
+ }, { requireName: true, requireEndpoint: true });
174
+ if (!validation.valid) {
175
+ setError(new Error(validation.errors.join("\n")));
170
176
  return;
171
177
  }
178
+ const { sanitized } = validation;
172
179
  setCreating(true);
173
180
  setError(null);
174
181
  try {
175
182
  const client = getClient();
176
183
  const authMechanism = {
177
- type: formData.auth_type,
184
+ type: sanitized.authType,
178
185
  };
179
- if (formData.auth_type === "header" && formData.auth_key.trim()) {
180
- authMechanism.key = formData.auth_key.trim();
186
+ if (sanitized.authType === "header" && sanitized.authKey) {
187
+ authMechanism.key = sanitized.authKey;
181
188
  }
182
189
  let config;
183
190
  if (isEditing && initialConfig?.id) {
184
191
  // Update existing config
185
192
  config = await client.gatewayConfigs.update(initialConfig.id, {
186
- name: formData.name.trim(),
187
- endpoint: formData.endpoint.trim(),
193
+ name: sanitized.name,
194
+ endpoint: sanitized.endpoint,
188
195
  auth_mechanism: authMechanism,
189
196
  description: formData.description.trim() || undefined,
190
197
  });
@@ -192,8 +199,8 @@ export const GatewayConfigCreatePage = ({ onBack, onCreate, initialConfig, }) =>
192
199
  else {
193
200
  // Create new config
194
201
  config = await client.gatewayConfigs.create({
195
- name: formData.name.trim(),
196
- endpoint: formData.endpoint.trim(),
202
+ name: sanitized.name,
203
+ endpoint: sanitized.endpoint,
197
204
  auth_mechanism: authMechanism,
198
205
  description: formData.description.trim() || undefined,
199
206
  });
@@ -210,16 +217,16 @@ export const GatewayConfigCreatePage = ({ onBack, onCreate, initialConfig, }) =>
210
217
  // Result screen
211
218
  if (result) {
212
219
  return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [
213
- { label: "Gateway Configs" },
220
+ { label: "AI Gateway Configs" },
214
221
  { label: isEditing ? "Update" : "Create", active: true },
215
- ] }), _jsx(SuccessMessage, { message: `Gateway config ${isEditing ? "updated" : "created"} successfully!` }), _jsxs(Box, { marginLeft: 2, flexDirection: "column", marginTop: 1, children: [_jsxs(Box, { children: [_jsxs(Text, { color: colors.textDim, dimColor: true, children: ["ID:", " "] }), _jsx(Text, { color: colors.idColor, children: result.id })] }), _jsx(Box, { children: _jsxs(Text, { color: colors.textDim, dimColor: true, children: ["Name: ", result.name || "(none)"] }) }), _jsx(Box, { children: _jsxs(Text, { color: colors.textDim, dimColor: true, children: ["Endpoint: ", result.endpoint] }) })] }), _jsx(NavigationTips, { tips: [{ key: "Enter/q/esc", label: "Return to list" }] })] }));
222
+ ] }), _jsx(SuccessMessage, { message: `AI gateway config ${isEditing ? "updated" : "created"} successfully!` }), _jsxs(Box, { marginLeft: 2, flexDirection: "column", marginTop: 1, children: [_jsxs(Box, { children: [_jsxs(Text, { color: colors.textDim, dimColor: true, children: ["ID:", " "] }), _jsx(Text, { color: colors.idColor, children: result.id })] }), _jsx(Box, { children: _jsxs(Text, { color: colors.textDim, dimColor: true, children: ["Name: ", result.name || "(none)"] }) }), _jsx(Box, { children: _jsxs(Text, { color: colors.textDim, dimColor: true, children: ["Endpoint: ", result.endpoint] }) })] }), _jsx(NavigationTips, { tips: [{ key: "Enter/q/esc", label: "Return to list" }] })] }));
216
223
  }
217
224
  // Error screen
218
225
  if (error) {
219
226
  return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [
220
- { label: "Gateway Configs" },
227
+ { label: "AI Gateway Configs" },
221
228
  { label: isEditing ? "Update" : "Create", active: true },
222
- ] }), _jsx(ErrorMessage, { message: `Failed to ${isEditing ? "update" : "create"} gateway config`, error: error }), _jsx(NavigationTips, { tips: [
229
+ ] }), _jsx(ErrorMessage, { message: `Failed to ${isEditing ? "update" : "create"} AI gateway config`, error: error }), _jsx(NavigationTips, { tips: [
223
230
  { key: "Enter/r", label: "Retry" },
224
231
  { key: "q/esc", label: "Cancel" },
225
232
  ] })] }));
@@ -227,13 +234,13 @@ export const GatewayConfigCreatePage = ({ onBack, onCreate, initialConfig, }) =>
227
234
  // Creating screen
228
235
  if (creating) {
229
236
  return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [
230
- { label: "Gateway Configs" },
237
+ { label: "AI Gateway Configs" },
231
238
  { label: isEditing ? "Update" : "Create", active: true },
232
- ] }), _jsx(SpinnerComponent, { message: `${isEditing ? "Updating" : "Creating"} gateway config...` })] }));
239
+ ] }), _jsx(SpinnerComponent, { message: `${isEditing ? "Updating" : "Creating"} AI gateway config...` })] }));
233
240
  }
234
241
  // Form screen
235
242
  return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [
236
- { label: "Gateway Configs" },
243
+ { label: "AI Gateway Configs" },
237
244
  { label: isEditing ? "Update" : "Create", active: true },
238
245
  ] }), _jsx(Box, { flexDirection: "column", marginBottom: 1, children: fields.map((field) => {
239
246
  const isActive = currentField === field.key;
@@ -31,14 +31,22 @@ export function ResourcePicker({ config, onSelect, onCancel, initialSelected = [
31
31
  onSearchClear: () => setSelectedIndex(0),
32
32
  });
33
33
  // Calculate overhead for viewport height
34
- const overhead = 15 + search.getSearchOverhead();
34
+ // Matches list pages: breadcrumb(4) + table chrome(4) + stats(2) + nav tips(2) + buffer(1) = 13
35
+ const overhead = 13 + search.getSearchOverhead();
35
36
  const { viewportHeight, terminalWidth } = useViewportHeight({
36
37
  overhead,
37
38
  minHeight: 5,
38
39
  });
39
40
  const PAGE_SIZE = viewportHeight;
40
- // terminalWidth available from viewport hook for column calculations
41
- void terminalWidth;
41
+ // Resolve columns - support both static array and function that receives terminalWidth
42
+ const resolvedColumns = React.useMemo(() => {
43
+ if (!config.columns)
44
+ return undefined;
45
+ if (typeof config.columns === "function") {
46
+ return config.columns(terminalWidth);
47
+ }
48
+ return config.columns;
49
+ }, [config.columns, terminalWidth]);
42
50
  // Store fetchPage in a ref to avoid dependency issues
43
51
  const fetchPageRef = React.useRef(config.fetchPage);
44
52
  React.useEffect(() => {
@@ -154,6 +162,9 @@ export function ResourcePicker({ config, onSelect, onCancel, initialSelected = [
154
162
  handleConfirm();
155
163
  }
156
164
  }
165
+ else if (input === "c" && config.onCreateNew) {
166
+ config.onCreateNew();
167
+ }
157
168
  else if (input === "/") {
158
169
  search.enterSearchMode();
159
170
  }
@@ -178,7 +189,7 @@ export function ResourcePicker({ config, onSelect, onCancel, initialSelected = [
178
189
  // Calculate pagination info for display
179
190
  const startIndex = currentPage * PAGE_SIZE;
180
191
  const endIndex = Math.min(startIndex + PAGE_SIZE, totalCount);
181
- return (_jsxs(_Fragment, { children: [config.breadcrumbItems && _jsx(Breadcrumb, { items: config.breadcrumbItems }), _jsx(SearchBar, { searchMode: search.searchMode, searchQuery: search.searchQuery, submittedSearchQuery: search.submittedSearchQuery, resultCount: totalCount, onSearchChange: search.setSearchQuery, onSearchSubmit: search.submitSearch, placeholder: config.searchPlaceholder || "Search..." }), config.columns ? (_jsx(Table, { data: items, keyExtractor: config.getItemId, selectedIndex: selectedIndex, title: `${config.title.toLowerCase()}[${totalCount}]${config.mode === "multi" ? ` (${selectedIds.size} selected)` : ""}`, columns: config.mode === "multi"
192
+ return (_jsxs(_Fragment, { children: [config.breadcrumbItems && _jsx(Breadcrumb, { items: config.breadcrumbItems }), _jsx(SearchBar, { searchMode: search.searchMode, searchQuery: search.searchQuery, submittedSearchQuery: search.submittedSearchQuery, resultCount: totalCount, onSearchChange: search.setSearchQuery, onSearchSubmit: search.submitSearch, placeholder: config.searchPlaceholder || "Search..." }), resolvedColumns ? (_jsx(Table, { data: items, keyExtractor: config.getItemId, selectedIndex: selectedIndex, title: `${config.title.toLowerCase()}[${totalCount}]${config.mode === "multi" ? ` (${selectedIds.size} selected)` : ""}`, columns: config.mode === "multi"
182
193
  ? [
183
194
  // Prepend checkbox column for multi-select mode
184
195
  createComponentColumn("_selection", "", (row) => {
@@ -187,11 +198,11 @@ export function ResourcePicker({ config, onSelect, onCancel, initialSelected = [
187
198
  ? figures.checkboxOn
188
199
  : figures.checkboxOff, " "] }));
189
200
  }, { width: 3 }),
190
- ...config.columns,
201
+ ...resolvedColumns,
191
202
  ]
192
- : config.columns, emptyState: _jsxs(Text, { color: colors.textDim, children: [figures.info, " ", config.emptyMessage || "No items found"] }) })) : (
203
+ : resolvedColumns, emptyState: _jsxs(Text, { color: colors.textDim, children: [figures.info, " ", config.emptyMessage || "No items found", config.onCreateNew ? " Press [c] to create one." : ""] }) })) : (
193
204
  // Fallback simple list view if no columns provided
194
- _jsx(Box, { flexDirection: "column", borderStyle: "round", borderColor: colors.border, paddingX: 1, paddingY: 0, children: items.length === 0 ? (_jsx(Box, { paddingY: 1, children: _jsxs(Text, { color: colors.textDim, children: [figures.info, " ", config.emptyMessage || "No items found"] }) })) : (items.map((item, index) => {
205
+ _jsx(Box, { flexDirection: "column", borderStyle: "round", borderColor: colors.border, paddingX: 1, paddingY: 0, children: items.length === 0 ? (_jsx(Box, { paddingY: 1, children: _jsxs(Text, { color: colors.textDim, children: [figures.info, " ", config.emptyMessage || "No items found", config.onCreateNew ? " Press [c] to create one." : ""] }) })) : (items.map((item, index) => {
195
206
  const isHighlighted = index === selectedIndex;
196
207
  const id = config.getItemId(item);
197
208
  const label = config.getItemLabel(item);
@@ -214,6 +225,9 @@ export function ResourcePicker({ config, onSelect, onCancel, initialSelected = [
214
225
  label: config.mode === "single" ? "Select" : "Confirm",
215
226
  condition: canConfirm,
216
227
  },
228
+ ...(config.onCreateNew
229
+ ? [{ key: "c", label: config.createNewLabel || "Create new" }]
230
+ : []),
217
231
  { key: "/", label: "Search" },
218
232
  { key: "Esc", label: "Cancel" },
219
233
  ] })] }));
@@ -14,10 +14,11 @@ import { NavigationTips } from "./NavigationTips.js";
14
14
  import { FormTextInput, FormActionButton } from "./form/index.js";
15
15
  import { colors } from "../utils/theme.js";
16
16
  import { useExitOnCtrlC } from "../hooks/useExitOnCtrlC.js";
17
- export const SecretCreatePage = ({ onBack, onCreate, }) => {
17
+ export const SecretCreatePage = ({ onBack, onCreate, initialSecret, }) => {
18
+ const isUpdating = !!initialSecret;
18
19
  const [currentField, setCurrentField] = React.useState("submit");
19
20
  const [formData, setFormData] = React.useState({
20
- name: "",
21
+ name: initialSecret?.name || "",
21
22
  value: "",
22
23
  });
23
24
  const [submitting, setSubmitting] = React.useState(false);
@@ -25,9 +26,17 @@ export const SecretCreatePage = ({ onBack, onCreate, }) => {
25
26
  const [error, setError] = React.useState(null);
26
27
  const [validationError, setValidationError] = React.useState(null);
27
28
  const fields = [
28
- { key: "submit", label: "Create Secret", type: "action" },
29
+ {
30
+ key: "submit",
31
+ label: isUpdating ? "Update Secret" : "Create Secret",
32
+ type: "action",
33
+ },
29
34
  { key: "name", label: "Name (required)", type: "text" },
30
- { key: "value", label: "Value (required)", type: "password" },
35
+ {
36
+ key: "value",
37
+ label: isUpdating ? "New Value (required)" : "Value (required)",
38
+ type: "password",
39
+ },
31
40
  ];
32
41
  const currentFieldIndex = fields.findIndex((f) => f.key === currentField);
33
42
  // Handle Ctrl+C to exit
@@ -78,13 +87,30 @@ export const SecretCreatePage = ({ onBack, onCreate, }) => {
78
87
  return;
79
88
  }
80
89
  // Navigation between fields (up/down arrows and tab/shift+tab)
81
- if ((key.upArrow || (key.tab && key.shift)) && currentFieldIndex > 0) {
82
- setCurrentField(fields[currentFieldIndex - 1].key);
90
+ // In update mode, skip the name field (it's read-only)
91
+ const getNextFieldIndex = (direction) => {
92
+ let nextIdx = direction === "up" ? currentFieldIndex - 1 : currentFieldIndex + 1;
93
+ while (nextIdx >= 0 && nextIdx < fields.length) {
94
+ if (isUpdating && fields[nextIdx].key === "name") {
95
+ nextIdx = direction === "up" ? nextIdx - 1 : nextIdx + 1;
96
+ continue;
97
+ }
98
+ return nextIdx;
99
+ }
100
+ return null;
101
+ };
102
+ if (key.upArrow || (key.tab && key.shift)) {
103
+ const nextIdx = getNextFieldIndex("up");
104
+ if (nextIdx !== null) {
105
+ setCurrentField(fields[nextIdx].key);
106
+ }
83
107
  return;
84
108
  }
85
- if ((key.downArrow || (key.tab && !key.shift)) &&
86
- currentFieldIndex < fields.length - 1) {
87
- setCurrentField(fields[currentFieldIndex + 1].key);
109
+ if (key.downArrow || (key.tab && !key.shift)) {
110
+ const nextIdx = getNextFieldIndex("down");
111
+ if (nextIdx !== null) {
112
+ setCurrentField(fields[nextIdx].key);
113
+ }
88
114
  return;
89
115
  }
90
116
  });
@@ -105,10 +131,19 @@ export const SecretCreatePage = ({ onBack, onCreate, }) => {
105
131
  setValidationError(null);
106
132
  try {
107
133
  const client = getClient();
108
- const secret = await client.secrets.create({
109
- name: formData.name.trim(),
110
- value: formData.value,
111
- });
134
+ let secret;
135
+ if (isUpdating) {
136
+ // Update existing secret by name
137
+ secret = (await client.secrets.update(formData.name.trim(), {
138
+ value: formData.value,
139
+ }));
140
+ }
141
+ else {
142
+ secret = (await client.secrets.create({
143
+ name: formData.name.trim(),
144
+ value: formData.value,
145
+ }));
146
+ }
112
147
  setResult(secret);
113
148
  }
114
149
  catch (err) {
@@ -123,16 +158,21 @@ export const SecretCreatePage = ({ onBack, onCreate, }) => {
123
158
  return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [
124
159
  { label: "Settings" },
125
160
  { label: "Secrets" },
126
- { label: "Create", active: true },
127
- ] }), _jsx(SuccessMessage, { message: "Secret created successfully!" }), _jsxs(Box, { marginLeft: 2, flexDirection: "column", marginTop: 1, children: [_jsxs(Box, { children: [_jsxs(Text, { color: colors.textDim, dimColor: true, children: ["ID:", " "] }), _jsx(Text, { color: colors.idColor, children: result.id })] }), _jsx(Box, { children: _jsxs(Text, { color: colors.textDim, dimColor: true, children: ["Name: ", result.name] }) })] }), _jsx(Box, { marginTop: 1, marginLeft: 2, children: _jsxs(Text, { color: colors.info, children: [figures.info, " The secret value has been securely stored and cannot be retrieved."] }) }), _jsx(NavigationTips, { tips: [{ key: "Enter/q/esc", label: "View secret details" }] })] }));
161
+ { label: isUpdating ? "Update" : "Create", active: true },
162
+ ] }), _jsx(SuccessMessage, { message: `Secret ${isUpdating ? "updated" : "created"} successfully!` }), _jsxs(Box, { marginLeft: 2, flexDirection: "column", marginTop: 1, children: [_jsxs(Box, { children: [_jsxs(Text, { color: colors.textDim, dimColor: true, children: ["ID:", " "] }), _jsx(Text, { color: colors.idColor, children: result.id })] }), _jsx(Box, { children: _jsxs(Text, { color: colors.textDim, dimColor: true, children: ["Name: ", result.name] }) })] }), _jsx(Box, { marginTop: 1, marginLeft: 2, children: _jsxs(Text, { color: colors.info, children: [figures.info, " The secret value has been securely stored and cannot be retrieved."] }) }), _jsx(NavigationTips, { tips: [
163
+ {
164
+ key: "Enter/q/esc",
165
+ label: isUpdating ? "Return to details" : "View secret details",
166
+ },
167
+ ] })] }));
128
168
  }
129
169
  // Error screen
130
170
  if (error) {
131
171
  return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [
132
172
  { label: "Settings" },
133
173
  { label: "Secrets" },
134
- { label: "Create", active: true },
135
- ] }), _jsx(ErrorMessage, { message: "Failed to create secret", error: error }), _jsx(NavigationTips, { tips: [
174
+ { label: isUpdating ? "Update" : "Create", active: true },
175
+ ] }), _jsx(ErrorMessage, { message: `Failed to ${isUpdating ? "update" : "create"} secret`, error: error }), _jsx(NavigationTips, { tips: [
136
176
  { key: "Enter/r", label: "Retry" },
137
177
  { key: "q/esc", label: "Cancel" },
138
178
  ] })] }));
@@ -142,20 +182,26 @@ export const SecretCreatePage = ({ onBack, onCreate, }) => {
142
182
  return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [
143
183
  { label: "Settings" },
144
184
  { label: "Secrets" },
145
- { label: "Create", active: true },
146
- ] }), _jsx(SpinnerComponent, { message: "Creating secret..." })] }));
185
+ { label: isUpdating ? "Update" : "Create", active: true },
186
+ ] }), _jsx(SpinnerComponent, { message: `${isUpdating ? "Updating" : "Creating"} secret...` })] }));
147
187
  }
148
188
  // Form screen
149
189
  return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [
150
190
  { label: "Settings" },
151
191
  { label: "Secrets" },
152
- { label: "Create", active: true },
153
- ] }), _jsx(Box, { borderStyle: "round", borderColor: colors.info, paddingX: 1, paddingY: 0, marginBottom: 1, children: _jsxs(Text, { color: colors.info, children: [figures.info, " ", _jsx(Text, { bold: true, children: "Note:" }), " Secret values are", " ", _jsx(Text, { bold: true, children: "write-only" }), ". Once created, the value cannot be retrieved or viewed. To change a secret, delete it and create a new one."] }) }), _jsx(Box, { flexDirection: "column", marginBottom: 1, children: fields.map((field) => {
192
+ { label: isUpdating ? "Update" : "Create", active: true },
193
+ ] }), _jsx(Box, { borderStyle: "round", borderColor: colors.info, paddingX: 1, paddingY: 0, marginBottom: 1, children: _jsxs(Text, { color: colors.info, children: [figures.info, " ", _jsx(Text, { bold: true, children: "Note:" }), " Secret values are", " ", _jsx(Text, { bold: true, children: "write-only" }), ". Once stored, the value cannot be retrieved or viewed.", " ", isUpdating
194
+ ? "Enter a new value to replace the current one."
195
+ : "You can update a secret's value later."] }) }), _jsx(Box, { flexDirection: "column", marginBottom: 1, children: fields.map((field) => {
154
196
  const isActive = currentField === field.key;
155
197
  if (field.type === "action") {
156
- return (_jsx(FormActionButton, { label: field.label, isActive: isActive, hint: "[Enter to create]" }, field.key));
198
+ return (_jsx(FormActionButton, { label: field.label, isActive: isActive, hint: `[Enter to ${isUpdating ? "update" : "create"}]` }, field.key));
157
199
  }
158
200
  if (field.type === "text") {
201
+ // In update mode, name is read-only
202
+ if (isUpdating && field.key === "name") {
203
+ return (_jsxs(Box, { marginBottom: 0, children: [_jsxs(Text, { color: colors.textDim, children: [" ", field.label, ":", " "] }), _jsx(Text, { color: colors.text, bold: true, children: formData.name })] }, field.key));
204
+ }
159
205
  const value = formData[field.key];
160
206
  const hasError = field.key === "name" && validationError === "Name is required";
161
207
  return (_jsx(FormTextInput, { label: field.label, value: value, onChange: (newValue) => {
@@ -177,7 +223,7 @@ export const SecretCreatePage = ({ onBack, onCreate, }) => {
177
223
  }
178
224
  return null;
179
225
  }) }), _jsx(NavigationTips, { showArrows: true, tips: [
180
- { key: "Enter", label: "Create" },
226
+ { key: "Enter", label: isUpdating ? "Update" : "Create" },
181
227
  { key: "q", label: "Cancel" },
182
228
  ] })] }));
183
229
  };
@@ -16,7 +16,7 @@ const settingsMenuItems = [
16
16
  },
17
17
  {
18
18
  key: "gateway-configs",
19
- label: "Gateway Configs",
19
+ label: "AI Gateway Configs",
20
20
  description: "Configure API credential proxying",
21
21
  icon: "⬡",
22
22
  color: colors.success,
@@ -61,23 +61,23 @@ export function GatewayConfigDetailScreen({ gatewayConfigId, }) {
61
61
  // Show loading state while fetching or before fetch starts
62
62
  if (!config && gatewayConfigId && !error) {
63
63
  return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [
64
- { label: "Gateway Configs" },
64
+ { label: "AI Gateway Configs" },
65
65
  { label: "Loading...", active: true },
66
- ] }), _jsx(SpinnerComponent, { message: "Loading gateway config details..." })] }));
66
+ ] }), _jsx(SpinnerComponent, { message: "Loading AI gateway config details..." })] }));
67
67
  }
68
68
  // Show error state if fetch failed
69
69
  if (error && !config) {
70
70
  return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [
71
- { label: "Gateway Configs" },
71
+ { label: "AI Gateway Configs" },
72
72
  { label: "Error", active: true },
73
- ] }), _jsx(ErrorMessage, { message: "Failed to load gateway config details", error: error })] }));
73
+ ] }), _jsx(ErrorMessage, { message: "Failed to load AI gateway config details", error: error })] }));
74
74
  }
75
75
  // Show error if no config found
76
76
  if (!config) {
77
77
  return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [
78
- { label: "Gateway Configs" },
78
+ { label: "AI Gateway Configs" },
79
79
  { label: "Not Found", active: true },
80
- ] }), _jsx(ErrorMessage, { message: `Gateway config ${gatewayConfigId || "unknown"} not found`, error: new Error("Gateway config not found") })] }));
80
+ ] }), _jsx(ErrorMessage, { message: `AI gateway config ${gatewayConfigId || "unknown"} not found`, error: new Error("AI gateway config not found") })] }));
81
81
  }
82
82
  // Build detail sections
83
83
  const detailSections = [];
@@ -135,14 +135,14 @@ export function GatewayConfigDetailScreen({ gatewayConfigId, }) {
135
135
  const operations = [
136
136
  {
137
137
  key: "edit",
138
- label: "Edit Gateway Config",
138
+ label: "Edit AI Gateway Config",
139
139
  color: colors.warning,
140
140
  icon: figures.pointer,
141
141
  shortcut: "e",
142
142
  },
143
143
  {
144
144
  key: "delete",
145
- label: "Delete Gateway Config",
145
+ label: "Delete AI Gateway Config",
146
146
  color: colors.error,
147
147
  icon: figures.cross,
148
148
  shortcut: "d",
@@ -179,7 +179,7 @@ export function GatewayConfigDetailScreen({ gatewayConfigId, }) {
179
179
  const buildDetailLines = (gc) => {
180
180
  const lines = [];
181
181
  // Core Information
182
- lines.push(_jsx(Text, { color: colors.warning, bold: true, children: "Gateway Config Details" }, "core-title"));
182
+ lines.push(_jsx(Text, { color: colors.warning, bold: true, children: "AI Gateway Config Details" }, "core-title"));
183
183
  lines.push(_jsxs(Text, { color: colors.idColor, children: [" ", "ID: ", gc.id] }, "core-id"));
184
184
  lines.push(_jsxs(Text, { dimColor: true, children: [" ", "Name: ", gc.name] }, "core-name"));
185
185
  if (gc.description) {
@@ -218,8 +218,8 @@ export function GatewayConfigDetailScreen({ gatewayConfigId, }) {
218
218
  }
219
219
  // Show delete confirmation
220
220
  if (showDeleteConfirm && config) {
221
- return (_jsx(ConfirmationPrompt, { title: "Delete Gateway Config", message: `Are you sure you want to delete "${config.name || config.id}"?`, details: "This action cannot be undone. Any devboxes using this gateway config will no longer have access to it.", breadcrumbItems: [
222
- { label: "Gateway Configs" },
221
+ return (_jsx(ConfirmationPrompt, { title: "Delete AI Gateway Config", message: `Are you sure you want to delete "${config.name || config.id}"?`, details: "This action cannot be undone. Any devboxes using this AI gateway config will no longer have access to it.", breadcrumbItems: [
222
+ { label: "AI Gateway Configs" },
223
223
  { label: config.name || config.id },
224
224
  { label: "Delete", active: true },
225
225
  ], onConfirm: executeDelete, onCancel: () => setShowDeleteConfirm(false) }));
@@ -227,10 +227,10 @@ export function GatewayConfigDetailScreen({ gatewayConfigId, }) {
227
227
  // Show deleting state
228
228
  if (deleting) {
229
229
  return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [
230
- { label: "Gateway Configs" },
230
+ { label: "AI Gateway Configs" },
231
231
  { label: config.name || config.id },
232
232
  { label: "Deleting...", active: true },
233
- ] }), _jsx(SpinnerComponent, { message: "Deleting gateway config..." })] }));
233
+ ] }), _jsx(SpinnerComponent, { message: "Deleting AI gateway config..." })] }));
234
234
  }
235
- return (_jsx(ResourceDetailPage, { resource: config, resourceType: "Gateway Configs", getDisplayName: (gc) => gc.name || gc.id, getId: (gc) => gc.id, getStatus: () => "active", detailSections: detailSections, operations: operations, onOperation: handleOperation, onBack: goBack, buildDetailLines: buildDetailLines }));
235
+ return (_jsx(ResourceDetailPage, { resource: config, resourceType: "AI Gateway Configs", getDisplayName: (gc) => gc.name || gc.id, getId: (gc) => gc.id, getStatus: () => "active", detailSections: detailSections, operations: operations, onOperation: handleOperation, onBack: goBack, buildDetailLines: buildDetailLines }));
236
236
  }
@@ -13,6 +13,7 @@ import { SpinnerComponent } from "../components/Spinner.js";
13
13
  import { ErrorMessage } from "../components/ErrorMessage.js";
14
14
  import { Breadcrumb } from "../components/Breadcrumb.js";
15
15
  import { ConfirmationPrompt } from "../components/ConfirmationPrompt.js";
16
+ import { SecretCreatePage } from "../components/SecretCreatePage.js";
16
17
  import { colors } from "../utils/theme.js";
17
18
  export function SecretDetailScreen({ secretId }) {
18
19
  const { goBack } = useNavigation();
@@ -21,6 +22,7 @@ export function SecretDetailScreen({ secretId }) {
21
22
  const [secret, setSecret] = React.useState(null);
22
23
  const [deleting, setDeleting] = React.useState(false);
23
24
  const [showDeleteConfirm, setShowDeleteConfirm] = React.useState(false);
25
+ const [showUpdateForm, setShowUpdateForm] = React.useState(false);
24
26
  // Fetch secret from API
25
27
  React.useEffect(() => {
26
28
  if (secretId && !loading && !secret) {
@@ -108,8 +110,15 @@ export function SecretDetailScreen({ secretId }) {
108
110
  },
109
111
  ],
110
112
  });
111
- // Operations available for secrets (only delete - secrets are immutable)
113
+ // Operations available for secrets
112
114
  const operations = [
115
+ {
116
+ key: "update",
117
+ label: "Update Value",
118
+ color: colors.warning,
119
+ icon: figures.pointer,
120
+ shortcut: "u",
121
+ },
113
122
  {
114
123
  key: "delete",
115
124
  label: "Delete Secret",
@@ -121,6 +130,9 @@ export function SecretDetailScreen({ secretId }) {
121
130
  // Handle operation selection
122
131
  const handleOperation = async (operation, _resource) => {
123
132
  switch (operation) {
133
+ case "update":
134
+ setShowUpdateForm(true);
135
+ break;
124
136
  case "delete":
125
137
  // Show confirmation dialog
126
138
  setShowDeleteConfirm(true);
@@ -160,7 +172,7 @@ export function SecretDetailScreen({ secretId }) {
160
172
  // Security Notice
161
173
  lines.push(_jsx(Text, { color: colors.warning, bold: true, children: "Security Notice" }, "security-title"));
162
174
  lines.push(_jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "Secret values are write-only and cannot be retrieved."] }, "security-notice"));
163
- lines.push(_jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "To change a secret, delete it and create a new one with the same name."] }, "security-notice2"));
175
+ lines.push(_jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "Use the Update Value operation to change a secret's value."] }, "security-notice2"));
164
176
  lines.push(_jsx(Text, { children: " " }, "security-space"));
165
177
  // Raw JSON (without value)
166
178
  lines.push(_jsx(Text, { color: colors.warning, bold: true, children: "Raw JSON" }, "json-title"));
@@ -176,6 +188,18 @@ export function SecretDetailScreen({ secretId }) {
176
188
  });
177
189
  return lines;
178
190
  };
191
+ // Show update form
192
+ if (showUpdateForm && secret) {
193
+ return (_jsx(SecretCreatePage, { onBack: () => setShowUpdateForm(false), onCreate: (updatedSecret) => {
194
+ // Refresh the secret data
195
+ setSecret({
196
+ ...secret,
197
+ ...updatedSecret,
198
+ update_time_ms: Date.now(),
199
+ });
200
+ setShowUpdateForm(false);
201
+ }, initialSecret: { id: secret.id, name: secret.name } }));
202
+ }
179
203
  // Show delete confirmation
180
204
  if (showDeleteConfirm && secret) {
181
205
  return (_jsx(ConfirmationPrompt, { title: "Delete Secret", message: `Are you sure you want to delete "${secret.name}"?`, details: "This action cannot be undone. Any devboxes using this secret will no longer have access to it.", breadcrumbItems: [