Skip to content

Commit 59be949

Browse files
🤖 feat: redesign chat creation UI (#1094)
## Summary Redesigns the workspace creation experience to be more prominent and user-friendly. ### Changes - **Centered card layout**: Creation form now centered vertically/horizontally with card styling, replacing the previous bottom-pinned input with splash screen - **Labeled runtime buttons**: Replaced icon-only runtime selector with a button group showing labels (Local/Worktree/Remote) with icons and themed colors - **Wide model selector**: New `displayMode="wide"` shows provider icon + formatted model name (e.g., "Sonnet 4.5") instead of raw model string - **Removed workspace name field**: Names are always auto-generated from the user's message - **Simplified center content**: `CreationCenterContent` now only renders as an overlay during workspace creation loading state ### Storybook Added `CreateWorkspace` and `CreateWorkspaceMultipleProjects` stories for visual testing. --- _Generated with `mux`_ --------- Co-authored-by: Ammar <ammar+ai@ammar.io>
1 parent 3700190 commit 59be949

File tree

9 files changed

+378
-137
lines changed

9 files changed

+378
-137
lines changed

src/browser/components/ChatInput/CreationCenterContent.tsx

Lines changed: 24 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -10,37 +10,33 @@ interface CreationCenterContentProps {
1010
}
1111

1212
/**
13-
* Center content displayed during workspace creation
14-
* Shows either a loading state with the workspace name or welcome message
13+
* Loading overlay displayed during workspace creation.
14+
* Shown as an overlay when isSending is true.
1515
*/
1616
export function CreationCenterContent(props: CreationCenterContentProps) {
17+
// Only render when actually sending/creating
18+
if (!props.isSending) {
19+
return null;
20+
}
21+
1722
return (
18-
<div className="flex flex-1 items-center justify-center">
19-
{props.isSending ? (
20-
<div className="max-w-xl px-8 text-center">
21-
<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>
22-
<h2 className="text-foreground mb-2 text-lg font-medium">Creating workspace</h2>
23-
<p className="text-muted text-sm leading-relaxed">
24-
{props.workspaceName ? (
25-
<>
26-
<code className="bg-separator rounded px-1">{props.workspaceName}</code>
27-
{props.workspaceTitle && (
28-
<span className="text-muted-foreground ml-1">{props.workspaceTitle}</span>
29-
)}
30-
</>
31-
) : (
32-
"Generating name…"
33-
)}
34-
</p>
35-
</div>
36-
) : (
37-
<div className="max-w-2xl px-8 text-center">
38-
<h1 className="text-foreground mb-4 text-2xl font-semibold">{props.projectName}</h1>
39-
<p className="text-muted text-sm leading-relaxed">
40-
Describe what you want to build and a workspace will be created.
41-
</p>
42-
</div>
43-
)}
23+
<div className="bg-bg-dark/80 absolute inset-0 z-10 flex items-center justify-center backdrop-blur-sm">
24+
<div className="max-w-xl px-8 text-center">
25+
<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>
26+
<h2 className="text-foreground mb-2 text-lg font-medium">Creating workspace</h2>
27+
<p className="text-muted text-sm leading-relaxed">
28+
{props.workspaceName ? (
29+
<>
30+
<code className="bg-separator rounded px-1">{props.workspaceName}</code>
31+
{props.workspaceTitle && (
32+
<span className="text-muted-foreground ml-1">{props.workspaceTitle}</span>
33+
)}
34+
</>
35+
) : (
36+
"Generating name…"
37+
)}
38+
</p>
39+
</div>
4440
</div>
4541
);
4642
}

src/browser/components/ChatInput/CreationControls.tsx

Lines changed: 209 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
import React, { useCallback, useEffect } from "react";
22
import { RUNTIME_MODE, type RuntimeMode } from "@/common/types/runtime";
33
import { Select } from "../Select";
4-
import { RuntimeIconSelector } from "../RuntimeIconSelector";
54
import { Loader2, Wand2 } from "lucide-react";
65
import { cn } from "@/common/lib/utils";
76
import { Tooltip, TooltipTrigger, TooltipContent } from "../ui/tooltip";
7+
import { SSHIcon, WorktreeIcon, LocalIcon } from "../icons/RuntimeIcons";
8+
import { DocsLink } from "../DocsLink";
89
import type { WorkspaceNameState } from "@/browser/hooks/useWorkspaceName";
910

1011
interface 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
*/
33149
export 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

Comments
 (0)