Skip to content

Commit 3277ee8

Browse files
ammar-agentroot
andauthored
🤖 refactor: simplify workspace creation and title generation (#578)
Simplifies workspace creation flow by removing the displayName concept and centralizing draft workspace settings. **Key changes:** - **Single workspace name** - Removed displayName/name duality. Workspace now has one git-safe name used everywhere (e.g., "feature-authentication"). Simpler mental model, less duplication. - **Centralized draft settings** - Created `useDraftWorkspaceSettings` hook that persists runtime mode, SSH host, and trunk branch when navigating away from draft workspace. Settings restore when returning to same project. - **Auto-focus fix** - Fixed bug where ChatInput stole focus during streaming (introduced in 9731da3). Effect now only triggers on workspace changes, not on every streaming update. - **Storage scope helpers** - Centralized scope ID generation (`getProjectScopeId`, `getPendingScopeId`, `GLOBAL_SCOPE_ID`) to eliminate duplicate patterns across files. **Backwards compatibility:** Schema still accepts displayName during load (ignored). No migration needed. _Generated with `mux`_ --------- Co-authored-by: root <root@ovh-1.tailc2a514.ts.net>
1 parent c817e7d commit 3277ee8

24 files changed

+930
-608
lines changed

docs/AGENTS.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
# AGENT INSTRUCTIONS
22

3+
**Edits to this file must be minimal and token-efficient.** Think carefully about how to represent information concisely. Avoid redundant examples or verbose explanations when the knowledge can be conveyed in a sentence or two.
4+
35
## Project Context
46

57
- Project is named `mux`
@@ -365,6 +367,27 @@ If IPC is hard to test, fix the test infrastructure or IPC layer, don't work aro
365367

366368
**For per-operation state tied to async workflows, parent components should own all localStorage operations.** Child components should notify parents of user intent without manipulating storage directly, preventing bugs from stale or orphaned state across component lifecycles.
367369

370+
**Always use persistedState helpers (`usePersistedState`, `readPersistedState`, `updatePersistedState`) instead of direct `localStorage` calls** - provides cross-component sync and consistent error handling.
371+
372+
**Avoid destructuring props in function signatures** - Use `props.fieldName` instead of destructuring in the parameter list. Destructuring duplicates field names and makes refactoring more cumbersome.
373+
374+
```typescript
375+
// ❌ BAD - Duplicates field names, harder to refactor
376+
export function MyComponent({
377+
field1,
378+
field2,
379+
field3,
380+
onAction,
381+
}: MyComponentProps) {
382+
return <div onClick={onAction}>{field1}</div>;
383+
}
384+
385+
// ✅ GOOD - Single source of truth, easier to refactor
386+
export function MyComponent(props: MyComponentProps) {
387+
return <div onClick={props.onAction}>{props.field1}</div>;
388+
}
389+
```
390+
368391
## Module Imports
369392

370393
- **NEVER use dynamic imports** - Always use static `import` statements at the top of files. Dynamic imports (`await import()`) are a code smell that indicates improper module structure.

src/App.tsx

Lines changed: 5 additions & 5 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";
@@ -120,10 +120,9 @@ function AppInner() {
120120
window.history.replaceState(null, "", newHash);
121121
}
122122

123-
// Update window title with workspace name (prefer displayName if available)
123+
// Update window title with workspace name
124124
const metadata = workspaceMetadata.get(selectedWorkspace.workspaceId);
125-
const workspaceName =
126-
metadata?.displayName ?? metadata?.name ?? selectedWorkspace.workspaceId;
125+
const workspaceName = metadata?.name ?? selectedWorkspace.workspaceId;
127126
const title = `${workspaceName} - ${selectedWorkspace.projectName} - cmux`;
128127
void window.api.window.setTitle(title);
129128
} else {
@@ -630,7 +629,8 @@ function AppInner() {
630629
return (
631630
<ModeProvider projectPath={projectPath}>
632631
<ThinkingProvider projectPath={projectPath}>
633-
<FirstMessageInput
632+
<ChatInput
633+
variant="creation"
634634
projectPath={projectPath}
635635
projectName={projectName}
636636
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: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import React from "react";
2+
import { RUNTIME_MODE, type RuntimeMode } from "@/types/runtime";
3+
import { TooltipWrapper, Tooltip } from "../Tooltip";
4+
import { Select } from "../Select";
5+
6+
interface CreationControlsProps {
7+
branches: string[];
8+
trunkBranch: string;
9+
onTrunkBranchChange: (branch: string) => void;
10+
runtimeMode: RuntimeMode;
11+
sshHost: string;
12+
onRuntimeChange: (mode: RuntimeMode, host: string) => void;
13+
disabled: boolean;
14+
}
15+
16+
/**
17+
* Additional controls shown only during workspace creation
18+
* - Trunk branch selector (which branch to fork from)
19+
* - Runtime mode (local vs SSH)
20+
*/
21+
export function CreationControls(props: CreationControlsProps) {
22+
return (
23+
<div className="flex flex-wrap items-center gap-x-3 gap-y-2">
24+
{/* Trunk Branch Selector */}
25+
{props.branches.length > 0 && (
26+
<div className="flex items-center gap-1" data-component="TrunkBranchGroup">
27+
<label htmlFor="trunk-branch" className="text-muted text-xs">
28+
From:
29+
</label>
30+
<Select
31+
id="trunk-branch"
32+
value={props.trunkBranch}
33+
options={props.branches}
34+
onChange={props.onTrunkBranchChange}
35+
disabled={props.disabled}
36+
className="max-w-[120px]"
37+
/>
38+
</div>
39+
)}
40+
41+
{/* Runtime Selector */}
42+
<div className="flex items-center gap-1" data-component="RuntimeSelectorGroup">
43+
<label className="text-muted text-xs">Runtime:</label>
44+
<Select
45+
value={props.runtimeMode}
46+
options={[
47+
{ value: RUNTIME_MODE.LOCAL, label: "Local" },
48+
{ value: RUNTIME_MODE.SSH, label: "SSH" },
49+
]}
50+
onChange={(newMode) => {
51+
const mode = newMode as RuntimeMode;
52+
props.onRuntimeChange(mode, mode === RUNTIME_MODE.LOCAL ? "" : props.sshHost);
53+
}}
54+
disabled={props.disabled}
55+
aria-label="Runtime mode"
56+
/>
57+
{props.runtimeMode === RUNTIME_MODE.SSH && (
58+
<input
59+
type="text"
60+
value={props.sshHost}
61+
onChange={(e) => props.onRuntimeChange(RUNTIME_MODE.SSH, e.target.value)}
62+
placeholder="user@host"
63+
disabled={props.disabled}
64+
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"
65+
/>
66+
)}
67+
<TooltipWrapper inline>
68+
<span className="text-muted cursor-help text-xs">?</span>
69+
<Tooltip className="tooltip" align="center" width="wide">
70+
<strong>Runtime:</strong>
71+
<br />
72+
• Local: git worktree in ~/.cmux/src
73+
<br />• SSH: remote clone in ~/cmux on SSH host
74+
</Tooltip>
75+
</TooltipWrapper>
76+
</div>
77+
</div>
78+
);
79+
}

0 commit comments

Comments
 (0)