1- import React , { useCallback , useEffect } from "react" ;
2- import { RUNTIME_MODE , type RuntimeMode } from "@/common/types/runtime" ;
1+ import React , { useCallback , useEffect , useState } from "react" ;
2+ import { RUNTIME_MODE , type RuntimeMode , type ParsedRuntime } from "@/common/types/runtime" ;
33import { Select } from "../Select" ;
44import { RuntimeIconSelector } from "../RuntimeIconSelector" ;
55import { Loader2 , Wand2 } from "lucide-react" ;
@@ -13,40 +13,150 @@ interface CreationControlsProps {
1313 branchesLoaded : boolean ;
1414 trunkBranch : string ;
1515 onTrunkBranchChange : ( branch : string ) => void ;
16- runtimeMode : RuntimeMode ;
17- defaultRuntimeMode : RuntimeMode ;
18- sshHost : string ;
19- onRuntimeModeChange : ( mode : RuntimeMode ) => void ;
20- onSetDefaultRuntime : ( mode : RuntimeMode ) => void ;
21- onSshHostChange : ( host : string ) => void ;
16+ /**
17+ * Currently selected runtime (discriminated union).
18+ * null when mode requires additional input (e.g., SSH without host).
19+ */
20+ selectedRuntime : ParsedRuntime | null ;
21+ /** Persisted default runtime for this project */
22+ defaultRuntime : ParsedRuntime | null ;
23+ /** Set the currently selected runtime */
24+ onRuntimeChange : ( runtime : ParsedRuntime | null ) => void ;
25+ /** Set the default runtime for this project (persists via checkbox) */
26+ onSetDefaultRuntime : ( runtime : ParsedRuntime | null ) => void ;
2227 disabled : boolean ;
2328 /** Workspace name/title generation state and actions */
2429 nameState : WorkspaceNameState ;
2530}
2631
32+ /**
33+ * Helper to get the mode from a ParsedRuntime (for UI display)
34+ */
35+ function getRuntimeMode ( runtime : ParsedRuntime | null ) : RuntimeMode {
36+ return runtime ?. mode ?? RUNTIME_MODE . WORKTREE ;
37+ }
38+
39+ /**
40+ * Helper to get SSH host from runtime (empty string if not SSH)
41+ */
42+ function getSshHost ( runtime : ParsedRuntime | null ) : string {
43+ return runtime ?. mode === "ssh" ? runtime . host : "" ;
44+ }
45+
46+ /**
47+ * Helper to get Docker image from runtime (empty string if not Docker)
48+ */
49+ function getDockerImage ( runtime : ParsedRuntime | null ) : string {
50+ return runtime ?. mode === "docker" ? runtime . image : "" ;
51+ }
52+
2753/**
2854 * Additional controls shown only during workspace creation
2955 * - Trunk branch selector (which branch to fork from) - hidden for Local runtime
30- * - Runtime mode (Local, Worktree, SSH) - only Local available for non-git directories
56+ * - Runtime mode (Local, Worktree, SSH, Docker ) - only Local available for non-git directories
3157 * - Workspace name (auto-generated with manual override)
3258 */
3359export function CreationControls ( props : CreationControlsProps ) {
60+ const { selectedRuntime, onRuntimeChange, nameState } = props ;
61+
62+ // Derive the current mode for UI display
63+ const runtimeMode = getRuntimeMode ( selectedRuntime ) ;
64+
65+ // Local state for SSH host and Docker image input (before user commits)
66+ // This allows typing without immediately updating the runtime
67+ const [ sshHostInput , setSshHostInput ] = useState ( ( ) => getSshHost ( selectedRuntime ) ) ;
68+ const [ dockerImageInput , setDockerImageInput ] = useState ( ( ) => getDockerImage ( selectedRuntime ) ) ;
69+
70+ // Sync local input state when runtime changes externally
71+ useEffect ( ( ) => {
72+ setSshHostInput ( getSshHost ( selectedRuntime ) ) ;
73+ setDockerImageInput ( getDockerImage ( selectedRuntime ) ) ;
74+ } , [ selectedRuntime ] ) ;
75+
3476 // Non-git directories (empty branches after loading completes) can only use local runtime
3577 // Don't check until branches have loaded to avoid prematurely switching runtime
3678 const isNonGitRepo = props . branchesLoaded && props . branches . length === 0 ;
3779
3880 // Local runtime doesn't need a trunk branch selector (uses project dir as-is)
39- const showTrunkBranchSelector =
40- props . branches . length > 0 && props . runtimeMode !== RUNTIME_MODE . LOCAL ;
41-
42- const { runtimeMode, onRuntimeModeChange, nameState } = props ;
81+ const showTrunkBranchSelector = props . branches . length > 0 && runtimeMode !== RUNTIME_MODE . LOCAL ;
4382
4483 // Force local runtime for non-git directories (only after branches loaded)
4584 useEffect ( ( ) => {
4685 if ( isNonGitRepo && runtimeMode !== RUNTIME_MODE . LOCAL ) {
47- onRuntimeModeChange ( RUNTIME_MODE . LOCAL ) ;
86+ onRuntimeChange ( { mode : "local" } ) ;
4887 }
49- } , [ isNonGitRepo , runtimeMode , onRuntimeModeChange ] ) ;
88+ } , [ isNonGitRepo , runtimeMode , onRuntimeChange ] ) ;
89+
90+ // Handle mode change from RuntimeIconSelector
91+ const handleModeChange = useCallback (
92+ ( mode : RuntimeMode ) => {
93+ switch ( mode ) {
94+ case RUNTIME_MODE . LOCAL :
95+ onRuntimeChange ( { mode : "local" } ) ;
96+ break ;
97+ case RUNTIME_MODE . WORKTREE :
98+ onRuntimeChange ( { mode : "worktree" } ) ;
99+ break ;
100+ case RUNTIME_MODE . SSH :
101+ // Use saved host if available, otherwise null (user needs to enter)
102+ onRuntimeChange ( sshHostInput ? { mode : "ssh" , host : sshHostInput } : null ) ;
103+ break ;
104+ case RUNTIME_MODE . DOCKER :
105+ // Use saved image if available, otherwise null (user needs to enter)
106+ onRuntimeChange ( dockerImageInput ? { mode : "docker" , image : dockerImageInput } : null ) ;
107+ break ;
108+ }
109+ } ,
110+ [ onRuntimeChange , sshHostInput , dockerImageInput ]
111+ ) ;
112+
113+ // Handle setting default from RuntimeIconSelector
114+ const { onSetDefaultRuntime } = props ;
115+ const handleSetDefault = useCallback (
116+ ( mode : RuntimeMode ) => {
117+ switch ( mode ) {
118+ case RUNTIME_MODE . LOCAL :
119+ onSetDefaultRuntime ( { mode : "local" } ) ;
120+ break ;
121+ case RUNTIME_MODE . WORKTREE :
122+ onSetDefaultRuntime ( { mode : "worktree" } ) ;
123+ break ;
124+ case RUNTIME_MODE . SSH :
125+ // Only set default if we have a valid host
126+ if ( sshHostInput ) {
127+ onSetDefaultRuntime ( { mode : "ssh" , host : sshHostInput } ) ;
128+ }
129+ break ;
130+ case RUNTIME_MODE . DOCKER :
131+ // Only set default if we have a valid image
132+ if ( dockerImageInput ) {
133+ onSetDefaultRuntime ( { mode : "docker" , image : dockerImageInput } ) ;
134+ }
135+ break ;
136+ }
137+ } ,
138+ [ onSetDefaultRuntime , sshHostInput , dockerImageInput ]
139+ ) ;
140+
141+ // Handle SSH host input change
142+ const handleSshHostChange = useCallback (
143+ ( host : string ) => {
144+ setSshHostInput ( host ) ;
145+ // Update runtime with new host (or null if empty)
146+ onRuntimeChange ( host . trim ( ) ? { mode : "ssh" , host : host . trim ( ) } : null ) ;
147+ } ,
148+ [ onRuntimeChange ]
149+ ) ;
150+
151+ // Handle Docker image input change
152+ const handleDockerImageChange = useCallback (
153+ ( image : string ) => {
154+ setDockerImageInput ( image ) ;
155+ // Update runtime with new image (or null if empty)
156+ onRuntimeChange ( image . trim ( ) ? { mode : "docker" , image : image . trim ( ) } : null ) ;
157+ } ,
158+ [ onRuntimeChange ]
159+ ) ;
50160
51161 const handleNameChange = useCallback (
52162 ( e : React . ChangeEvent < HTMLInputElement > ) => {
@@ -125,16 +235,20 @@ export function CreationControls(props: CreationControlsProps) {
125235 { nameState . error && < span className = "text-xs text-red-500" > { nameState . error } </ span > }
126236 </ div >
127237
128- { /* Second row: Runtime, Branch, SSH */ }
238+ { /* Second row: Runtime, Branch, SSH/Docker input */ }
129239 < div className = "flex flex-wrap items-center gap-x-3 gap-y-2" >
130240 { /* Runtime Selector - icon-based with tooltips */ }
131241 < RuntimeIconSelector
132- value = { props . runtimeMode }
133- onChange = { props . onRuntimeModeChange }
134- defaultMode = { props . defaultRuntimeMode }
135- onSetDefault = { props . onSetDefaultRuntime }
242+ value = { runtimeMode }
243+ onChange = { handleModeChange }
244+ defaultMode = { getRuntimeMode ( props . defaultRuntime ) }
245+ onSetDefault = { handleSetDefault }
136246 disabled = { props . disabled }
137- disabledModes = { isNonGitRepo ? [ RUNTIME_MODE . WORKTREE , RUNTIME_MODE . SSH ] : undefined }
247+ disabledModes = {
248+ isNonGitRepo
249+ ? [ RUNTIME_MODE . WORKTREE , RUNTIME_MODE . SSH , RUNTIME_MODE . DOCKER ]
250+ : undefined
251+ }
138252 />
139253
140254 { /* Trunk Branch Selector - hidden for Local runtime */ }
@@ -158,17 +272,29 @@ export function CreationControls(props: CreationControlsProps) {
158272 </ div >
159273 ) }
160274
161- { /* SSH Host Input - after From selector */ }
162- { props . runtimeMode === RUNTIME_MODE . SSH && (
275+ { /* SSH Host Input - shown when SSH mode selected */ }
276+ { runtimeMode === RUNTIME_MODE . SSH && (
163277 < input
164278 type = "text"
165- value = { props . sshHost }
166- onChange = { ( e ) => props . onSshHostChange ( e . target . value ) }
279+ value = { sshHostInput }
280+ onChange = { ( e ) => handleSshHostChange ( e . target . value ) }
167281 placeholder = "user@host"
168282 disabled = { props . disabled }
169283 className = "bg-separator text-foreground border-border-medium focus:border-accent h-6 w-32 rounded border px-1 text-xs focus:outline-none disabled:opacity-50"
170284 />
171285 ) }
286+
287+ { /* Docker Image Input - shown when Docker mode selected */ }
288+ { runtimeMode === RUNTIME_MODE . DOCKER && (
289+ < input
290+ type = "text"
291+ value = { dockerImageInput }
292+ onChange = { ( e ) => handleDockerImageChange ( e . target . value ) }
293+ placeholder = "ubuntu:22.04"
294+ disabled = { props . disabled }
295+ className = "bg-separator text-foreground border-border-medium focus:border-accent h-6 w-32 rounded border px-1 text-xs focus:outline-none disabled:opacity-50"
296+ />
297+ ) }
172298 </ div >
173299 </ div >
174300 ) ;
0 commit comments