Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 13 additions & 15 deletions src/browser/components/ChatInput/CreationCenterContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,38 +3,36 @@ import React from "react";
interface CreationCenterContentProps {
projectName: string;
isSending: boolean;
inputPreview?: string;
/** The confirmed workspace name (null while name generation is in progress) */
workspaceName?: string | null;
}

/**
* Center content displayed during workspace creation
* Shows either a loading state with the user's prompt or welcome message
* Shows either a loading state with the workspace name or welcome message
*/
export function CreationCenterContent(props: CreationCenterContentProps) {
// Truncate long prompts for preview display
const truncatedPreview =
props.inputPreview && props.inputPreview.length > 150
? props.inputPreview.slice(0, 150) + "..."
: props.inputPreview;

return (
<div className="flex flex-1 items-center justify-center">
{props.isSending ? (
<div className="max-w-xl px-8 text-center">
<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>
<h2 className="text-foreground mb-2 text-lg font-medium">Creating workspace</h2>
{truncatedPreview && (
<p className="text-muted text-sm leading-relaxed">
Generating name for &ldquo;{truncatedPreview}&rdquo;
</p>
)}
<p className="text-muted text-sm leading-relaxed">
{props.workspaceName ? (
<>
<code className="bg-separator rounded px-1">{props.workspaceName}</code>
</>
) : (
"Generating name…"
)}
</p>
</div>
) : (
<div className="max-w-2xl px-8 text-center">
<h1 className="text-foreground mb-4 text-2xl font-semibold">{props.projectName}</h1>
<p className="text-muted text-sm leading-relaxed">
Describe what you want to build. A new workspace will be created with an automatically
generated branch name. Configure runtime and model options below.
Describe what you want to build and a workspace will be created.
</p>
</div>
)}
Expand Down
160 changes: 120 additions & 40 deletions src/browser/components/ChatInput/CreationControls.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import React from "react";
import React, { useCallback } from "react";
import { RUNTIME_MODE, type RuntimeMode } from "@/common/types/runtime";
import { Select } from "../Select";
import { RuntimeIconSelector } from "../RuntimeIconSelector";
import { Loader2, Wand2 } from "lucide-react";
import { cn } from "@/common/lib/utils";
import { Tooltip, TooltipWrapper } from "../Tooltip";
import type { WorkspaceNameState } from "@/browser/hooks/useWorkspaceName";

interface CreationControlsProps {
branches: string[];
Expand All @@ -10,68 +14,144 @@ interface CreationControlsProps {
runtimeMode: RuntimeMode;
defaultRuntimeMode: RuntimeMode;
sshHost: string;
/** Called when user clicks a runtime icon to select it (does not persist) */
onRuntimeModeChange: (mode: RuntimeMode) => void;
/** Called when user checks "Default for project" checkbox (persists) */
onSetDefaultRuntime: (mode: RuntimeMode) => void;
/** Called when user changes SSH host */
onSshHostChange: (host: string) => void;
disabled: boolean;
/** Workspace name generation state and actions */
nameState: WorkspaceNameState;
}

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

return (
<div className="flex flex-wrap items-center gap-x-3 gap-y-2">
{/* Runtime Selector - icon-based with tooltips */}
<RuntimeIconSelector
value={props.runtimeMode}
onChange={props.onRuntimeModeChange}
defaultMode={props.defaultRuntimeMode}
onSetDefault={props.onSetDefaultRuntime}
disabled={props.disabled}
/>
const { nameState } = props;

const handleNameChange = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
nameState.setName(e.target.value);
},
[nameState]
);

// Clicking into the input disables auto-generation so user can edit
const handleInputFocus = useCallback(() => {
if (nameState.autoGenerate) {
nameState.setAutoGenerate(false);
}
}, [nameState]);

// Toggle auto-generation via wand button
const handleWandClick = useCallback(() => {
nameState.setAutoGenerate(!nameState.autoGenerate);
}, [nameState]);

{/* Trunk Branch Selector - hidden for Local runtime */}
{showTrunkBranchSelector && (
<div
className="flex items-center gap-1"
data-component="TrunkBranchGroup"
data-tutorial="trunk-branch"
>
<label htmlFor="trunk-branch" className="text-muted text-xs">
From:
</label>
<Select
id="trunk-branch"
value={props.trunkBranch}
options={props.branches}
onChange={props.onTrunkBranchChange}
return (
<div className="flex flex-col gap-2">
{/* First row: Workspace name with magic wand toggle */}
<div className="flex items-center gap-2" data-component="WorkspaceNameGroup">
<label htmlFor="workspace-name" className="text-muted text-xs whitespace-nowrap">
Name:
</label>
<div className="relative max-w-xs flex-1">
<input
id="workspace-name"
type="text"
value={nameState.name}
onChange={handleNameChange}
onFocus={handleInputFocus}
placeholder={nameState.isGenerating ? "Generating..." : "workspace-name"}
disabled={props.disabled}
className="max-w-[120px]"
className={cn(
"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",
nameState.error && "border-red-500"
)}
/>
{/* Magic wand / loading indicator - vertically centered */}
<div className="absolute inset-y-0 right-0 flex items-center pr-1.5">
{nameState.isGenerating ? (
<Loader2 className="text-accent h-3.5 w-3.5 animate-spin" />
) : (
<TooltipWrapper inline>
<button
type="button"
onClick={handleWandClick}
disabled={props.disabled}
className="flex h-full items-center disabled:opacity-50"
aria-label={nameState.autoGenerate ? "Disable auto-naming" : "Enable auto-naming"}
>
<Wand2
className={cn(
"h-3.5 w-3.5 transition-colors",
nameState.autoGenerate
? "text-accent"
: "text-muted-foreground opacity-50 hover:opacity-75"
)}
/>
</button>
<Tooltip className="tooltip" align="center">
{nameState.autoGenerate ? "Auto-naming enabled" : "Click to enable auto-naming"}
</Tooltip>
</TooltipWrapper>
)}
</div>
</div>
)}
{/* Error display - inline */}
{nameState.error && <span className="text-xs text-red-500">{nameState.error}</span>}
</div>

{/* SSH Host Input - after From selector */}
{props.runtimeMode === RUNTIME_MODE.SSH && (
<input
type="text"
value={props.sshHost}
onChange={(e) => props.onSshHostChange(e.target.value)}
placeholder="user@host"
{/* Second row: Runtime, Branch, SSH */}
<div className="flex flex-wrap items-center gap-x-3 gap-y-2">
{/* Runtime Selector - icon-based with tooltips */}
<RuntimeIconSelector
value={props.runtimeMode}
onChange={props.onRuntimeModeChange}
defaultMode={props.defaultRuntimeMode}
onSetDefault={props.onSetDefaultRuntime}
disabled={props.disabled}
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"
/>
)}

{/* Trunk Branch Selector - hidden for Local runtime */}
{showTrunkBranchSelector && (
<div
className="flex h-6 items-center gap-1"
data-component="TrunkBranchGroup"
data-tutorial="trunk-branch"
>
<label htmlFor="trunk-branch" className="text-muted text-xs">
From:
</label>
<Select
id="trunk-branch"
value={props.trunkBranch}
options={props.branches}
onChange={props.onTrunkBranchChange}
disabled={props.disabled}
className="h-6 max-w-[120px]"
/>
</div>
)}

{/* SSH Host Input - after From selector */}
{props.runtimeMode === RUNTIME_MODE.SSH && (
<input
type="text"
value={props.sshHost}
onChange={(e) => props.onSshHostChange(e.target.value)}
placeholder="user@host"
disabled={props.disabled}
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"
/>
)}
</div>
</div>
);
}
9 changes: 7 additions & 2 deletions src/browser/components/ChatInput/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -244,12 +244,14 @@ export const ChatInput: React.FC<ChatInputProps> = (props) => {
? {
projectPath: props.projectPath,
onWorkspaceCreated: props.onWorkspaceCreated,
message: input,
}
: {
// Dummy values for workspace variant (never used)
projectPath: "",
// eslint-disable-next-line @typescript-eslint/no-empty-function
onWorkspaceCreated: () => {},
message: "",
}
);

Expand Down Expand Up @@ -1190,7 +1192,9 @@ export const ChatInput: React.FC<ChatInputProps> = (props) => {
<CreationCenterContent
projectName={props.projectName}
isSending={creationState.isSending || isSending}
inputPreview={creationState.isSending || isSending ? input : undefined}
workspaceName={
creationState.isSending || isSending ? creationState.creatingWithName : undefined
}
/>
)}

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

{/* Creation controls - second row for creation variant */}
{/* Creation controls - below model controls for creation variant */}
{variant === "creation" && (
<CreationControls
branches={creationState.branches}
Expand All @@ -1400,6 +1404,7 @@ export const ChatInput: React.FC<ChatInputProps> = (props) => {
onSetDefaultRuntime={creationState.setDefaultRuntimeMode}
onSshHostChange={creationState.setSshHost}
disabled={creationState.isSending || isSending}
nameState={creationState.nameState}
/>
)}
</div>
Expand Down
Loading