11import React , { useCallback , useEffect } from "react" ;
2- import { RUNTIME_MODE , type RuntimeMode } from "@/common/types/runtime" ;
2+ import {
3+ RUNTIME_MODE ,
4+ RUNTIME_MODES_REQUIRING_GIT ,
5+ type RuntimeMode ,
6+ type ParsedRuntime ,
7+ } from "@/common/types/runtime" ;
38import { Select } from "../Select" ;
49import { Loader2 , Wand2 } from "lucide-react" ;
510import { cn } from "@/common/lib/utils" ;
611import { Tooltip , TooltipTrigger , TooltipContent } from "../ui/tooltip" ;
7- import { SSHIcon , WorktreeIcon , LocalIcon } from "../icons/RuntimeIcons" ;
12+ import { SSHIcon , WorktreeIcon , LocalIcon , DockerIcon } from "../icons/RuntimeIcons" ;
813import { DocsLink } from "../DocsLink" ;
914import type { WorkspaceNameState } from "@/browser/hooks/useWorkspaceName" ;
1015
@@ -14,12 +19,12 @@ interface CreationControlsProps {
1419 branchesLoaded : boolean ;
1520 trunkBranch : string ;
1621 onTrunkBranchChange : ( branch : string ) => void ;
17- runtimeMode : RuntimeMode ;
22+ /** Currently selected runtime (discriminated union: SSH has host, Docker has image) */
23+ selectedRuntime : ParsedRuntime ;
1824 defaultRuntimeMode : RuntimeMode ;
19- sshHost : string ;
20- onRuntimeModeChange : ( mode : RuntimeMode ) => void ;
25+ /** Set the currently selected runtime (discriminated union) */
26+ onSelectedRuntimeChange : ( runtime : ParsedRuntime ) => void ;
2127 onSetDefaultRuntime : ( mode : RuntimeMode ) => void ;
22- onSshHostChange : ( host : string ) => void ;
2328 disabled : boolean ;
2429 /** Project name to display as header */
2530 projectName : string ;
@@ -80,6 +85,17 @@ const RUNTIME_OPTIONS: Array<{
8085 idleClass :
8186 "bg-transparent text-muted border-transparent hover:border-[var(--color-runtime-ssh)]/40" ,
8287 } ,
88+ {
89+ value : RUNTIME_MODE . DOCKER ,
90+ label : "Docker" ,
91+ description : "Run in Docker container" ,
92+ docsPath : "/runtime/docker" ,
93+ Icon : DockerIcon ,
94+ activeClass :
95+ "bg-[var(--color-runtime-docker)]/20 text-[var(--color-runtime-docker-text)] border-[var(--color-runtime-docker)]/60" ,
96+ idleClass :
97+ "bg-transparent text-muted border-transparent hover:border-[var(--color-runtime-docker)]/40" ,
98+ } ,
8399] ;
84100
85101function RuntimeButtonGroup ( props : RuntimeButtonGroupProps ) {
@@ -153,18 +169,20 @@ export function CreationControls(props: CreationControlsProps) {
153169 // Don't check until branches have loaded to avoid prematurely switching runtime
154170 const isNonGitRepo = props . branchesLoaded && props . branches . length === 0 ;
155171
172+ // Extract mode from discriminated union for convenience
173+ const runtimeMode = props . selectedRuntime . mode ;
174+
156175 // Local runtime doesn't need a trunk branch selector (uses project dir as-is)
157- const showTrunkBranchSelector =
158- props . branches . length > 0 && props . runtimeMode !== RUNTIME_MODE . LOCAL ;
176+ const showTrunkBranchSelector = props . branches . length > 0 && runtimeMode !== RUNTIME_MODE . LOCAL ;
159177
160- const { runtimeMode , onRuntimeModeChange } = props ;
178+ const { selectedRuntime , onSelectedRuntimeChange } = props ;
161179
162180 // Force local runtime for non-git directories (only after branches loaded)
163181 useEffect ( ( ) => {
164- if ( isNonGitRepo && runtimeMode !== RUNTIME_MODE . LOCAL ) {
165- onRuntimeModeChange ( RUNTIME_MODE . LOCAL ) ;
182+ if ( isNonGitRepo && selectedRuntime . mode !== RUNTIME_MODE . LOCAL ) {
183+ onSelectedRuntimeChange ( { mode : "local" } ) ;
166184 }
167- } , [ isNonGitRepo , runtimeMode , onRuntimeModeChange ] ) ;
185+ } , [ isNonGitRepo , selectedRuntime . mode , onSelectedRuntimeChange ] ) ;
168186
169187 const handleNameChange = useCallback (
170188 ( e : React . ChangeEvent < HTMLInputElement > ) => {
@@ -263,12 +281,35 @@ export function CreationControls(props: CreationControlsProps) {
263281 < label className = "text-muted-foreground text-xs font-medium" > Workspace Type</ label >
264282 < div className = "flex flex-wrap items-center gap-3" >
265283 < RuntimeButtonGroup
266- value = { props . runtimeMode }
267- onChange = { props . onRuntimeModeChange }
284+ value = { runtimeMode }
285+ onChange = { ( mode ) => {
286+ // Convert mode to ParsedRuntime with appropriate defaults
287+ switch ( mode ) {
288+ case RUNTIME_MODE . SSH :
289+ onSelectedRuntimeChange ( {
290+ mode : "ssh" ,
291+ host : selectedRuntime . mode === "ssh" ? selectedRuntime . host : "" ,
292+ } ) ;
293+ break ;
294+ case RUNTIME_MODE . DOCKER :
295+ onSelectedRuntimeChange ( {
296+ mode : "docker" ,
297+ image : selectedRuntime . mode === "docker" ? selectedRuntime . image : "" ,
298+ } ) ;
299+ break ;
300+ case RUNTIME_MODE . LOCAL :
301+ onSelectedRuntimeChange ( { mode : "local" } ) ;
302+ break ;
303+ case RUNTIME_MODE . WORKTREE :
304+ default :
305+ onSelectedRuntimeChange ( { mode : "worktree" } ) ;
306+ break ;
307+ }
308+ } }
268309 defaultMode = { props . defaultRuntimeMode }
269310 onSetDefault = { props . onSetDefaultRuntime }
270311 disabled = { props . disabled }
271- disabledModes = { isNonGitRepo ? [ RUNTIME_MODE . WORKTREE , RUNTIME_MODE . SSH ] : undefined }
312+ disabledModes = { isNonGitRepo ? RUNTIME_MODES_REQUIRING_GIT : undefined }
272313 />
273314
274315 { /* Branch selector - shown for worktree/SSH */ }
@@ -293,19 +334,34 @@ export function CreationControls(props: CreationControlsProps) {
293334 ) }
294335
295336 { /* SSH Host Input */ }
296- { props . runtimeMode === RUNTIME_MODE . SSH && (
337+ { selectedRuntime . mode === "ssh" && (
297338 < div className = "flex items-center gap-2" >
298339 < label className = "text-muted-foreground text-xs" > host</ label >
299340 < input
300341 type = "text"
301- value = { props . sshHost }
302- onChange = { ( e ) => props . onSshHostChange ( e . target . value ) }
342+ value = { selectedRuntime . host }
343+ onChange = { ( e ) => onSelectedRuntimeChange ( { mode : "ssh" , host : e . target . value } ) }
303344 placeholder = "user@host"
304345 disabled = { props . disabled }
305346 className = "bg-bg-dark text-foreground border-border-medium focus:border-accent h-7 w-36 rounded-md border px-2 text-sm focus:outline-none disabled:opacity-50"
306347 />
307348 </ div >
308349 ) }
350+
351+ { /* Docker Image Input */ }
352+ { selectedRuntime . mode === "docker" && (
353+ < div className = "flex items-center gap-2" >
354+ < label className = "text-muted-foreground text-xs" > image</ label >
355+ < input
356+ type = "text"
357+ value = { selectedRuntime . image }
358+ onChange = { ( e ) => onSelectedRuntimeChange ( { mode : "docker" , image : e . target . value } ) }
359+ placeholder = "ubuntu:22.04"
360+ disabled = { props . disabled }
361+ className = "bg-bg-dark text-foreground border-border-medium focus:border-accent h-7 w-36 rounded-md border px-2 text-sm focus:outline-none disabled:opacity-50"
362+ />
363+ </ div >
364+ ) }
309365 </ div >
310366 </ div >
311367 </ div >
0 commit comments