1- import React from "react" ;
1+ import React , { useCallback } from "react" ;
22import { RUNTIME_MODE , type RuntimeMode } from "@/common/types/runtime" ;
33import { Select } from "../Select" ;
44import { RuntimeIconSelector } from "../RuntimeIconSelector" ;
5+ import { Loader2 , Wand2 } from "lucide-react" ;
6+ import { cn } from "@/common/lib/utils" ;
7+ import { Tooltip , TooltipWrapper } from "../Tooltip" ;
8+ import type { WorkspaceNameState } from "@/browser/hooks/useWorkspaceName" ;
59
610interface CreationControlsProps {
711 branches : string [ ] ;
@@ -10,68 +14,144 @@ interface CreationControlsProps {
1014 runtimeMode : RuntimeMode ;
1115 defaultRuntimeMode : RuntimeMode ;
1216 sshHost : string ;
13- /** Called when user clicks a runtime icon to select it (does not persist) */
1417 onRuntimeModeChange : ( mode : RuntimeMode ) => void ;
15- /** Called when user checks "Default for project" checkbox (persists) */
1618 onSetDefaultRuntime : ( mode : RuntimeMode ) => void ;
17- /** Called when user changes SSH host */
1819 onSshHostChange : ( host : string ) => void ;
1920 disabled : boolean ;
21+ /** Workspace name generation state and actions */
22+ nameState : WorkspaceNameState ;
2023}
2124
2225/**
2326 * Additional controls shown only during workspace creation
2427 * - Trunk branch selector (which branch to fork from) - hidden for Local runtime
2528 * - Runtime mode (Local, Worktree, SSH)
29+ * - Workspace name (auto-generated with manual override)
2630 */
2731export function CreationControls ( props : CreationControlsProps ) {
2832 // Local runtime doesn't need a trunk branch selector (uses project dir as-is)
2933 const showTrunkBranchSelector =
3034 props . branches . length > 0 && props . runtimeMode !== RUNTIME_MODE . LOCAL ;
3135
32- return (
33- < div className = "flex flex-wrap items-center gap-x-3 gap-y-2" >
34- { /* Runtime Selector - icon-based with tooltips */ }
35- < RuntimeIconSelector
36- value = { props . runtimeMode }
37- onChange = { props . onRuntimeModeChange }
38- defaultMode = { props . defaultRuntimeMode }
39- onSetDefault = { props . onSetDefaultRuntime }
40- disabled = { props . disabled }
41- />
36+ const { nameState } = props ;
37+
38+ const handleNameChange = useCallback (
39+ ( e : React . ChangeEvent < HTMLInputElement > ) => {
40+ nameState . setName ( e . target . value ) ;
41+ } ,
42+ [ nameState ]
43+ ) ;
44+
45+ // Clicking into the input disables auto-generation so user can edit
46+ const handleInputFocus = useCallback ( ( ) => {
47+ if ( nameState . autoGenerate ) {
48+ nameState . setAutoGenerate ( false ) ;
49+ }
50+ } , [ nameState ] ) ;
51+
52+ // Toggle auto-generation via wand button
53+ const handleWandClick = useCallback ( ( ) => {
54+ nameState . setAutoGenerate ( ! nameState . autoGenerate ) ;
55+ } , [ nameState ] ) ;
4256
43- { /* Trunk Branch Selector - hidden for Local runtime */ }
44- { showTrunkBranchSelector && (
45- < div
46- className = "flex items-center gap-1"
47- data-component = "TrunkBranchGroup"
48- data-tutorial = "trunk-branch"
49- >
50- < label htmlFor = "trunk-branch" className = "text-muted text-xs " >
51- From:
52- </ label >
53- < Select
54- id = "trunk-branch"
55- value = { props . trunkBranch }
56- options = { props . branches }
57- onChange = { props . onTrunkBranchChange }
57+ return (
58+ < div className = "flex flex-col gap-2" >
59+ { /* First row: Workspace name with magic wand toggle */ }
60+ < div className = "flex items-center gap-2" data-component = "WorkspaceNameGroup" >
61+ < label htmlFor = "workspace-name" className = "text-muted text-xs whitespace-nowrap" >
62+ Name:
63+ </ label >
64+ < div className = "relative max-w-xs flex-1 " >
65+ < input
66+ id = "workspace-name"
67+ type = "text"
68+ value = { nameState . name }
69+ onChange = { handleNameChange }
70+ onFocus = { handleInputFocus }
71+ placeholder = { nameState . isGenerating ? "Generating..." : "workspace-name" }
5872 disabled = { props . disabled }
59- className = "max-w-[120px]"
73+ className = { cn (
74+ "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" ,
75+ nameState . error && "border-red-500"
76+ ) }
6077 />
78+ { /* Magic wand / loading indicator - vertically centered */ }
79+ < div className = "absolute inset-y-0 right-0 flex items-center pr-1.5" >
80+ { nameState . isGenerating ? (
81+ < Loader2 className = "text-accent h-3.5 w-3.5 animate-spin" />
82+ ) : (
83+ < TooltipWrapper inline >
84+ < button
85+ type = "button"
86+ onClick = { handleWandClick }
87+ disabled = { props . disabled }
88+ className = "flex h-full items-center disabled:opacity-50"
89+ aria-label = { nameState . autoGenerate ? "Disable auto-naming" : "Enable auto-naming" }
90+ >
91+ < Wand2
92+ className = { cn (
93+ "h-3.5 w-3.5 transition-colors" ,
94+ nameState . autoGenerate
95+ ? "text-accent"
96+ : "text-muted-foreground opacity-50 hover:opacity-75"
97+ ) }
98+ />
99+ </ button >
100+ < Tooltip className = "tooltip" align = "center" >
101+ { nameState . autoGenerate ? "Auto-naming enabled" : "Click to enable auto-naming" }
102+ </ Tooltip >
103+ </ TooltipWrapper >
104+ ) }
105+ </ div >
61106 </ div >
62- ) }
107+ { /* Error display - inline */ }
108+ { nameState . error && < span className = "text-xs text-red-500" > { nameState . error } </ span > }
109+ </ div >
63110
64- { /* SSH Host Input - after From selector */ }
65- { props . runtimeMode === RUNTIME_MODE . SSH && (
66- < input
67- type = "text"
68- value = { props . sshHost }
69- onChange = { ( e ) => props . onSshHostChange ( e . target . value ) }
70- placeholder = "user@host"
111+ { /* Second row: Runtime, Branch, SSH */ }
112+ < div className = "flex flex-wrap items-center gap-x-3 gap-y-2" >
113+ { /* Runtime Selector - icon-based with tooltips */ }
114+ < RuntimeIconSelector
115+ value = { props . runtimeMode }
116+ onChange = { props . onRuntimeModeChange }
117+ defaultMode = { props . defaultRuntimeMode }
118+ onSetDefault = { props . onSetDefaultRuntime }
71119 disabled = { props . disabled }
72- className = "bg-separator text-foreground border-border-medium focus:border-accent w-32 rounded border px-1 py-0.5 text-xs focus:outline-none disabled:opacity-50"
73120 />
74- ) }
121+
122+ { /* Trunk Branch Selector - hidden for Local runtime */ }
123+ { showTrunkBranchSelector && (
124+ < div
125+ className = "flex h-6 items-center gap-1"
126+ data-component = "TrunkBranchGroup"
127+ data-tutorial = "trunk-branch"
128+ >
129+ < label htmlFor = "trunk-branch" className = "text-muted text-xs" >
130+ From:
131+ </ label >
132+ < Select
133+ id = "trunk-branch"
134+ value = { props . trunkBranch }
135+ options = { props . branches }
136+ onChange = { props . onTrunkBranchChange }
137+ disabled = { props . disabled }
138+ className = "h-6 max-w-[120px]"
139+ />
140+ </ div >
141+ ) }
142+
143+ { /* SSH Host Input - after From selector */ }
144+ { props . runtimeMode === RUNTIME_MODE . SSH && (
145+ < input
146+ type = "text"
147+ value = { props . sshHost }
148+ onChange = { ( e ) => props . onSshHostChange ( e . target . value ) }
149+ placeholder = "user@host"
150+ disabled = { props . disabled }
151+ 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"
152+ />
153+ ) }
154+ </ div >
75155 </ div >
76156 ) ;
77157}
0 commit comments