Skip to content

Commit 43bce08

Browse files
authored
🤖 feat: AI-generated workspace creation on first message (#500)
Changes the "New Workspace" modal to instead be inline in the chat form. https://github.com/user-attachments/assets/a8c36701-5507-4c53-b9c9-4afbda43cbe8
1 parent a8b86e9 commit 43bce08

File tree

13 files changed

+923
-134
lines changed

13 files changed

+923
-134
lines changed

src/App.tsx

Lines changed: 60 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,13 @@ 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";
1718

1819
import { useStableReference, compareMaps } from "./hooks/useStableReference";
1920
import { CommandRegistryProvider, useCommandRegistry } from "./contexts/CommandRegistryContext";
2021
import type { CommandAction } from "./contexts/CommandRegistryContext";
22+
import { ModeProvider } from "./contexts/ModeContext";
23+
import { ThinkingProvider } from "./contexts/ThinkingContext";
2124
import { CommandPalette } from "./components/CommandPalette";
2225
import { buildCoreSources, type BuildSourcesParams } from "./utils/commands/sources";
2326

@@ -57,6 +60,9 @@ function AppInner() {
5760
const [workspaceModalLoadError, setWorkspaceModalLoadError] = useState<string | null>(null);
5861
const workspaceModalProjectRef = useRef<string | null>(null);
5962

63+
// Track when we're in "new workspace creation" mode (show FirstMessageInput)
64+
const [pendingNewWorkspaceProject, setPendingNewWorkspaceProject] = useState<string | null>(null);
65+
6066
// Auto-collapse sidebar on mobile by default
6167
const isMobile = typeof window !== "undefined" && window.innerWidth <= 768;
6268
const [sidebarCollapsed, setSidebarCollapsed] = usePersistedState("sidebarCollapsed", isMobile);
@@ -114,9 +120,10 @@ function AppInner() {
114120
window.history.replaceState(null, "", newHash);
115121
}
116122

117-
// Update window title with workspace name
123+
// Update window title with workspace name (prefer displayName if available)
124+
const metadata = workspaceMetadata.get(selectedWorkspace.workspaceId);
118125
const workspaceName =
119-
workspaceMetadata.get(selectedWorkspace.workspaceId)?.name ?? selectedWorkspace.workspaceId;
126+
metadata?.displayName ?? metadata?.name ?? selectedWorkspace.workspaceId;
120127
const title = `${workspaceName} - ${selectedWorkspace.projectName} - cmux`;
121128
void window.api.window.setTitle(title);
122129
} else {
@@ -169,46 +176,15 @@ function AppInner() {
169176
[removeProject, selectedWorkspace, setSelectedWorkspace]
170177
);
171178

172-
const handleAddWorkspace = useCallback(async (projectPath: string) => {
173-
const projectName = projectPath.split("/").pop() ?? projectPath.split("\\").pop() ?? "project";
174-
175-
workspaceModalProjectRef.current = projectPath;
176-
setWorkspaceModalProject(projectPath);
177-
setWorkspaceModalProjectName(projectName);
178-
setWorkspaceModalBranches([]);
179-
setWorkspaceModalDefaultTrunk(undefined);
180-
setWorkspaceModalLoadError(null);
181-
setWorkspaceModalOpen(true);
182-
183-
try {
184-
const branchResult = await window.api.projects.listBranches(projectPath);
185-
186-
// Guard against race condition: only update state if this is still the active project
187-
if (workspaceModalProjectRef.current !== projectPath) {
188-
return;
189-
}
190-
191-
const sanitizedBranches = Array.isArray(branchResult?.branches)
192-
? branchResult.branches.filter((branch): branch is string => typeof branch === "string")
193-
: [];
194-
195-
const recommended =
196-
typeof branchResult?.recommendedTrunk === "string" &&
197-
sanitizedBranches.includes(branchResult.recommendedTrunk)
198-
? branchResult.recommendedTrunk
199-
: sanitizedBranches[0];
200-
201-
setWorkspaceModalBranches(sanitizedBranches);
202-
setWorkspaceModalDefaultTrunk(recommended);
203-
setWorkspaceModalLoadError(null);
204-
} catch (err) {
205-
console.error("Failed to load branches for modal:", err);
206-
const message = err instanceof Error ? err.message : "Unknown error";
207-
setWorkspaceModalLoadError(
208-
`Unable to load branches automatically: ${message}. You can still enter the trunk branch manually.`
209-
);
210-
}
211-
}, []);
179+
const handleAddWorkspace = useCallback(
180+
(projectPath: string) => {
181+
// Show FirstMessageInput for this project
182+
setPendingNewWorkspaceProject(projectPath);
183+
// Clear any selected workspace so FirstMessageInput is shown
184+
setSelectedWorkspace(null);
185+
},
186+
[setSelectedWorkspace]
187+
);
212188

213189
// Memoize callbacks to prevent LeftSidebar/ProjectSidebar re-renders
214190
const handleAddProjectCallback = useCallback(() => {
@@ -646,6 +622,48 @@ function AppInner() {
646622
}
647623
/>
648624
</ErrorBoundary>
625+
) : pendingNewWorkspaceProject || projects.size === 1 ? (
626+
(() => {
627+
const projectPath = pendingNewWorkspaceProject ?? Array.from(projects.keys())[0];
628+
const projectName =
629+
projectPath.split("/").pop() ?? projectPath.split("\\").pop() ?? "Project";
630+
return (
631+
<ModeProvider projectPath={projectPath}>
632+
<ThinkingProvider projectPath={projectPath}>
633+
<FirstMessageInput
634+
projectPath={projectPath}
635+
projectName={projectName}
636+
onWorkspaceCreated={(metadata) => {
637+
// Add to workspace metadata map
638+
setWorkspaceMetadata((prev) => new Map(prev).set(metadata.id, metadata));
639+
640+
// Switch to new workspace
641+
handleWorkspaceSwitch({
642+
workspaceId: metadata.id,
643+
projectPath: metadata.projectPath,
644+
projectName: metadata.projectName,
645+
namedWorkspacePath: metadata.namedWorkspacePath,
646+
});
647+
648+
// Track telemetry
649+
telemetry.workspaceCreated(metadata.id);
650+
651+
// Clear pending state
652+
setPendingNewWorkspaceProject(null);
653+
}}
654+
onCancel={
655+
pendingNewWorkspaceProject
656+
? () => {
657+
// User cancelled workspace creation - clear pending state
658+
setPendingNewWorkspaceProject(null);
659+
}
660+
: undefined
661+
}
662+
/>
663+
</ThinkingProvider>
664+
</ModeProvider>
665+
);
666+
})()
649667
) : (
650668
<div
651669
className="[&_p]:text-muted mx-auto w-full max-w-3xl text-center [&_h2]:mb-4 [&_h2]:font-bold [&_h2]:tracking-tight [&_h2]:text-white [&_p]:leading-[1.6]"

src/components/ChatInput.tsx

Lines changed: 2 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import React, {
88
useMemo,
99
useDeferredValue,
1010
} from "react";
11-
import { cn } from "@/lib/utils";
1211
import { CommandSuggestions, COMMAND_SUGGESTION_KEYS } from "./CommandSuggestions";
1312
import type { Toast } from "./ChatInputToast";
1413
import { ChatInputToast } from "./ChatInputToast";
@@ -27,14 +26,13 @@ import {
2726
prepareCompactionMessage,
2827
type CommandHandlerContext,
2928
} from "@/utils/chatCommands";
30-
import { ToggleGroup, type ToggleOption } from "./ToggleGroup";
3129
import { CUSTOM_EVENTS } from "@/constants/events";
32-
import type { UIMode } from "@/types/mode";
3330
import {
3431
getSlashCommandSuggestions,
3532
type SlashSuggestion,
3633
} from "@/utils/slashCommands/suggestions";
3734
import { TooltipWrapper, Tooltip, HelpIndicator } from "./Tooltip";
35+
import { ModeSelector } from "./ModeSelector";
3836
import { matchesKeybind, formatKeybind, KEYBINDS, isEditableElement } from "@/utils/ui/keybinds";
3937
import { ModelSelector, type ModelSelectorRef } from "./ModelSelector";
4038
import { useModelLRU } from "@/hooks/useModelLRU";
@@ -88,25 +86,6 @@ export interface ChatInputAPI {
8886
appendText: (text: string) => void;
8987
}
9088

91-
const MODE_OPTIONS: Array<ToggleOption<UIMode>> = [
92-
{ value: "exec", label: "Exec", activeClassName: "bg-exec-mode text-white" },
93-
{ value: "plan", label: "Plan", activeClassName: "bg-plan-mode text-white" },
94-
];
95-
96-
const ModeHelpTooltip: React.FC = () => (
97-
<TooltipWrapper inline>
98-
<HelpIndicator>?</HelpIndicator>
99-
<Tooltip className="tooltip" align="center" width="wide">
100-
<strong>Exec Mode:</strong> AI edits files and execute commands
101-
<br />
102-
<br />
103-
<strong>Plan Mode:</strong> AI proposes plans but does not edit files
104-
<br />
105-
<br />
106-
Toggle with: {formatKeybind(KEYBINDS.TOGGLE_MODE)}
107-
</Tooltip>
108-
</TooltipWrapper>
109-
);
11089
export interface ChatInputProps {
11190
workspaceId: string;
11291
onMessageSent?: () => void; // Optional callback after successful send
@@ -902,28 +881,7 @@ export const ChatInput: React.FC<ChatInputProps> = ({
902881
</div>
903882
)}
904883

905-
{/* Mode Switch - full version for wide containers */}
906-
<div className="ml-auto flex items-center gap-1.5 [@container(max-width:550px)]:hidden">
907-
<div
908-
className={cn(
909-
"flex gap-0 bg-toggle-bg rounded",
910-
"[&>button:first-of-type]:rounded-l [&>button:last-of-type]:rounded-r",
911-
mode === "exec" &&
912-
"[&>button:first-of-type]:bg-exec-mode [&>button:first-of-type]:text-white [&>button:first-of-type]:hover:bg-exec-mode-hover",
913-
mode === "plan" &&
914-
"[&>button:last-of-type]:bg-plan-mode [&>button:last-of-type]:text-white [&>button:last-of-type]:hover:bg-plan-mode-hover"
915-
)}
916-
>
917-
<ToggleGroup<UIMode> options={MODE_OPTIONS} value={mode} onChange={setMode} />
918-
</div>
919-
<ModeHelpTooltip />
920-
</div>
921-
922-
{/* Mode Switch - compact version for narrow containers */}
923-
<div className="ml-auto hidden items-center gap-1.5 [@container(max-width:550px)]:flex">
924-
<ToggleGroup<UIMode> options={MODE_OPTIONS} value={mode} onChange={setMode} compact />
925-
<ModeHelpTooltip />
926-
</div>
884+
<ModeSelector mode={mode} onChange={setMode} className="ml-auto" />
927885
</div>
928886
</div>
929887
</div>

0 commit comments

Comments
 (0)