Skip to content

Commit 48532f0

Browse files
committed
🤖 feat: auto-generate workspace names before creation
- Add frontend name generation with debounced AI calls - New useWorkspaceName hook with auto-generate checkbox control - Frontend creates workspace with generated name, then sends message - Remove legacy backend async name generation flow - Simplify sendMessage to require workspaceId (no more null for creation) - Remove createForFirstMessage, generateAndApplyAIName, placeholder names - Backend uses preferred small models (Haiku, Codex-mini) for generation _Generated with mux_
1 parent 2c91806 commit 48532f0

File tree

20 files changed

+702
-638
lines changed

20 files changed

+702
-638
lines changed

src/browser/components/ChatInput/CreationCenterContent.tsx

Lines changed: 5 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,29 +3,23 @@ import React from "react";
33
interface CreationCenterContentProps {
44
projectName: string;
55
isSending: boolean;
6-
inputPreview?: string;
6+
workspaceName?: string;
77
}
88

99
/**
1010
* Center content displayed during workspace creation
11-
* Shows either a loading state with the user's prompt or welcome message
11+
* Shows either a loading state with the workspace name or welcome message
1212
*/
1313
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-
2014
return (
2115
<div className="flex flex-1 items-center justify-center">
2216
{props.isSending ? (
2317
<div className="max-w-xl px-8 text-center">
2418
<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>
2519
<h2 className="text-foreground mb-2 text-lg font-medium">Creating workspace</h2>
26-
{truncatedPreview && (
20+
{props.workspaceName && (
2721
<p className="text-muted text-sm leading-relaxed">
28-
Generating name for &ldquo;{truncatedPreview}&rdquo;
22+
Creating <code className="bg-separator rounded px-1">{props.workspaceName}</code>
2923
</p>
3024
)}
3125
</div>
@@ -34,7 +28,7 @@ export function CreationCenterContent(props: CreationCenterContentProps) {
3428
<h1 className="text-foreground mb-4 text-2xl font-semibold">{props.projectName}</h1>
3529
<p className="text-muted text-sm leading-relaxed">
3630
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.
31+
generated name. Configure runtime and model options below.
3832
</p>
3933
</div>
4034
)}
Lines changed: 105 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
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 } from "lucide-react";
6+
import { cn } from "@/common/lib/utils";
57

68
interface CreationControlsProps {
79
branches: string[];
@@ -17,61 +19,126 @@ interface CreationControlsProps {
1719
/** Called when user changes SSH host */
1820
onSshHostChange: (host: string) => void;
1921
disabled: boolean;
22+
/** Workspace name state */
23+
workspaceName: string;
24+
/** Whether name is being generated */
25+
isGeneratingName: boolean;
26+
/** Whether auto-generation is enabled */
27+
autoGenerateName: boolean;
28+
/** Name generation error */
29+
nameError: string | null;
30+
/** Called when auto-generate checkbox changes */
31+
onAutoGenerateChange: (enabled: boolean) => void;
32+
/** Called when user types in the name field */
33+
onNameChange: (name: string) => void;
2034
}
2135

2236
/**
2337
* Additional controls shown only during workspace creation
2438
* - Trunk branch selector (which branch to fork from) - hidden for Local runtime
2539
* - Runtime mode (Local, Worktree, SSH)
40+
* - Workspace name (auto-generated with manual override)
2641
*/
2742
export function CreationControls(props: CreationControlsProps) {
2843
// Local runtime doesn't need a trunk branch selector (uses project dir as-is)
2944
const showTrunkBranchSelector =
3045
props.branches.length > 0 && props.runtimeMode !== RUNTIME_MODE.LOCAL;
3146

47+
const { onNameChange } = props;
48+
const handleNameChange = useCallback(
49+
(e: React.ChangeEvent<HTMLInputElement>) => {
50+
onNameChange(e.target.value);
51+
},
52+
[onNameChange]
53+
);
54+
3255
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-
/>
56+
<div className="flex flex-col gap-2">
57+
{/* First row: Runtime, Branch, SSH */}
58+
<div className="flex flex-wrap items-center gap-x-3 gap-y-2">
59+
{/* Runtime Selector - icon-based with tooltips */}
60+
<RuntimeIconSelector
61+
value={props.runtimeMode}
62+
onChange={props.onRuntimeModeChange}
63+
defaultMode={props.defaultRuntimeMode}
64+
onSetDefault={props.onSetDefaultRuntime}
65+
disabled={props.disabled}
66+
/>
67+
68+
{/* Trunk Branch Selector - hidden for Local runtime */}
69+
{showTrunkBranchSelector && (
70+
<div
71+
className="flex items-center gap-1"
72+
data-component="TrunkBranchGroup"
73+
data-tutorial="trunk-branch"
74+
>
75+
<label htmlFor="trunk-branch" className="text-muted text-xs">
76+
From:
77+
</label>
78+
<Select
79+
id="trunk-branch"
80+
value={props.trunkBranch}
81+
options={props.branches}
82+
onChange={props.onTrunkBranchChange}
83+
disabled={props.disabled}
84+
className="max-w-[120px]"
85+
/>
86+
</div>
87+
)}
4288

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}
89+
{/* SSH Host Input - after From selector */}
90+
{props.runtimeMode === RUNTIME_MODE.SSH && (
91+
<input
92+
type="text"
93+
value={props.sshHost}
94+
onChange={(e) => props.onSshHostChange(e.target.value)}
95+
placeholder="user@host"
5896
disabled={props.disabled}
59-
className="max-w-[120px]"
97+
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"
6098
/>
99+
)}
100+
</div>
101+
102+
{/* Second row: Workspace name with auto-generate checkbox */}
103+
<div className="flex items-center gap-2" data-component="WorkspaceNameGroup">
104+
<label htmlFor="workspace-name" className="text-muted text-xs whitespace-nowrap">
105+
Name:
106+
</label>
107+
<div className="relative max-w-xs flex-1">
108+
<input
109+
id="workspace-name"
110+
type="text"
111+
value={props.workspaceName}
112+
onChange={handleNameChange}
113+
placeholder={props.isGeneratingName ? "Generating..." : "workspace-name"}
114+
disabled={props.disabled || props.autoGenerateName}
115+
className={cn(
116+
"bg-separator text-foreground border-border-medium focus:border-accent w-full rounded border px-2 py-0.5 pr-6 text-xs focus:outline-none disabled:opacity-50",
117+
props.nameError && "border-red-500"
118+
)}
119+
/>
120+
{/* Loading indicator when generating */}
121+
{props.isGeneratingName && (
122+
<div className="absolute top-1/2 right-1 -translate-y-1/2">
123+
<Loader2 className="text-muted h-3 w-3 animate-spin" />
124+
</div>
125+
)}
61126
</div>
62-
)}
127+
{/* Auto-generate checkbox */}
128+
<label className="text-muted flex items-center gap-1 text-[10px] whitespace-nowrap">
129+
<input
130+
type="checkbox"
131+
checked={props.autoGenerateName}
132+
onChange={(e) => props.onAutoGenerateChange(e.target.checked)}
133+
disabled={props.disabled}
134+
className="h-3 w-3"
135+
/>
136+
auto
137+
</label>
138+
</div>
63139

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"
71-
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"
73-
/>
74-
)}
140+
{/* Error display */}
141+
{props.nameError && <div className="text-xs text-red-500">{props.nameError}</div>}
75142
</div>
76143
);
77144
}

src/browser/components/ChatInput/index.tsx

Lines changed: 11 additions & 1 deletion
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.workspaceName : undefined
1197+
}
11941198
/>
11951199
)}
11961200

@@ -1400,6 +1404,12 @@ export const ChatInput: React.FC<ChatInputProps> = (props) => {
14001404
onSetDefaultRuntime={creationState.setDefaultRuntimeMode}
14011405
onSshHostChange={creationState.setSshHost}
14021406
disabled={creationState.isSending || isSending}
1407+
workspaceName={creationState.workspaceName}
1408+
isGeneratingName={creationState.isGeneratingName}
1409+
autoGenerateName={creationState.autoGenerateName}
1410+
nameError={creationState.nameError}
1411+
onAutoGenerateChange={creationState.setAutoGenerateName}
1412+
onNameChange={creationState.setWorkspaceName}
14031413
/>
14041414
)}
14051415
</div>

0 commit comments

Comments
 (0)