11import React , { useCallback , useEffect } from "react" ;
22import { RUNTIME_MODE , type RuntimeMode } from "@/common/types/runtime" ;
33import { Select } from "../Select" ;
4- import { RuntimeIconSelector } from "../RuntimeIconSelector" ;
54import { Loader2 , Wand2 } from "lucide-react" ;
65import { cn } from "@/common/lib/utils" ;
76import { Tooltip , TooltipTrigger , TooltipContent } from "../ui/tooltip" ;
7+ import { SSHIcon , WorktreeIcon , LocalIcon } from "../icons/RuntimeIcons" ;
8+ import { DocsLink } from "../DocsLink" ;
89import type { WorkspaceNameState } from "@/browser/hooks/useWorkspaceName" ;
910
1011interface CreationControlsProps {
@@ -20,17 +21,134 @@ interface CreationControlsProps {
2021 onSetDefaultRuntime : ( mode : RuntimeMode ) => void ;
2122 onSshHostChange : ( host : string ) => void ;
2223 disabled : boolean ;
24+ /** Project name to display as header */
25+ projectName : string ;
2326 /** Workspace name/title generation state and actions */
2427 nameState : WorkspaceNameState ;
2528}
2629
30+ /** Runtime type button group with icons and colors */
31+ interface RuntimeButtonGroupProps {
32+ value : RuntimeMode ;
33+ onChange : ( mode : RuntimeMode ) => void ;
34+ defaultMode : RuntimeMode ;
35+ onSetDefault : ( mode : RuntimeMode ) => void ;
36+ disabled ?: boolean ;
37+ disabledModes ?: RuntimeMode [ ] ;
38+ }
39+
40+ const RUNTIME_OPTIONS : Array < {
41+ value : RuntimeMode ;
42+ label : string ;
43+ description : string ;
44+ docsPath : string ;
45+ Icon : React . FC < { size ?: number ; className ?: string } > ;
46+ // Active state colors using CSS variables for theme support
47+ activeClass : string ;
48+ idleClass : string ;
49+ } > = [
50+ {
51+ value : RUNTIME_MODE . LOCAL ,
52+ label : "Local" ,
53+ description : "Work directly in project directory" ,
54+ docsPath : "/runtime/local" ,
55+ Icon : LocalIcon ,
56+ activeClass :
57+ "bg-[var(--color-runtime-local)]/30 text-foreground border-[var(--color-runtime-local)]/60" ,
58+ idleClass :
59+ "bg-transparent text-muted border-transparent hover:border-[var(--color-runtime-local)]/40" ,
60+ } ,
61+ {
62+ value : RUNTIME_MODE . WORKTREE ,
63+ label : "Worktree" ,
64+ description : "Isolated git worktree" ,
65+ docsPath : "/runtime/worktree" ,
66+ Icon : WorktreeIcon ,
67+ activeClass :
68+ "bg-[var(--color-runtime-worktree)]/20 text-[var(--color-runtime-worktree-text)] border-[var(--color-runtime-worktree)]/60" ,
69+ idleClass :
70+ "bg-transparent text-muted border-transparent hover:border-[var(--color-runtime-worktree)]/40" ,
71+ } ,
72+ {
73+ value : RUNTIME_MODE . SSH ,
74+ label : "Remote" ,
75+ description : "Clone on SSH host" ,
76+ docsPath : "/runtime/ssh" ,
77+ Icon : SSHIcon ,
78+ activeClass :
79+ "bg-[var(--color-runtime-ssh)]/20 text-[var(--color-runtime-ssh-text)] border-[var(--color-runtime-ssh)]/60" ,
80+ idleClass :
81+ "bg-transparent text-muted border-transparent hover:border-[var(--color-runtime-ssh)]/40" ,
82+ } ,
83+ ] ;
84+
85+ function RuntimeButtonGroup ( props : RuntimeButtonGroupProps ) {
86+ const disabledModes = props . disabledModes ?? [ ] ;
87+
88+ return (
89+ < div className = "flex gap-1" >
90+ { RUNTIME_OPTIONS . map ( ( option ) => {
91+ const isActive = props . value === option . value ;
92+ const isDefault = props . defaultMode === option . value ;
93+ const isModeDisabled = disabledModes . includes ( option . value ) ;
94+ const Icon = option . Icon ;
95+
96+ return (
97+ < Tooltip key = { option . value } >
98+ < TooltipTrigger asChild >
99+ < button
100+ type = "button"
101+ onClick = { ( ) => props . onChange ( option . value ) }
102+ disabled = { Boolean ( props . disabled ) || isModeDisabled }
103+ aria-pressed = { isActive }
104+ className = { cn (
105+ "inline-flex items-center gap-1.5 rounded-md border px-2.5 py-1.5 text-xs font-medium transition-all duration-150" ,
106+ "cursor-pointer" ,
107+ isActive ? option . activeClass : option . idleClass ,
108+ ( Boolean ( props . disabled ) || isModeDisabled ) && "cursor-not-allowed opacity-50"
109+ ) }
110+ >
111+ < Icon size = { 12 } />
112+ { option . label }
113+ </ button >
114+ </ TooltipTrigger >
115+ < TooltipContent
116+ align = "center"
117+ side = "bottom"
118+ className = "pointer-events-auto whitespace-normal"
119+ >
120+ < div className = "flex items-baseline justify-between gap-3" >
121+ < span > { option . description } </ span >
122+ < DocsLink path = { option . docsPath } />
123+ </ div >
124+ { isModeDisabled ? (
125+ < p className = "mt-1 text-yellow-500" > Requires git repository</ p >
126+ ) : (
127+ < label className = "mt-1.5 flex cursor-pointer items-center gap-1.5 text-xs" >
128+ < input
129+ type = "checkbox"
130+ checked = { isDefault }
131+ onChange = { ( ) => props . onSetDefault ( option . value ) }
132+ className = "accent-accent h-3 w-3"
133+ />
134+ < span className = "text-muted" > Default for project</ span >
135+ </ label >
136+ ) }
137+ </ TooltipContent >
138+ </ Tooltip >
139+ ) ;
140+ } ) }
141+ </ div >
142+ ) ;
143+ }
144+
27145/**
28- * Additional controls shown only during workspace creation
29- * - 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
31- * - Workspace name (auto-generated with manual override)
146+ * Prominent controls shown above the input during workspace creation.
147+ * Displays project name as header, workspace name with magic wand, and runtime/branch selectors.
32148 */
33149export function CreationControls ( props : CreationControlsProps ) {
150+ const { nameState } = props ;
151+
34152 // Non-git directories (empty branches after loading completes) can only use local runtime
35153 // Don't check until branches have loaded to avoid prematurely switching runtime
36154 const isNonGitRepo = props . branchesLoaded && props . branches . length === 0 ;
@@ -39,7 +157,7 @@ export function CreationControls(props: CreationControlsProps) {
39157 const showTrunkBranchSelector =
40158 props . branches . length > 0 && props . runtimeMode !== RUNTIME_MODE . LOCAL ;
41159
42- const { runtimeMode, onRuntimeModeChange, nameState } = props ;
160+ const { runtimeMode, onRuntimeModeChange } = props ;
43161
44162 // Force local runtime for non-git directories (only after branches loaded)
45163 useEffect ( ( ) => {
@@ -68,28 +186,42 @@ export function CreationControls(props: CreationControlsProps) {
68186 } , [ nameState ] ) ;
69187
70188 return (
71- < div className = "flex flex-col gap-2" >
72- { /* First row: Workspace name with magic wand toggle */ }
73- < div className = "flex items-center gap-2" data-component = "WorkspaceNameGroup" >
74- < label htmlFor = "workspace-name" className = "text-muted text-xs whitespace-nowrap" >
75- Name:
76- </ label >
77- < div className = "relative max-w-xs flex-1" >
78- < input
79- id = "workspace-name"
80- type = "text"
81- value = { nameState . name }
82- onChange = { handleNameChange }
83- onFocus = { handleInputFocus }
84- placeholder = { nameState . isGenerating ? "Generating..." : "workspace-name" }
85- disabled = { props . disabled }
86- className = { cn (
87- "bg-separator text-foreground border-border-medium focus:border-accent h-6 w-full rounded border px-2 pr-6 text-xs focus:outline-none disabled:opacity-50" ,
88- nameState . error && "border-red-500"
89- ) }
90- />
91- { /* Magic wand / loading indicator - vertically centered */ }
92- < div className = "absolute inset-y-0 right-0 flex items-center pr-1.5" >
189+ < div className = "mb-3 flex flex-col gap-4" >
190+ { /* Project name / workspace name header row */ }
191+ < div className = "flex items-center" data-component = "WorkspaceNameGroup" >
192+ < h2 className = "text-foreground shrink-0 text-lg font-semibold" > { props . projectName } </ h2 >
193+ < span className = "text-muted-foreground mx-2 text-lg" > /</ span >
194+
195+ { /* Name input with magic wand - uses grid overlay technique for auto-sizing */ }
196+ < div className = "relative inline-grid items-center" >
197+ { /* Hidden sizer span - determines width based on content, minimum is placeholder width */ }
198+ < span className = "invisible col-start-1 row-start-1 pr-7 text-lg font-semibold whitespace-pre" >
199+ { nameState . name || "workspace-name" }
200+ </ span >
201+ < Tooltip >
202+ < TooltipTrigger asChild >
203+ < input
204+ id = "workspace-name"
205+ type = "text"
206+ size = { 1 }
207+ value = { nameState . name }
208+ onChange = { handleNameChange }
209+ onFocus = { handleInputFocus }
210+ placeholder = { nameState . isGenerating ? "Generating..." : "workspace-name" }
211+ disabled = { props . disabled }
212+ className = { cn (
213+ "col-start-1 row-start-1 min-w-0 bg-transparent border-border-medium focus:border-accent h-7 w-full rounded-md border border-transparent text-lg font-semibold focus:border focus:bg-bg-dark focus:outline-none disabled:opacity-50" ,
214+ nameState . autoGenerate ? "text-muted" : "text-foreground" ,
215+ nameState . error && "border-red-500"
216+ ) }
217+ />
218+ </ TooltipTrigger >
219+ < TooltipContent align = "start" className = "max-w-64" >
220+ A stable identifier used for git branches, worktree folders, and session directories.
221+ </ TooltipContent >
222+ </ Tooltip >
223+ { /* Magic wand / loading indicator */ }
224+ < div className = "absolute inset-y-0 right-0 flex items-center pr-2" >
93225 { nameState . isGenerating ? (
94226 < Loader2 className = "text-accent h-3.5 w-3.5 animate-spin" />
95227 ) : (
@@ -121,54 +253,60 @@ export function CreationControls(props: CreationControlsProps) {
121253 ) }
122254 </ div >
123255 </ div >
124- { /* Error display - inline */ }
256+
257+ { /* Error display */ }
125258 { nameState . error && < span className = "text-xs text-red-500" > { nameState . error } </ span > }
126259 </ div >
127260
128- { /* Second row: Runtime, Branch, SSH */ }
129- < div className = "flex flex-wrap items-center gap-x-3 gap-y-2" >
130- { /* Runtime Selector - icon-based with tooltips */ }
131- < RuntimeIconSelector
132- value = { props . runtimeMode }
133- onChange = { props . onRuntimeModeChange }
134- defaultMode = { props . defaultRuntimeMode }
135- onSetDefault = { props . onSetDefaultRuntime }
136- disabled = { props . disabled }
137- disabledModes = { isNonGitRepo ? [ RUNTIME_MODE . WORKTREE , RUNTIME_MODE . SSH ] : undefined }
138- />
139-
140- { /* Trunk Branch Selector - hidden for Local runtime */ }
141- { showTrunkBranchSelector && (
142- < div
143- className = "flex h-6 items-center gap-1"
144- data-component = "TrunkBranchGroup"
145- data-tutorial = "trunk-branch"
146- >
147- < label htmlFor = "trunk-branch" className = "text-muted text-xs" >
148- From:
149- </ label >
150- < Select
151- id = "trunk-branch"
152- value = { props . trunkBranch }
153- options = { props . branches }
154- onChange = { props . onTrunkBranchChange }
155- disabled = { props . disabled }
156- className = "h-6 max-w-[120px]"
157- />
158- </ div >
159- ) }
160-
161- { /* SSH Host Input - after From selector */ }
162- { props . runtimeMode === RUNTIME_MODE . SSH && (
163- < input
164- type = "text"
165- value = { props . sshHost }
166- onChange = { ( e ) => props . onSshHostChange ( e . target . value ) }
167- placeholder = "user@host"
261+ { /* Runtime type - button group */ }
262+ < div className = "flex flex-col gap-1.5" data-component = "RuntimeTypeGroup" >
263+ < label className = "text-muted-foreground text-xs font-medium" > Workspace Type</ label >
264+ < div className = "flex flex-wrap items-center gap-3" >
265+ < RuntimeButtonGroup
266+ value = { props . runtimeMode }
267+ onChange = { props . onRuntimeModeChange }
268+ defaultMode = { props . defaultRuntimeMode }
269+ onSetDefault = { props . onSetDefaultRuntime }
168270 disabled = { props . disabled }
169- 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"
271+ disabledModes = { isNonGitRepo ? [ RUNTIME_MODE . WORKTREE , RUNTIME_MODE . SSH ] : undefined }
170272 />
171- ) }
273+
274+ { /* Branch selector - shown for worktree/SSH */ }
275+ { showTrunkBranchSelector && (
276+ < div
277+ className = "flex items-center gap-2"
278+ data-component = "TrunkBranchGroup"
279+ data-tutorial = "trunk-branch"
280+ >
281+ < label htmlFor = "trunk-branch" className = "text-muted-foreground text-xs" >
282+ from
283+ </ label >
284+ < Select
285+ id = "trunk-branch"
286+ value = { props . trunkBranch }
287+ options = { props . branches }
288+ onChange = { props . onTrunkBranchChange }
289+ disabled = { props . disabled }
290+ className = "h-7 max-w-[140px]"
291+ />
292+ </ div >
293+ ) }
294+
295+ { /* SSH Host Input */ }
296+ { props . runtimeMode === RUNTIME_MODE . SSH && (
297+ < div className = "flex items-center gap-2" >
298+ < label className = "text-muted-foreground text-xs" > host</ label >
299+ < input
300+ type = "text"
301+ value = { props . sshHost }
302+ onChange = { ( e ) => props . onSshHostChange ( e . target . value ) }
303+ placeholder = "user@host"
304+ disabled = { props . disabled }
305+ 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"
306+ />
307+ </ div >
308+ ) }
309+ </ div >
172310 </ div >
173311 </ div >
174312 ) ;
0 commit comments