Skip to content

Commit 3643542

Browse files
authored
🤖 fix: preserve /new start message and focus creation input (#595)
## Summary - Preserve the user's start message (plus -t/-r flags) when `/new` launches the chat-based creation flow by reusing the parsed payload; added parser tests to cover whitespace and runtime cases. - Autofocus the creation chat input whenever the new workspace view opens and reuse a single computed `creationProjectPath` so the view only renders when a concrete project exists. ## Testing - bun test src/utils/slashCommands/parser.test.ts - bun test src/hooks/useStartWorkspaceCreation.test.ts - make static-check _Generated with `mux`_
1 parent 70f7fb6 commit 3643542

File tree

12 files changed

+379
-561
lines changed

12 files changed

+379
-561
lines changed

src/App.tsx

Lines changed: 32 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import { useApp } from "./contexts/AppContext";
44
import type { WorkspaceSelection } from "./components/ProjectSidebar";
55
import type { FrontendWorkspaceMetadata } from "./types/workspace";
66
import { LeftSidebar } from "./components/LeftSidebar";
7-
import NewWorkspaceModal from "./components/NewWorkspaceModal";
87
import { ProjectCreateModal } from "./components/ProjectCreateModal";
98
import { AIView } from "./components/AIView";
109
import { ErrorBoundary } from "./components/ErrorBoundary";
@@ -15,6 +14,7 @@ import { useUnreadTracking } from "./hooks/useUnreadTracking";
1514
import { useAutoCompactContinue } from "./hooks/useAutoCompactContinue";
1615
import { useWorkspaceStoreRaw, useWorkspaceRecency } from "./stores/WorkspaceStore";
1716
import { ChatInput } from "./components/ChatInput/index";
17+
import type { ChatInputAPI } from "./components/ChatInput/types";
1818

1919
import { useStableReference, compareMaps } from "./hooks/useStableReference";
2020
import { CommandRegistryProvider, useCommandRegistry } from "./contexts/CommandRegistryContext";
@@ -25,13 +25,12 @@ import { CommandPalette } from "./components/CommandPalette";
2525
import { buildCoreSources, type BuildSourcesParams } from "./utils/commands/sources";
2626

2727
import type { ThinkingLevel } from "./types/thinking";
28-
import type { RuntimeConfig } from "./types/runtime";
2928
import { CUSTOM_EVENTS } from "./constants/events";
3029
import { isWorkspaceForkSwitchEvent } from "./utils/workspaceFork";
31-
import { getThinkingLevelKey, getRuntimeKey } from "./constants/storage";
30+
import { getThinkingLevelKey } from "./constants/storage";
3231
import type { BranchListResult } from "./types/ipc";
3332
import { useTelemetry } from "./hooks/useTelemetry";
34-
import { parseRuntimeString } from "./utils/chatCommands";
33+
import { useStartWorkspaceCreation, getFirstProjectPath } from "./hooks/useStartWorkspaceCreation";
3534

3635
const THINKING_LEVELS: ThinkingLevel[] = ["off", "low", "medium", "high"];
3736

@@ -43,29 +42,40 @@ function AppInner() {
4342
removeProject,
4443
workspaceMetadata,
4544
setWorkspaceMetadata,
46-
createWorkspace,
4745
removeWorkspace,
4846
renameWorkspace,
4947
selectedWorkspace,
5048
setSelectedWorkspace,
5149
} = useApp();
5250
const [projectCreateModalOpen, setProjectCreateModalOpen] = useState(false);
53-
const [workspaceModalOpen, setWorkspaceModalOpen] = useState(false);
54-
const [workspaceModalProject, setWorkspaceModalProject] = useState<string | null>(null);
55-
const [workspaceModalProjectName, setWorkspaceModalProjectName] = useState<string>("");
56-
const [workspaceModalBranches, setWorkspaceModalBranches] = useState<string[]>([]);
57-
const [workspaceModalDefaultTrunk, setWorkspaceModalDefaultTrunk] = useState<string | undefined>(
58-
undefined
59-
);
60-
const [workspaceModalLoadError, setWorkspaceModalLoadError] = useState<string | null>(null);
61-
const workspaceModalProjectRef = useRef<string | null>(null);
6251

6352
// Track when we're in "new workspace creation" mode (show FirstMessageInput)
6453
const [pendingNewWorkspaceProject, setPendingNewWorkspaceProject] = useState<string | null>(null);
6554

6655
// Auto-collapse sidebar on mobile by default
6756
const isMobile = typeof window !== "undefined" && window.innerWidth <= 768;
6857
const [sidebarCollapsed, setSidebarCollapsed] = usePersistedState("sidebarCollapsed", isMobile);
58+
const defaultProjectPath = getFirstProjectPath(projects);
59+
const creationChatInputRef = useRef<ChatInputAPI | null>(null);
60+
const creationProjectPath = !selectedWorkspace
61+
? (pendingNewWorkspaceProject ?? (projects.size === 1 ? defaultProjectPath : null))
62+
: null;
63+
const handleCreationChatReady = useCallback((api: ChatInputAPI) => {
64+
creationChatInputRef.current = api;
65+
api.focus();
66+
}, []);
67+
68+
const startWorkspaceCreation = useStartWorkspaceCreation({
69+
projects,
70+
setPendingNewWorkspaceProject,
71+
setSelectedWorkspace,
72+
});
73+
74+
useEffect(() => {
75+
if (creationProjectPath) {
76+
creationChatInputRef.current?.focus();
77+
}
78+
}, [creationProjectPath]);
6979

7080
const handleToggleSidebar = useCallback(() => {
7181
setSidebarCollapsed((prev) => !prev);
@@ -133,7 +143,6 @@ function AppInner() {
133143
void window.api.window.setTitle("mux");
134144
}
135145
}, [selectedWorkspace, workspaceMetadata]);
136-
137146
// Validate selected workspace exists and has all required fields
138147
useEffect(() => {
139148
if (selectedWorkspace) {
@@ -177,12 +186,9 @@ function AppInner() {
177186

178187
const handleAddWorkspace = useCallback(
179188
(projectPath: string) => {
180-
// Show FirstMessageInput for this project
181-
setPendingNewWorkspaceProject(projectPath);
182-
// Clear any selected workspace so FirstMessageInput is shown
183-
setSelectedWorkspace(null);
189+
startWorkspaceCreation(projectPath);
184190
},
185-
[setSelectedWorkspace]
191+
[startWorkspaceCreation]
186192
);
187193

188194
// Memoize callbacks to prevent LeftSidebar/ProjectSidebar re-renders
@@ -204,48 +210,6 @@ function AppInner() {
204210
[handleRemoveProject]
205211
);
206212

207-
const handleCreateWorkspace = async (
208-
branchName: string,
209-
trunkBranch: string,
210-
runtime?: string
211-
) => {
212-
if (!workspaceModalProject) return;
213-
214-
console.assert(
215-
typeof trunkBranch === "string" && trunkBranch.trim().length > 0,
216-
"Expected trunk branch to be provided by the workspace modal"
217-
);
218-
219-
// Parse runtime config if provided
220-
let runtimeConfig: RuntimeConfig | undefined;
221-
if (runtime) {
222-
try {
223-
runtimeConfig = parseRuntimeString(runtime, branchName);
224-
} catch (err) {
225-
console.error("Failed to parse runtime config:", err);
226-
throw err; // Let modal handle the error
227-
}
228-
}
229-
230-
const newWorkspace = await createWorkspace(
231-
workspaceModalProject,
232-
branchName,
233-
trunkBranch,
234-
runtimeConfig
235-
);
236-
if (newWorkspace) {
237-
// Track workspace creation
238-
telemetry.workspaceCreated(newWorkspace.workspaceId);
239-
setSelectedWorkspace(newWorkspace);
240-
241-
// Save runtime preference for this project if provided
242-
if (runtime) {
243-
const runtimeKey = getRuntimeKey(workspaceModalProject);
244-
localStorage.setItem(runtimeKey, runtime);
245-
}
246-
}
247-
};
248-
249213
const handleGetSecrets = useCallback(async (projectPath: string) => {
250214
return await window.api.projects.secrets.get(projectPath);
251215
}, []);
@@ -398,9 +362,9 @@ function AppInner() {
398362

399363
const openNewWorkspaceFromPalette = useCallback(
400364
(projectPath: string) => {
401-
void handleAddWorkspace(projectPath);
365+
startWorkspaceCreation(projectPath);
402366
},
403-
[handleAddWorkspace]
367+
[startWorkspaceCreation]
404368
);
405369

406370
const getBranchesForProject = useCallback(
@@ -469,7 +433,7 @@ function AppInner() {
469433
selectedWorkspace,
470434
getThinkingLevel: getThinkingLevelForWorkspace,
471435
onSetThinkingLevel: setThinkingLevelFromPalette,
472-
onOpenNewWorkspaceModal: openNewWorkspaceFromPalette,
436+
onStartWorkspaceCreation: openNewWorkspaceFromPalette,
473437
getBranchesForProject,
474438
onSelectWorkspace: selectWorkspaceFromPalette,
475439
onRemoveWorkspace: removeWorkspaceFromPalette,
@@ -621,9 +585,9 @@ function AppInner() {
621585
}
622586
/>
623587
</ErrorBoundary>
624-
) : pendingNewWorkspaceProject || projects.size === 1 ? (
588+
) : creationProjectPath ? (
625589
(() => {
626-
const projectPath = pendingNewWorkspaceProject ?? Array.from(projects.keys())[0];
590+
const projectPath = creationProjectPath;
627591
const projectName =
628592
projectPath.split("/").pop() ?? projectPath.split("\\").pop() ?? "Project";
629593
return (
@@ -633,6 +597,7 @@ function AppInner() {
633597
variant="creation"
634598
projectPath={projectPath}
635599
projectName={projectName}
600+
onReady={handleCreationChatReady}
636601
onWorkspaceCreated={(metadata) => {
637602
// Add to workspace metadata map
638603
setWorkspaceMetadata((prev) => new Map(prev).set(metadata.id, metadata));
@@ -686,26 +651,6 @@ function AppInner() {
686651
workspaceId: selectedWorkspace?.workspaceId,
687652
})}
688653
/>
689-
{workspaceModalOpen && workspaceModalProject && (
690-
<NewWorkspaceModal
691-
isOpen={workspaceModalOpen}
692-
projectName={workspaceModalProjectName}
693-
projectPath={workspaceModalProject}
694-
branches={workspaceModalBranches}
695-
defaultTrunkBranch={workspaceModalDefaultTrunk}
696-
loadErrorMessage={workspaceModalLoadError}
697-
onClose={() => {
698-
workspaceModalProjectRef.current = null;
699-
setWorkspaceModalOpen(false);
700-
setWorkspaceModalProject(null);
701-
setWorkspaceModalProjectName("");
702-
setWorkspaceModalBranches([]);
703-
setWorkspaceModalDefaultTrunk(undefined);
704-
setWorkspaceModalLoadError(null);
705-
}}
706-
onAdd={handleCreateWorkspace}
707-
/>
708-
)}
709654
<ProjectCreateModal
710655
isOpen={projectCreateModalOpen}
711656
onClose={() => setProjectCreateModalOpen(false)}

src/components/NewWorkspaceModal.stories.tsx

Lines changed: 0 additions & 105 deletions
This file was deleted.

0 commit comments

Comments
 (0)