Skip to content

Commit d4d6816

Browse files
authored
🤖 feat: upfront workspace name generation with magic wand toggle (#910)
Workspace names are now generated **before** creation, with a magic wand toggle for auto/manual control. - New `useWorkspaceName` hook with debounced generation - Wand icon in name field: colored = auto, gray = manual - Click field to edit, preserving generated name - Falls back to user's configured model if Haiku/GPT-Mini unavailable - Removed legacy background renaming code --- _Generated with `mux`_
1 parent 3a84ef4 commit d4d6816

File tree

21 files changed

+733
-647
lines changed

21 files changed

+733
-647
lines changed

src/browser/components/ChatInput/CreationCenterContent.tsx

Lines changed: 13 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,38 +3,36 @@ import React from "react";
33
interface CreationCenterContentProps {
44
projectName: string;
55
isSending: boolean;
6-
inputPreview?: string;
6+
/** The confirmed workspace name (null while name generation is in progress) */
7+
workspaceName?: string | null;
78
}
89

910
/**
1011
* Center content displayed during workspace creation
11-
* Shows either a loading state with the user's prompt or welcome message
12+
* Shows either a loading state with the workspace name or welcome message
1213
*/
1314
export function CreationCenterContent(props: CreationCenterContentProps) {
14-
// Truncate long prompts for preview display
15-
const truncatedPreview =
16-
props.inputPreview && props.inputPreview.length > 150
17-
? props.inputPreview.slice(0, 150) + "..."
18-
: props.inputPreview;
19-
2015
return (
2116
<div className="flex flex-1 items-center justify-center">
2217
{props.isSending ? (
2318
<div className="max-w-xl px-8 text-center">
2419
<div className="bg-accent mb-4 inline-block h-8 w-8 animate-spin rounded-full border-4 border-solid border-current border-r-transparent"></div>
2520
<h2 className="text-foreground mb-2 text-lg font-medium">Creating workspace</h2>
26-
{truncatedPreview && (
27-
<p className="text-muted text-sm leading-relaxed">
28-
Generating name for &ldquo;{truncatedPreview}&rdquo;
29-
</p>
30-
)}
21+
<p className="text-muted text-sm leading-relaxed">
22+
{props.workspaceName ? (
23+
<>
24+
<code className="bg-separator rounded px-1">{props.workspaceName}</code>
25+
</>
26+
) : (
27+
"Generating name…"
28+
)}
29+
</p>
3130
</div>
3231
) : (
3332
<div className="max-w-2xl px-8 text-center">
3433
<h1 className="text-foreground mb-4 text-2xl font-semibold">{props.projectName}</h1>
3534
<p className="text-muted text-sm leading-relaxed">
36-
Describe what you want to build. A new workspace will be created with an automatically
37-
generated branch name. Configure runtime and model options below.
35+
Describe what you want to build and a workspace will be created.
3836
</p>
3937
</div>
4038
)}
Lines changed: 120 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
1-
import React from "react";
1+
import React, { useCallback } from "react";
22
import { RUNTIME_MODE, type RuntimeMode } from "@/common/types/runtime";
33
import { Select } from "../Select";
44
import { 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

610
interface 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
*/
2731
export 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
}

src/browser/components/ChatInput/index.tsx

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -244,12 +244,14 @@ export const ChatInput: React.FC<ChatInputProps> = (props) => {
244244
? {
245245
projectPath: props.projectPath,
246246
onWorkspaceCreated: props.onWorkspaceCreated,
247+
message: input,
247248
}
248249
: {
249250
// Dummy values for workspace variant (never used)
250251
projectPath: "",
251252
// eslint-disable-next-line @typescript-eslint/no-empty-function
252253
onWorkspaceCreated: () => {},
254+
message: "",
253255
}
254256
);
255257

@@ -1190,7 +1192,9 @@ export const ChatInput: React.FC<ChatInputProps> = (props) => {
11901192
<CreationCenterContent
11911193
projectName={props.projectName}
11921194
isSending={creationState.isSending || isSending}
1193-
inputPreview={creationState.isSending || isSending ? input : undefined}
1195+
workspaceName={
1196+
creationState.isSending || isSending ? creationState.creatingWithName : undefined
1197+
}
11941198
/>
11951199
)}
11961200

@@ -1387,7 +1391,7 @@ export const ChatInput: React.FC<ChatInputProps> = (props) => {
13871391
</div>
13881392
</div>
13891393

1390-
{/* Creation controls - second row for creation variant */}
1394+
{/* Creation controls - below model controls for creation variant */}
13911395
{variant === "creation" && (
13921396
<CreationControls
13931397
branches={creationState.branches}
@@ -1400,6 +1404,7 @@ export const ChatInput: React.FC<ChatInputProps> = (props) => {
14001404
onSetDefaultRuntime={creationState.setDefaultRuntimeMode}
14011405
onSshHostChange={creationState.setSshHost}
14021406
disabled={creationState.isSending || isSending}
1407+
nameState={creationState.nameState}
14031408
/>
14041409
)}
14051410
</div>

0 commit comments

Comments
 (0)