From 7b6e3d8d6424b19443a28ed46770eaec60c8cfef Mon Sep 17 00:00:00 2001 From: Ammar Date: Mon, 27 Oct 2025 20:41:55 +0000 Subject: [PATCH 01/10] =?UTF-8?q?=F0=9F=A4=96=20feat:=20add=20-m=20flag=20?= =?UTF-8?q?to=20/new,=20forward=20start=20message=20to=20modal=20on=20erro?= =?UTF-8?q?r?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Support /new -m to set initial model (like /compact) - Forward start message and model to modal when errors occur - Add textarea in NewWorkspaceModal for start message display - Model selector defaults to provided model or workspace default - Send start message with specified model after workspace creation Fixes three bugs: 1. Missing -m flag support for model selection 2. Lost start message on errors (now preserved in modal) 3. No way to see/edit start message in error modal All tests pass (16 /new command tests + 54 other slash command tests). --- src/App.tsx | 135 ++++++++++++++++++++------- src/components/NewWorkspaceModal.tsx | 41 +++++++- src/constants/events.ts | 6 ++ src/utils/chatCommands.ts | 57 +++++++---- src/utils/slashCommands/new.test.ts | 66 +++++++++++++ src/utils/slashCommands/registry.ts | 19 +++- src/utils/slashCommands/types.ts | 1 + 7 files changed, 264 insertions(+), 61 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index dbed6f47f..b59b6c8ee 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -54,6 +54,10 @@ function AppInner() { undefined ); const [workspaceModalLoadError, setWorkspaceModalLoadError] = useState(null); + const [workspaceModalStartMessage, setWorkspaceModalStartMessage] = useState( + undefined + ); + const [workspaceModalModel, setWorkspaceModalModel] = useState(undefined); const workspaceModalProjectRef = useRef(null); // Auto-collapse sidebar on mobile by default @@ -175,46 +179,55 @@ function AppInner() { [removeProject, selectedWorkspace, setSelectedWorkspace] ); - const handleAddWorkspace = useCallback(async (projectPath: string) => { - const projectName = projectPath.split("/").pop() ?? projectPath.split("\\").pop() ?? "project"; + const handleAddWorkspace = useCallback( + async ( + projectPath: string, + initialData?: { startMessage?: string; model?: string; error?: string } + ) => { + const projectName = + projectPath.split("/").pop() ?? projectPath.split("\\").pop() ?? "project"; + + workspaceModalProjectRef.current = projectPath; + setWorkspaceModalProject(projectPath); + setWorkspaceModalProjectName(projectName); + setWorkspaceModalBranches([]); + setWorkspaceModalDefaultTrunk(undefined); + setWorkspaceModalLoadError(initialData?.error ?? null); + setWorkspaceModalStartMessage(initialData?.startMessage); + setWorkspaceModalModel(initialData?.model); + setWorkspaceModalOpen(true); - workspaceModalProjectRef.current = projectPath; - setWorkspaceModalProject(projectPath); - setWorkspaceModalProjectName(projectName); - setWorkspaceModalBranches([]); - setWorkspaceModalDefaultTrunk(undefined); - setWorkspaceModalLoadError(null); - setWorkspaceModalOpen(true); + try { + const branchResult = await window.api.projects.listBranches(projectPath); - try { - const branchResult = await window.api.projects.listBranches(projectPath); + // Guard against race condition: only update state if this is still the active project + if (workspaceModalProjectRef.current !== projectPath) { + return; + } - // Guard against race condition: only update state if this is still the active project - if (workspaceModalProjectRef.current !== projectPath) { - return; - } + const sanitizedBranches = Array.isArray(branchResult?.branches) + ? branchResult.branches.filter((branch): branch is string => typeof branch === "string") + : []; - const sanitizedBranches = Array.isArray(branchResult?.branches) - ? branchResult.branches.filter((branch): branch is string => typeof branch === "string") - : []; + const recommended = + typeof branchResult?.recommendedTrunk === "string" && + sanitizedBranches.includes(branchResult.recommendedTrunk) + ? branchResult.recommendedTrunk + : sanitizedBranches[0]; - const recommended = - typeof branchResult?.recommendedTrunk === "string" && - sanitizedBranches.includes(branchResult.recommendedTrunk) - ? branchResult.recommendedTrunk - : sanitizedBranches[0]; - - setWorkspaceModalBranches(sanitizedBranches); - setWorkspaceModalDefaultTrunk(recommended); - setWorkspaceModalLoadError(null); - } catch (err) { - console.error("Failed to load branches for modal:", err); - const message = err instanceof Error ? err.message : "Unknown error"; - setWorkspaceModalLoadError( - `Unable to load branches automatically: ${message}. You can still enter the trunk branch manually.` - ); - } - }, []); + setWorkspaceModalBranches(sanitizedBranches); + setWorkspaceModalDefaultTrunk(recommended); + setWorkspaceModalLoadError(null); + } catch (err) { + console.error("Failed to load branches for modal:", err); + const message = err instanceof Error ? err.message : "Unknown error"; + setWorkspaceModalLoadError( + `Unable to load branches automatically: ${message}. You can still enter the trunk branch manually.` + ); + } + }, + [] + ); // Memoize callbacks to prevent LeftSidebar/ProjectSidebar re-renders const handleAddProjectCallback = useCallback(() => { @@ -238,7 +251,9 @@ function AppInner() { const handleCreateWorkspace = async ( branchName: string, trunkBranch: string, - runtime?: string + runtime?: string, + startMessage?: string, + model?: string ) => { if (!workspaceModalProject) return; @@ -274,6 +289,26 @@ function AppInner() { const runtimeKey = getRuntimeKey(workspaceModalProject); localStorage.setItem(runtimeKey, runtime); } + + // Send start message if provided + if (startMessage) { + // Build send message options - use provided model or default + const { buildSendMessageOptions } = await import("@/hooks/useSendMessageOptions"); + const sendOptions = buildSendMessageOptions(newWorkspace.workspaceId); + + if (model) { + sendOptions.model = model; + } + + // Defer until React finishes rendering and WorkspaceStore subscribes + requestAnimationFrame(() => { + void window.api.workspace.sendMessage( + newWorkspace.workspaceId, + startMessage, + sendOptions + ); + }); + } } }; @@ -615,6 +650,30 @@ function AppInner() { ); }, [projects, setSelectedWorkspace, setWorkspaceMetadata]); + // Handle open new workspace modal event + useEffect(() => { + const handleOpenNewWorkspaceModal = (e: Event) => { + const customEvent = e as CustomEvent<{ + projectPath: string; + startMessage?: string; + model?: string; + error?: string; + }>; + const { projectPath, startMessage, model, error } = customEvent.detail; + void handleAddWorkspace(projectPath, { startMessage, model, error }); + }; + + window.addEventListener( + CUSTOM_EVENTS.OPEN_NEW_WORKSPACE_MODAL, + handleOpenNewWorkspaceModal as EventListener + ); + return () => + window.removeEventListener( + CUSTOM_EVENTS.OPEN_NEW_WORKSPACE_MODAL, + handleOpenNewWorkspaceModal as EventListener + ); + }, [handleAddWorkspace]); + return ( <>
@@ -682,6 +741,8 @@ function AppInner() { branches={workspaceModalBranches} defaultTrunkBranch={workspaceModalDefaultTrunk} loadErrorMessage={workspaceModalLoadError} + initialStartMessage={workspaceModalStartMessage} + initialModel={workspaceModalModel} onClose={() => { workspaceModalProjectRef.current = null; setWorkspaceModalOpen(false); @@ -690,6 +751,8 @@ function AppInner() { setWorkspaceModalBranches([]); setWorkspaceModalDefaultTrunk(undefined); setWorkspaceModalLoadError(null); + setWorkspaceModalStartMessage(undefined); + setWorkspaceModalModel(undefined); }} onAdd={handleCreateWorkspace} /> diff --git a/src/components/NewWorkspaceModal.tsx b/src/components/NewWorkspaceModal.tsx index 9b1d7e51f..047384d5a 100644 --- a/src/components/NewWorkspaceModal.tsx +++ b/src/components/NewWorkspaceModal.tsx @@ -12,8 +12,16 @@ interface NewWorkspaceModalProps { branches: string[]; defaultTrunkBranch?: string; loadErrorMessage?: string | null; + initialStartMessage?: string; + initialModel?: string; onClose: () => void; - onAdd: (branchName: string, trunkBranch: string, runtime?: string) => Promise; + onAdd: ( + branchName: string, + trunkBranch: string, + runtime?: string, + startMessage?: string, + model?: string + ) => Promise; } // Shared form field styles @@ -27,11 +35,14 @@ const NewWorkspaceModal: React.FC = ({ branches, defaultTrunkBranch, loadErrorMessage, + initialStartMessage, + initialModel, onClose, onAdd, }) => { const [branchName, setBranchName] = useState(""); const [trunkBranch, setTrunkBranch] = useState(defaultTrunkBranch ?? branches[0] ?? ""); + const [startMessage, setStartMessage] = useState(initialStartMessage ?? ""); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); const infoId = useId(); @@ -45,6 +56,10 @@ const NewWorkspaceModal: React.FC = ({ setError(loadErrorMessage ?? null); }, [loadErrorMessage]); + useEffect(() => { + setStartMessage(initialStartMessage ?? ""); + }, [initialStartMessage]); + useEffect(() => { const fallbackTrunk = defaultTrunkBranch ?? branches[0] ?? ""; setTrunkBranch((current) => { @@ -66,6 +81,7 @@ const NewWorkspaceModal: React.FC = ({ setBranchName(""); setTrunkBranch(defaultTrunkBranch ?? branches[0] ?? ""); setRuntimeOptions(RUNTIME_MODE.LOCAL, ""); + setStartMessage(""); setError(loadErrorMessage ?? null); onClose(); }; @@ -104,11 +120,19 @@ const NewWorkspaceModal: React.FC = ({ try { // Get runtime string from hook helper const runtime = getRuntimeString(); + const trimmedStartMessage = startMessage.trim(); - await onAdd(trimmedBranchName, normalizedTrunkBranch, runtime); + await onAdd( + trimmedBranchName, + normalizedTrunkBranch, + runtime, + trimmedStartMessage || undefined, + initialModel + ); setBranchName(""); setTrunkBranch(defaultTrunkBranch ?? branches[0] ?? ""); setRuntimeOptions(RUNTIME_MODE.LOCAL, ""); + setStartMessage(""); onClose(); } catch (err) { const message = err instanceof Error ? err.message : "Failed to create workspace"; @@ -202,6 +226,7 @@ const NewWorkspaceModal: React.FC = ({ )}
+<<<<<<< HEAD