From 0a6650b9a98024c2ab2dfca7074fce04caf9fdcb Mon Sep 17 00:00:00 2001 From: Ross Date: Thu, 5 Feb 2026 09:59:44 -0700 Subject: [PATCH 1/3] Allow tunnel in create, show in info --- src/components/DevboxCreatePage.tsx | 39 +++++++++++++++-- src/components/DevboxDetailPage.tsx | 66 ++++++++++++++++++++++++++--- 2 files changed, 95 insertions(+), 10 deletions(-) diff --git a/src/components/DevboxCreatePage.tsx b/src/components/DevboxCreatePage.tsx index 964cb8ed..80a01caa 100644 --- a/src/components/DevboxCreatePage.tsx +++ b/src/components/DevboxCreatePage.tsx @@ -60,6 +60,7 @@ type FormField = | "metadata" | "source" | "network_policy_id" + | "tunnel_auth_mode" | "gateways"; // Gateway configuration for devbox @@ -94,6 +95,7 @@ interface FormData { blueprint_id: string; snapshot_id: string; network_policy_id: string; + tunnel_auth_mode: "none" | "open" | "authenticated"; gateways: GatewaySpec[]; } @@ -107,6 +109,7 @@ const resourceSizes = [ "XX_LARGE", "CUSTOM_SIZE", ] as const; +const tunnelAuthModes = ["none", "open", "authenticated"] as const; export const DevboxCreatePage = ({ onBack, @@ -127,6 +130,7 @@ export const DevboxCreatePage = ({ blueprint_id: initialBlueprintId || "", snapshot_id: initialSnapshotId || "", network_policy_id: "", + tunnel_auth_mode: "none", gateways: [], }); const [metadataKey, setMetadataKey] = React.useState(""); @@ -242,6 +246,12 @@ export const DevboxCreatePage = ({ type: "picker", placeholder: "Select a network policy...", }, + { + key: "tunnel_auth_mode", + label: "Tunnel (optional)", + type: "select", + placeholder: "none", + }, { key: "gateways", label: "Gateways (optional)", @@ -273,6 +283,13 @@ export const DevboxCreatePage = ({ currentField === "resource_size", ); + const handleTunnelNav = useFormSelectNavigation( + formData.tunnel_auth_mode, + tunnelAuthModes, + (value) => setFormData({ ...formData, tunnel_auth_mode: value }), + currentField === "tunnel_auth_mode", + ); + const handleSourceTypeNav = useFormSelectNavigation( sourceTypeToggle, sourceTypes, @@ -378,6 +395,7 @@ export const DevboxCreatePage = ({ // Handle select field navigation using shared hooks if (handleArchitectureNav(input, key)) return; if (handleResourceSizeNav(input, key)) return; + if (handleTunnelNav(input, key)) return; if (handleSourceTypeNav(input, key)) return; // Navigation (up/down arrows and tab/shift+tab) @@ -767,6 +785,13 @@ export const DevboxCreatePage = ({ createParams.gateways = gateways; } + // Add tunnel configuration if not "none" + if (formData.tunnel_auth_mode !== "none") { + createParams.tunnel = { + auth_mode: formData.tunnel_auth_mode, + }; + } + const devbox = await client.devboxes.create(createParams); setResult(devbox); } catch (err) { @@ -1253,14 +1278,22 @@ export const DevboxCreatePage = ({ if (field.type === "select") { const value = fieldData as string; + let options: readonly string[]; + if (field.key === "architecture") { + options = architectures; + } else if (field.key === "resource_size") { + options = resourceSizes; + } else if (field.key === "tunnel_auth_mode") { + options = tunnelAuthModes; + } else { + options = []; + } return ( setFormData({ ...formData, [field.key]: newValue }) } diff --git a/src/components/DevboxDetailPage.tsx b/src/components/DevboxDetailPage.tsx index 45b3c472..30f49969 100644 --- a/src/components/DevboxDetailPage.tsx +++ b/src/components/DevboxDetailPage.tsx @@ -91,13 +91,6 @@ export const DevboxDetailPage = ({ icon: figures.arrowRight, shortcut: "s", }, - { - key: "tunnel", - label: "Open Tunnel", - color: colors.secondary, - icon: figures.pointerSmall, - shortcut: "t", - }, { key: "suspend", label: "Suspend Devbox", @@ -265,6 +258,25 @@ export const DevboxDetailPage = ({ }); } + // Tunnel URL + if (devbox.tunnel && devbox.tunnel.tunnel_key) { + const tunnelKey = devbox.tunnel.tunnel_key; + const authMode = devbox.tunnel.auth_mode; + const tunnelUrl = `https://{port}-${tunnelKey}.tunnel.runloop.ai`; + + detailFields.push({ + label: "Tunnel URL", + value: ( + <> + {tunnelUrl} + {authMode === "authenticated" && ( + (authenticated) + )} + + ), + }); + } + // Initiator if (devbox.initiator_id) { detailFields.push({ @@ -520,6 +532,46 @@ export const DevboxDetailPage = ({ lines.push( ); } + // Tunnel Information + if (devbox.tunnel && devbox.tunnel.tunnel_key) { + lines.push( + + Tunnel + , + ); + lines.push( + + {" "} + Tunnel Key: {devbox.tunnel.tunnel_key} + , + ); + lines.push( + + {" "} + Auth Mode: {devbox.tunnel.auth_mode} + , + ); + + const tunnelUrl = `https://{port}-${devbox.tunnel.tunnel_key}.tunnel.runloop.ai`; + lines.push( + + {" "} + Tunnel URL: {tunnelUrl} + , + ); + + if (devbox.tunnel.auth_token) { + lines.push( + + {" "} + Auth Token: {devbox.tunnel.auth_token} + , + ); + } + + lines.push( ); + } + // Source if (devbox.blueprint_id || devbox.snapshot_id) { lines.push( From 0ab36a4bf10c94593b04ad506fce4b598eab62e8 Mon Sep 17 00:00:00 2001 From: Alexander Dines Date: Mon, 9 Feb 2026 13:23:45 -0800 Subject: [PATCH 2/3] cp dines --- src/commands/devbox/list.tsx | 52 +++++++++++------ src/components/DevboxActionsMenu.tsx | 58 ++++++++++++------- src/components/DevboxDetailPage.tsx | 83 ++++++++++++++++++++-------- 3 files changed, 132 insertions(+), 61 deletions(-) diff --git a/src/commands/devbox/list.tsx b/src/commands/devbox/list.tsx index 1fd6bf52..dd8170b5 100644 --- a/src/commands/devbox/list.tsx +++ b/src/commands/devbox/list.tsx @@ -423,28 +423,46 @@ const ListDevboxesUI = ({ const endIndex = startIndex + devboxes.length; // Filter operations based on devbox status + const hasTunnel = !!( + selectedDevbox?.tunnel && selectedDevbox.tunnel.tunnel_key + ); const operations = selectedDevbox - ? allOperations.filter((op) => { - const devboxStatus = selectedDevbox.status; + ? allOperations + .filter((op) => { + const devboxStatus = selectedDevbox.status; - if (devboxStatus === "suspended") { - return op.key === "resume" || op.key === "logs"; - } + if (devboxStatus === "suspended") { + return op.key === "resume" || op.key === "logs"; + } - if ( - devboxStatus !== "running" && - devboxStatus !== "provisioning" && - devboxStatus !== "initializing" - ) { - return op.key === "logs"; - } + if ( + devboxStatus !== "running" && + devboxStatus !== "provisioning" && + devboxStatus !== "initializing" + ) { + return op.key === "logs"; + } - if (devboxStatus === "running") { - return op.key !== "resume"; - } + if (devboxStatus === "running") { + return op.key !== "resume"; + } - return op.key === "logs" || op.key === "delete"; - }) + return op.key === "logs" || op.key === "delete"; + }) + .map((op) => { + // Dynamic tunnel label based on whether tunnel is active + if (op.key === "tunnel") { + return hasTunnel + ? { + ...op, + label: "Tunnel (Active)", + color: colors.success, + icon: figures.tick, + } + : op; + } + return op; + }) : allOperations; useInput((input, key) => { diff --git a/src/components/DevboxActionsMenu.tsx b/src/components/DevboxActionsMenu.tsx index d4d8fb51..8869eecc 100644 --- a/src/components/DevboxActionsMenu.tsx +++ b/src/components/DevboxActionsMenu.tsx @@ -194,32 +194,48 @@ export const DevboxActionsMenu = ({ ]; // Filter operations based on devbox status + const hasTunnel = !!(devbox?.tunnel && devbox.tunnel.tunnel_key); const operations = devbox - ? allOperations.filter((op) => { - const status = devbox.status; + ? allOperations + .filter((op) => { + const status = devbox.status; - // When suspended: logs and resume - if (status === "suspended") { - return op.key === "resume" || op.key === "logs"; - } + // When suspended: logs and resume + if (status === "suspended") { + return op.key === "resume" || op.key === "logs"; + } - // When not running (shutdown, failure, etc): only logs - if ( - status !== "running" && - status !== "provisioning" && - status !== "initializing" - ) { - return op.key === "logs"; - } + // When not running (shutdown, failure, etc): only logs + if ( + status !== "running" && + status !== "provisioning" && + status !== "initializing" + ) { + return op.key === "logs"; + } - // When running: everything except resume - if (status === "running") { - return op.key !== "resume"; - } + // When running: everything except resume + if (status === "running") { + return op.key !== "resume"; + } - // Default for transitional states (provisioning, initializing) - return op.key === "logs" || op.key === "delete"; - }) + // Default for transitional states (provisioning, initializing) + return op.key === "logs" || op.key === "delete"; + }) + .map((op) => { + // Dynamic tunnel label based on whether tunnel is active + if (op.key === "tunnel") { + return hasTunnel + ? { + ...op, + label: "Tunnel (Active)", + color: colors.success, + icon: figures.tick, + } + : op; + } + return op; + }) : allOperations; // Auto-execute operations that don't need input (except delete which needs confirmation) diff --git a/src/components/DevboxDetailPage.tsx b/src/components/DevboxDetailPage.tsx index 30f49969..851fb0ef 100644 --- a/src/components/DevboxDetailPage.tsx +++ b/src/components/DevboxDetailPage.tsx @@ -91,6 +91,13 @@ export const DevboxDetailPage = ({ icon: figures.arrowRight, shortcut: "s", }, + { + key: "tunnel", + label: "Open Tunnel", + color: colors.secondary, + icon: figures.pointerSmall, + shortcut: "t", + }, { key: "suspend", label: "Suspend Devbox", @@ -116,31 +123,48 @@ export const DevboxDetailPage = ({ // Filter operations based on devbox status const getFilteredOperations = (devbox: Devbox): ResourceOperation[] => { - return allOperations.filter((op) => { - const status = devbox.status; + const hasTunnel = !!(devbox.tunnel && devbox.tunnel.tunnel_key); - // When suspended: logs and resume - if (status === "suspended") { - return op.key === "resume" || op.key === "logs"; - } + return allOperations + .filter((op) => { + const status = devbox.status; - // When not running (shutdown, failure, etc): only logs - if ( - status !== "running" && - status !== "provisioning" && - status !== "initializing" - ) { - return op.key === "logs"; - } + // When suspended: logs and resume + if (status === "suspended") { + return op.key === "resume" || op.key === "logs"; + } - // When running: everything except resume - if (status === "running") { - return op.key !== "resume"; - } + // When not running (shutdown, failure, etc): only logs + if ( + status !== "running" && + status !== "provisioning" && + status !== "initializing" + ) { + return op.key === "logs"; + } - // Default for transitional states (provisioning, initializing) - return op.key === "logs" || op.key === "delete"; - }); + // When running: everything except resume + if (status === "running") { + return op.key !== "resume"; + } + + // Default for transitional states (provisioning, initializing) + return op.key === "logs" || op.key === "delete"; + }) + .map((op) => { + // Dynamic tunnel label based on whether tunnel is active + if (op.key === "tunnel") { + return hasTunnel + ? { + ...op, + label: "Tunnel (Active)", + color: colors.success, + icon: figures.tick, + } + : op; + } + return op; + }); }; // Build detail sections for the devbox @@ -258,16 +282,20 @@ export const DevboxDetailPage = ({ }); } - // Tunnel URL + // Tunnel status - always show when running if (devbox.tunnel && devbox.tunnel.tunnel_key) { const tunnelKey = devbox.tunnel.tunnel_key; const authMode = devbox.tunnel.auth_mode; const tunnelUrl = `https://{port}-${tunnelKey}.tunnel.runloop.ai`; detailFields.push({ - label: "Tunnel URL", + label: "Tunnel", value: ( <> + + {figures.tick} Active + + {tunnelUrl} {authMode === "authenticated" && ( (authenticated) @@ -275,6 +303,15 @@ export const DevboxDetailPage = ({ ), }); + } else if (devbox.status === "running") { + detailFields.push({ + label: "Tunnel", + value: ( + + {figures.cross} Off + + ), + }); } // Initiator From 47ccab1d4b10a48449d1ef92da5fe8749bda5e22 Mon Sep 17 00:00:00 2001 From: Alexander Dines Date: Mon, 9 Feb 2026 15:59:16 -0800 Subject: [PATCH 3/3] cp dines --- src/components/DevboxActionsMenu.tsx | 3 ++- src/components/ResourceActionsMenu.tsx | 4 +++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/components/DevboxActionsMenu.tsx b/src/components/DevboxActionsMenu.tsx index 8869eecc..987aa22a 100644 --- a/src/components/DevboxActionsMenu.tsx +++ b/src/components/DevboxActionsMenu.tsx @@ -23,6 +23,7 @@ import { createSSHKey, } from "../services/devboxService.js"; import { StreamingLogsViewer } from "./StreamingLogsViewer.js"; +import { DevboxView } from "@runloop/api-client/resources/devboxes.mjs"; type Operation = | "exec" @@ -37,7 +38,7 @@ type Operation = | null; interface DevboxActionsMenuProps { - devbox: any; + devbox: DevboxView; onBack: () => void; breadcrumbItems?: Array<{ label: string; active?: boolean }>; initialOperation?: string; // Operation to execute immediately diff --git a/src/components/ResourceActionsMenu.tsx b/src/components/ResourceActionsMenu.tsx index cda6e85e..1d461479 100644 --- a/src/components/ResourceActionsMenu.tsx +++ b/src/components/ResourceActionsMenu.tsx @@ -6,6 +6,7 @@ import { Breadcrumb } from "./Breadcrumb.js"; import { NavigationTips } from "./NavigationTips.js"; import { ActionsPopup } from "./ActionsPopup.js"; import { DevboxActionsMenu } from "./DevboxActionsMenu.js"; +import type { Devbox } from "../store/devboxStore.js"; type OperationDef = { key: string; @@ -27,8 +28,9 @@ interface BaseProps { skipOperationsMenu?: boolean; } -type DevboxMenuProps = BaseProps & { +type DevboxMenuProps = Omit & { resourceType: "devbox"; + resource: Devbox; }; type BlueprintMenuProps = BaseProps & {