Skip to content

Commit 9731da3

Browse files
committed
🤖 refactor: unify ChatInput with variant pattern
- Merged FirstMessageInput into ChatInput using discriminated union - Created ChatInput/ directory with focused, modular components: - index.tsx: Main unified component - types.ts: Variant type definitions - useCreationWorkspace.ts: Creation state & logic hook - CreationCenterContent.tsx: Welcome screen component - CreationControls.tsx: Runtime & trunk branch selectors - utils.ts: Shared error message extraction - Eliminated logic duplication by reusing useSendMessageOptions - Single source of truth for chat input behavior - Strong typing prevents invalid prop combinations - Consistent UX between workspace and creation modes Net: -257 LOC (1273 → 1016), cleaner architecture
1 parent ab9ad36 commit 9731da3

File tree

9 files changed

+514
-459
lines changed

9 files changed

+514
-459
lines changed

src/App.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import { useResumeManager } from "./hooks/useResumeManager";
1414
import { useUnreadTracking } from "./hooks/useUnreadTracking";
1515
import { useAutoCompactContinue } from "./hooks/useAutoCompactContinue";
1616
import { useWorkspaceStoreRaw, useWorkspaceRecency } from "./stores/WorkspaceStore";
17-
import { FirstMessageInput } from "./components/FirstMessageInput";
17+
import { ChatInput } from "./components/ChatInput/index";
1818

1919
import { useStableReference, compareMaps } from "./hooks/useStableReference";
2020
import { CommandRegistryProvider, useCommandRegistry } from "./contexts/CommandRegistryContext";
@@ -630,7 +630,8 @@ function AppInner() {
630630
return (
631631
<ModeProvider projectPath={projectPath}>
632632
<ThinkingProvider projectPath={projectPath}>
633-
<FirstMessageInput
633+
<ChatInput
634+
variant="creation"
634635
projectPath={projectPath}
635636
projectName={projectName}
636637
onWorkspaceCreated={(metadata) => {

src/components/AIView.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { StreamingBarrier } from "./Messages/ChatBarrier/StreamingBarrier";
66
import { RetryBarrier } from "./Messages/ChatBarrier/RetryBarrier";
77
import { PinnedTodoList } from "./PinnedTodoList";
88
import { getAutoRetryKey, VIM_ENABLED_KEY } from "@/constants/storage";
9-
import { ChatInput, type ChatInputAPI } from "./ChatInput";
9+
import { ChatInput, type ChatInputAPI } from "./ChatInput/index";
1010
import { RightSidebar, type TabType } from "./RightSidebar";
1111
import { useResizableSidebar } from "@/hooks/useResizableSidebar";
1212
import {
@@ -462,6 +462,7 @@ const AIViewInner: React.FC<AIViewProps> = ({
462462
</div>
463463

464464
<ChatInput
465+
variant="workspace"
465466
workspaceId={workspaceId}
466467
onMessageSent={handleMessageSent}
467468
onTruncateHistory={handleClearHistory}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import React from "react";
2+
3+
interface CreationCenterContentProps {
4+
projectName: string;
5+
isSending: boolean;
6+
}
7+
8+
/**
9+
* Center content displayed during workspace creation
10+
* Shows either a loading spinner or welcome message
11+
*/
12+
export function CreationCenterContent({ projectName, isSending }: CreationCenterContentProps) {
13+
return (
14+
<div className="flex flex-1 items-center justify-center">
15+
{isSending ? (
16+
<div className="text-center">
17+
<div className="bg-accent mb-3 inline-block h-8 w-8 animate-spin rounded-full border-4 border-solid border-current border-r-transparent"></div>
18+
<p className="text-muted text-sm">Creating workspace...</p>
19+
</div>
20+
) : (
21+
<div className="max-w-2xl px-8 text-center">
22+
<h1 className="text-foreground mb-4 text-2xl font-semibold">{projectName}</h1>
23+
<p className="text-muted text-sm leading-relaxed">
24+
Describe what you want to build. A new workspace will be created with an automatically
25+
generated branch name. Configure runtime and model options below.
26+
</p>
27+
</div>
28+
)}
29+
</div>
30+
);
31+
}
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import React from "react";
2+
import { RUNTIME_MODE } from "@/types/runtime";
3+
import { TooltipWrapper, Tooltip } from "../Tooltip";
4+
5+
interface CreationControlsProps {
6+
branches: string[];
7+
trunkBranch: string;
8+
onTrunkBranchChange: (branch: string) => void;
9+
runtimeMode: typeof RUNTIME_MODE.LOCAL | typeof RUNTIME_MODE.SSH;
10+
sshHost: string;
11+
onRuntimeChange: (
12+
mode: typeof RUNTIME_MODE.LOCAL | typeof RUNTIME_MODE.SSH,
13+
host: string
14+
) => void;
15+
disabled: boolean;
16+
}
17+
18+
/**
19+
* Additional controls shown only during workspace creation
20+
* - Trunk branch selector (which branch to fork from)
21+
* - Runtime mode (local vs SSH)
22+
*/
23+
export function CreationControls({
24+
branches,
25+
trunkBranch,
26+
onTrunkBranchChange,
27+
runtimeMode,
28+
sshHost,
29+
onRuntimeChange,
30+
disabled,
31+
}: CreationControlsProps) {
32+
return (
33+
<div className="flex flex-wrap items-center gap-x-3 gap-y-2">
34+
{/* Trunk Branch Selector */}
35+
{branches.length > 0 && (
36+
<div className="flex items-center gap-1" data-component="TrunkBranchGroup">
37+
<label htmlFor="trunk-branch" className="text-muted text-xs">
38+
From:
39+
</label>
40+
<select
41+
id="trunk-branch"
42+
value={trunkBranch}
43+
onChange={(e) => onTrunkBranchChange(e.target.value)}
44+
disabled={disabled}
45+
className="bg-separator text-foreground border-border-medium focus:border-accent max-w-[120px] rounded border px-2 py-1 text-xs focus:outline-none disabled:opacity-50"
46+
>
47+
{branches.map((branch) => (
48+
<option key={branch} value={branch}>
49+
{branch}
50+
</option>
51+
))}
52+
</select>
53+
</div>
54+
)}
55+
56+
{/* Runtime Selector */}
57+
<div className="flex items-center gap-1" data-component="RuntimeSelectorGroup">
58+
<label className="text-muted text-xs">Runtime:</label>
59+
<select
60+
value={runtimeMode}
61+
onChange={(e) => {
62+
const newMode = e.target.value as
63+
| typeof RUNTIME_MODE.LOCAL
64+
| typeof RUNTIME_MODE.SSH;
65+
onRuntimeChange(newMode, newMode === RUNTIME_MODE.LOCAL ? "" : sshHost);
66+
}}
67+
disabled={disabled}
68+
className="bg-separator text-foreground border-border-medium focus:border-accent rounded border px-2 py-1 text-xs focus:outline-none disabled:opacity-50"
69+
>
70+
<option value={RUNTIME_MODE.LOCAL}>Local</option>
71+
<option value={RUNTIME_MODE.SSH}>SSH Remote</option>
72+
</select>
73+
{runtimeMode === RUNTIME_MODE.SSH && (
74+
<input
75+
type="text"
76+
value={sshHost}
77+
onChange={(e) => onRuntimeChange(RUNTIME_MODE.SSH, e.target.value)}
78+
placeholder="user@host"
79+
disabled={disabled}
80+
className="bg-separator text-foreground border-border-medium focus:border-accent w-32 rounded border px-2 py-1 text-xs focus:outline-none disabled:opacity-50"
81+
/>
82+
)}
83+
<TooltipWrapper inline>
84+
<span className="text-muted cursor-help text-xs">?</span>
85+
<Tooltip className="tooltip" align="center" width="wide">
86+
<strong>Runtime:</strong>
87+
<br />
88+
• Local: git worktree in ~/.cmux/src
89+
<br />• SSH: remote clone in ~/cmux on SSH host
90+
</Tooltip>
91+
</TooltipWrapper>
92+
</div>
93+
</div>
94+
);
95+
}

0 commit comments

Comments
 (0)