11import React , { useCallback , useEffect } from "react" ;
2- import { RUNTIME_MODE , type RuntimeMode } from "@/common/types/runtime" ;
2+ import { RUNTIME_MODE , type RuntimeMode , type ParsedRuntime } from "@/common/types/runtime" ;
33import { Select } from "../Select" ;
44import { Loader2 , Wand2 } from "lucide-react" ;
55import { cn } from "@/common/lib/utils" ;
66import { Tooltip , TooltipTrigger , TooltipContent } from "../ui/tooltip" ;
7- import { SSHIcon , WorktreeIcon , LocalIcon } from "../icons/RuntimeIcons" ;
7+ import { SSHIcon , WorktreeIcon , LocalIcon , DockerIcon } from "../icons/RuntimeIcons" ;
88import { DocsLink } from "../DocsLink" ;
99import type { WorkspaceNameState } from "@/browser/hooks/useWorkspaceName" ;
1010
@@ -14,12 +14,12 @@ interface CreationControlsProps {
1414 branchesLoaded : boolean ;
1515 trunkBranch : string ;
1616 onTrunkBranchChange : ( branch : string ) => void ;
17- runtimeMode : RuntimeMode ;
17+ /** Currently selected runtime (discriminated union: SSH has host, Docker has image) */
18+ selectedRuntime : ParsedRuntime ;
1819 defaultRuntimeMode : RuntimeMode ;
19- sshHost : string ;
20- onRuntimeModeChange : ( mode : RuntimeMode ) => void ;
20+ /** Set the currently selected runtime (discriminated union) */
21+ onSelectedRuntimeChange : ( runtime : ParsedRuntime ) => void ;
2122 onSetDefaultRuntime : ( mode : RuntimeMode ) => void ;
22- onSshHostChange : ( host : string ) => void ;
2323 disabled : boolean ;
2424 /** Project name to display as header */
2525 projectName : string ;
@@ -80,6 +80,17 @@ const RUNTIME_OPTIONS: Array<{
8080 idleClass :
8181 "bg-transparent text-muted border-transparent hover:border-[var(--color-runtime-ssh)]/40" ,
8282 } ,
83+ {
84+ value : RUNTIME_MODE . DOCKER ,
85+ label : "Docker" ,
86+ description : "Run in Docker container" ,
87+ docsPath : "/runtime/docker" ,
88+ Icon : DockerIcon ,
89+ activeClass :
90+ "bg-[var(--color-runtime-docker)]/20 text-[var(--color-runtime-docker-text)] border-[var(--color-runtime-docker)]/60" ,
91+ idleClass :
92+ "bg-transparent text-muted border-transparent hover:border-[var(--color-runtime-docker)]/40" ,
93+ } ,
8394] ;
8495
8596function RuntimeButtonGroup ( props : RuntimeButtonGroupProps ) {
@@ -153,18 +164,20 @@ export function CreationControls(props: CreationControlsProps) {
153164 // Don't check until branches have loaded to avoid prematurely switching runtime
154165 const isNonGitRepo = props . branchesLoaded && props . branches . length === 0 ;
155166
167+ // Extract mode from discriminated union for convenience
168+ const runtimeMode = props . selectedRuntime . mode ;
169+
156170 // 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 ;
171+ const showTrunkBranchSelector = props . branches . length > 0 && runtimeMode !== RUNTIME_MODE . LOCAL ;
159172
160- const { runtimeMode , onRuntimeModeChange } = props ;
173+ const { selectedRuntime , onSelectedRuntimeChange } = props ;
161174
162175 // Force local runtime for non-git directories (only after branches loaded)
163176 useEffect ( ( ) => {
164- if ( isNonGitRepo && runtimeMode !== RUNTIME_MODE . LOCAL ) {
165- onRuntimeModeChange ( RUNTIME_MODE . LOCAL ) ;
177+ if ( isNonGitRepo && selectedRuntime . mode !== RUNTIME_MODE . LOCAL ) {
178+ onSelectedRuntimeChange ( { mode : "local" } ) ;
166179 }
167- } , [ isNonGitRepo , runtimeMode , onRuntimeModeChange ] ) ;
180+ } , [ isNonGitRepo , selectedRuntime . mode , onSelectedRuntimeChange ] ) ;
168181
169182 const handleNameChange = useCallback (
170183 ( e : React . ChangeEvent < HTMLInputElement > ) => {
@@ -263,12 +276,39 @@ export function CreationControls(props: CreationControlsProps) {
263276 < label className = "text-muted-foreground text-xs font-medium" > Workspace Type</ label >
264277 < div className = "flex flex-wrap items-center gap-3" >
265278 < RuntimeButtonGroup
266- value = { props . runtimeMode }
267- onChange = { props . onRuntimeModeChange }
279+ value = { runtimeMode }
280+ onChange = { ( mode ) => {
281+ // Convert mode to ParsedRuntime with appropriate defaults
282+ switch ( mode ) {
283+ case RUNTIME_MODE . SSH :
284+ onSelectedRuntimeChange ( {
285+ mode : "ssh" ,
286+ host : selectedRuntime . mode === "ssh" ? selectedRuntime . host : "" ,
287+ } ) ;
288+ break ;
289+ case RUNTIME_MODE . DOCKER :
290+ onSelectedRuntimeChange ( {
291+ mode : "docker" ,
292+ image : selectedRuntime . mode === "docker" ? selectedRuntime . image : "" ,
293+ } ) ;
294+ break ;
295+ case RUNTIME_MODE . LOCAL :
296+ onSelectedRuntimeChange ( { mode : "local" } ) ;
297+ break ;
298+ case RUNTIME_MODE . WORKTREE :
299+ default :
300+ onSelectedRuntimeChange ( { mode : "worktree" } ) ;
301+ break ;
302+ }
303+ } }
268304 defaultMode = { props . defaultRuntimeMode }
269305 onSetDefault = { props . onSetDefaultRuntime }
270306 disabled = { props . disabled }
271- disabledModes = { isNonGitRepo ? [ RUNTIME_MODE . WORKTREE , RUNTIME_MODE . SSH ] : undefined }
307+ disabledModes = {
308+ isNonGitRepo
309+ ? [ RUNTIME_MODE . WORKTREE , RUNTIME_MODE . SSH , RUNTIME_MODE . DOCKER ]
310+ : undefined
311+ }
272312 />
273313
274314 { /* Branch selector - shown for worktree/SSH */ }
@@ -293,19 +333,34 @@ export function CreationControls(props: CreationControlsProps) {
293333 ) }
294334
295335 { /* SSH Host Input */ }
296- { props . runtimeMode === RUNTIME_MODE . SSH && (
336+ { selectedRuntime . mode === "ssh" && (
297337 < div className = "flex items-center gap-2" >
298338 < label className = "text-muted-foreground text-xs" > host</ label >
299339 < input
300340 type = "text"
301- value = { props . sshHost }
302- onChange = { ( e ) => props . onSshHostChange ( e . target . value ) }
341+ value = { selectedRuntime . host }
342+ onChange = { ( e ) => onSelectedRuntimeChange ( { mode : "ssh" , host : e . target . value } ) }
303343 placeholder = "user@host"
304344 disabled = { props . disabled }
305345 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"
306346 />
307347 </ div >
308348 ) }
349+
350+ { /* Docker Image Input */ }
351+ { selectedRuntime . mode === "docker" && (
352+ < div className = "flex items-center gap-2" >
353+ < label className = "text-muted-foreground text-xs" > image</ label >
354+ < input
355+ type = "text"
356+ value = { selectedRuntime . image }
357+ onChange = { ( e ) => onSelectedRuntimeChange ( { mode : "docker" , image : e . target . value } ) }
358+ placeholder = "ubuntu:22.04"
359+ disabled = { props . disabled }
360+ 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"
361+ />
362+ </ div >
363+ ) }
309364 </ div >
310365 </ div >
311366 </ div >
0 commit comments