From ee7e9e204c45ddbda2a47a6c1f377f4a4bc6685e Mon Sep 17 00:00:00 2001 From: Ammar Date: Sat, 20 Dec 2025 19:50:50 -0600 Subject: [PATCH 1/4] feat: persist all draft settings on New Workspace page All settings changed on the New Workspace page now persist as part of the draft until the message is sent: - Runtime mode selection (worktree/local/SSH) - Workspace name (generated or manual) - Auto-generate toggle state Added clearCreationDraftStorage() to centralize clearing draft state when workspace creation succeeds. Settings that already persisted (model, mode, thinking level, input text, images) continue to work as before. --- .../ChatInput/useCreationWorkspace.ts | 12 ++-- .../hooks/useDraftWorkspaceSettings.ts | 30 ++++---- src/browser/hooks/useWorkspaceName.ts | 71 ++++++++++++------- src/common/constants/storage.ts | 56 +++++++++++++++ 4 files changed, 122 insertions(+), 47 deletions(-) diff --git a/src/browser/components/ChatInput/useCreationWorkspace.ts b/src/browser/components/ChatInput/useCreationWorkspace.ts index 0d86fa700d..08bd088f72 100644 --- a/src/browser/components/ChatInput/useCreationWorkspace.ts +++ b/src/browser/components/ChatInput/useCreationWorkspace.ts @@ -8,12 +8,10 @@ import { useDraftWorkspaceSettings } from "@/browser/hooks/useDraftWorkspaceSett import { readPersistedState, updatePersistedState } from "@/browser/hooks/usePersistedState"; import { getSendOptionsFromStorage } from "@/browser/utils/messages/sendOptions"; import { - getInputKey, - getInputImagesKey, + clearCreationDraftStorage, getModelKey, getModeKey, getThinkingLevelKey, - getPendingScopeId, getProjectScopeId, } from "@/common/constants/storage"; import type { Toast } from "@/browser/components/ChatInputToast"; @@ -121,6 +119,7 @@ export function useCreationWorkspace({ // Workspace name generation with debounce const workspaceNameState = useWorkspaceName({ + projectPath, message, debounceMs: 500, fallbackModel: fallbackModel ?? undefined, @@ -218,11 +217,8 @@ export function useCreationWorkspace({ }); // Sync preferences immediately (before switching) syncCreationPreferences(projectPath, metadata.id); - if (projectPath) { - const pendingScopeId = getPendingScopeId(projectPath); - updatePersistedState(getInputKey(pendingScopeId), ""); - updatePersistedState(getInputImagesKey(pendingScopeId), undefined); - } + // Clear draft state now that workspace is created + clearCreationDraftStorage(projectPath); // Switch to the workspace IMMEDIATELY after creation to exit splash faster. // The user sees the workspace UI while sendMessage kicks off the stream. diff --git a/src/browser/hooks/useDraftWorkspaceSettings.ts b/src/browser/hooks/useDraftWorkspaceSettings.ts index ea83ff2f14..b8b19b3e1b 100644 --- a/src/browser/hooks/useDraftWorkspaceSettings.ts +++ b/src/browser/hooks/useDraftWorkspaceSettings.ts @@ -1,4 +1,4 @@ -import { useState, useEffect } from "react"; +import { useEffect } from "react"; import { usePersistedState } from "./usePersistedState"; import { useThinkingLevel } from "./useThinkingLevel"; import { useMode } from "@/browser/contexts/ModeContext"; @@ -9,6 +9,7 @@ import { buildRuntimeString, } from "@/common/types/runtime"; import { + getDraftRuntimeKey, getModelKey, getRuntimeKey, getTrunkBranchKey, @@ -52,7 +53,7 @@ export function useDraftWorkspaceSettings( recommendedTrunk: string | null ): { settings: DraftWorkspaceSettings; - /** Set the currently selected runtime mode (does not persist) */ + /** Set the currently selected runtime mode (persists as part of draft) */ setRuntimeMode: (mode: RuntimeMode) => void; /** Set the default runtime mode for this project (persists via checkbox) */ setDefaultRuntimeMode: (mode: RuntimeMode) => void; @@ -81,14 +82,17 @@ export function useDraftWorkspaceSettings( // Parse default runtime string into mode (worktree when undefined) const { mode: defaultRuntimeMode } = parseRuntimeModeAndHost(defaultRuntimeString); - // Currently selected runtime mode for this session (initialized from default) - // This allows user to select a different runtime without changing the default - const [selectedRuntimeMode, setSelectedRuntimeMode] = useState(defaultRuntimeMode); + // Draft runtime selection - persisted so it survives navigation away and back. + // Uses undefined to mean "use default", allowing the default to be respected + // until the user explicitly selects something different. + const [draftRuntimeMode, setDraftRuntimeMode] = usePersistedState( + getDraftRuntimeKey(projectPath), + undefined, + { listener: true } + ); - // Sync selected mode when default changes (e.g., from checkbox or project switch) - useEffect(() => { - setSelectedRuntimeMode(defaultRuntimeMode); - }, [defaultRuntimeMode]); + // Effective selected mode: draft selection if set, otherwise fall back to default + const selectedRuntimeMode = draftRuntimeMode ?? defaultRuntimeMode; // Project-scoped trunk branch preference (persisted per project) const [trunkBranch, setTrunkBranch] = usePersistedState( @@ -113,17 +117,17 @@ export function useDraftWorkspaceSettings( } }, [branches, recommendedTrunk, trunkBranch, setTrunkBranch]); - // Setter for selected runtime mode (changes current selection, does not persist) + // Setter for selected runtime mode (persists as part of draft) const setRuntimeMode = (newMode: RuntimeMode) => { - setSelectedRuntimeMode(newMode); + setDraftRuntimeMode(newMode); }; // Setter for default runtime mode (persists via checkbox in tooltip) const setDefaultRuntimeMode = (newMode: RuntimeMode) => { const newRuntimeString = buildRuntimeString(newMode, lastSshHost); setDefaultRuntimeString(newRuntimeString); - // Also update selection to match new default - setSelectedRuntimeMode(newMode); + // Also update draft selection to match new default + setDraftRuntimeMode(newMode); }; // Setter for SSH host (persisted separately so it's remembered across mode switches) diff --git a/src/browser/hooks/useWorkspaceName.ts b/src/browser/hooks/useWorkspaceName.ts index 5a807a857f..d768cce14e 100644 --- a/src/browser/hooks/useWorkspaceName.ts +++ b/src/browser/hooks/useWorkspaceName.ts @@ -1,9 +1,13 @@ -import { useState, useRef, useCallback, useEffect, useMemo } from "react"; +import { useRef, useCallback, useEffect, useMemo } from "react"; import { useAPI } from "@/browser/contexts/API"; import { useGateway, formatAsGatewayModel } from "@/browser/hooks/useGatewayModels"; import { getKnownModel } from "@/common/constants/knownModels"; +import { getDraftNameKey, getDraftAutoGenerateKey } from "@/common/constants/storage"; +import { usePersistedState } from "@/browser/hooks/usePersistedState"; export interface UseWorkspaceNameOptions { + /** Project path for persisting draft state */ + projectPath: string; /** The user's message to generate a name for */ message: string; /** Debounce delay in milliseconds (default: 500) */ @@ -54,7 +58,7 @@ export interface UseWorkspaceNameReturn extends WorkspaceNameState { const PREFERRED_NAME_MODELS = [getKnownModel("HAIKU").id, getKnownModel("GPT_MINI").id]; export function useWorkspaceName(options: UseWorkspaceNameOptions): UseWorkspaceNameReturn { - const { message, debounceMs = 500, fallbackModel } = options; + const { projectPath, message, debounceMs = 500, fallbackModel } = options; const { api } = useAPI(); // Use global gateway availability (configured + enabled), not per-model toggles. // Name generation uses utility models (Haiku, GPT-Mini) that users don't explicitly @@ -70,13 +74,24 @@ export function useWorkspaceName(options: UseWorkspaceNameOptions): UseWorkspace return models; }, [gatewayConfigured]); - // Generated identity (name + title) from AI - const [generatedIdentity, setGeneratedIdentity] = useState(null); - // Manual name (user-editable during creation) - const [manualName, setManualName] = useState(""); - const [autoGenerate, setAutoGenerate] = useState(true); - const [isGenerating, setIsGenerating] = useState(false); - const [error, setError] = useState(null); + // Generated identity (name + title) from AI - not persisted since it's regenerated from message + const [generatedIdentity, setGeneratedIdentity] = usePersistedState( + getDraftNameKey(projectPath), + null, + { listener: true } + ); + // Manual name persisted together with generated identity in the same key + // When autoGenerate is off, we store the manual name in generatedIdentity.name + const [autoGenerate, setAutoGenerate] = usePersistedState( + getDraftAutoGenerateKey(projectPath), + true, + { listener: true } + ); + const [isGenerating, setIsGenerating] = usePersistedState( + `isGenerating:${projectPath}`, + false + ); + const [error, setError] = usePersistedState(`nameError:${projectPath}`, null); // Track the message that was used for the last successful generation const lastGeneratedForRef = useRef(""); @@ -93,9 +108,9 @@ export function useWorkspaceName(options: UseWorkspaceNameOptions): UseWorkspace requestId: number; } | null>(null); - // Name shown in CreationControls UI: generated name when auto, manual when not - const name = autoGenerate ? (generatedIdentity?.name ?? "") : manualName; - // Title is only shown when auto-generation is enabled (manual mode doesn't have generated title) + // Name shown in CreationControls UI: always from generatedIdentity (which stores both auto and manual) + const name = generatedIdentity?.name ?? ""; + // Title is only available when auto-generation is enabled const title = autoGenerate ? (generatedIdentity?.title ?? null) : null; // Cancel any pending generation and resolve waiters with null @@ -114,7 +129,7 @@ export function useWorkspaceName(options: UseWorkspaceNameOptions): UseWorkspace generationPromiseRef.current = null; setIsGenerating(false); } - }, []); + }, [setIsGenerating]); const generateIdentity = useCallback( async (forMessage: string): Promise => { @@ -181,7 +196,7 @@ export function useWorkspaceName(options: UseWorkspaceNameOptions): UseWorkspace } } }, - [api, preferredModels, fallbackModel] + [api, preferredModels, fallbackModel, setError, setGeneratedIdentity, setIsGenerating] ); // Debounced generation effect @@ -230,27 +245,31 @@ export function useWorkspaceName(options: UseWorkspaceNameOptions): UseWorkspace // Switching to auto: reset so debounced generation will trigger lastGeneratedForRef.current = ""; setError(null); - } else { - // Switching to manual: copy generated name as starting point for editing - if (generatedIdentity?.name) { - setManualName(generatedIdentity.name); - } } + // When switching to manual, the current name is already in generatedIdentity setAutoGenerate(enabled); }, - [generatedIdentity] + [setAutoGenerate, setError] ); - const setNameManual = useCallback((newName: string) => { - setManualName(newName); - // Clear error when user starts typing - setError(null); - }, []); + const setNameManual = useCallback( + (newName: string) => { + // Store manual name in generatedIdentity (preserving any existing title) + setGeneratedIdentity((prev) => ({ + name: newName, + title: prev?.title ?? newName, + })); + // Clear error when user starts typing + setError(null); + }, + [setGeneratedIdentity, setError] + ); const waitForGeneration = useCallback(async (): Promise => { // If auto-generate is off, user has provided a manual name // Use that name directly with a generated title from the message if (!autoGenerate) { + const manualName = generatedIdentity?.name ?? ""; if (!manualName.trim()) { setError("Please enter a workspace name"); return null; @@ -297,7 +316,7 @@ export function useWorkspaceName(options: UseWorkspaceNameOptions): UseWorkspace } return null; - }, [autoGenerate, manualName, generatedIdentity, message, generateIdentity]); + }, [autoGenerate, generatedIdentity, message, generateIdentity, setError]); return useMemo( () => ({ diff --git a/src/common/constants/storage.ts b/src/common/constants/storage.ts index 7a6e8a12c9..c70ec0388e 100644 --- a/src/common/constants/storage.ts +++ b/src/common/constants/storage.ts @@ -140,6 +140,36 @@ export function getRuntimeKey(projectPath: string): string { return `runtime:${projectPath}`; } +/** + * Get the localStorage key for the draft runtime selection for a project + * Stores the currently selected runtime during workspace creation (may differ from default). + * Cleared when workspace is created. + * Format: "draftRuntime:{projectPath}" + */ +export function getDraftRuntimeKey(projectPath: string): string { + return `draftRuntime:${projectPath}`; +} + +/** + * Get the localStorage key for the draft workspace name for a project. + * Stores the manually entered or auto-generated name during workspace creation. + * Cleared when workspace is created. + * Format: "draftName:{projectPath}" + */ +export function getDraftNameKey(projectPath: string): string { + return `draftName:${projectPath}`; +} + +/** + * Get the localStorage key for the draft auto-generate toggle for a project. + * Stores whether auto-generation is enabled during workspace creation. + * Cleared when workspace is created. + * Format: "draftAutoGenerate:{projectPath}" + */ +export function getDraftAutoGenerateKey(projectPath: string): string { + return `draftAutoGenerate:${projectPath}`; +} + /** * Get the localStorage key for trunk branch preference for a project * Stores the last used trunk branch when creating a workspace @@ -385,3 +415,29 @@ export function migrateWorkspaceStorage(oldWorkspaceId: string, newWorkspaceId: copyWorkspaceStorage(oldWorkspaceId, newWorkspaceId); deleteWorkspaceStorage(oldWorkspaceId); } + +/** + * Key functions for draft state during workspace creation. + * These are cleared when a workspace is successfully created. + */ +const DRAFT_KEY_FUNCTIONS: Array<(projectPath: string) => string> = [ + getDraftRuntimeKey, + getDraftNameKey, + getDraftAutoGenerateKey, +]; + +/** + * Clear all draft state for workspace creation. + * Called after a workspace is successfully created. + */ +export function clearCreationDraftStorage(projectPath: string): void { + // Clear project-scoped draft keys + for (const getKey of DRAFT_KEY_FUNCTIONS) { + localStorage.removeItem(getKey(projectPath)); + } + + // Clear pending-scoped input keys + const pendingScopeId = getPendingScopeId(projectPath); + localStorage.removeItem(getInputKey(pendingScopeId)); + localStorage.removeItem(getInputImagesKey(pendingScopeId)); +} From 9c9f1f297e9234fd7343cc143d68cef9bc746a73 Mon Sep 17 00:00:00 2001 From: Ammar Date: Sat, 20 Dec 2025 19:59:01 -0600 Subject: [PATCH 2/4] fix: update test to check localStorage for cleared draft state --- .../ChatInput/useCreationWorkspace.test.tsx | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/src/browser/components/ChatInput/useCreationWorkspace.test.tsx b/src/browser/components/ChatInput/useCreationWorkspace.test.tsx index 9534665824..9242050761 100644 --- a/src/browser/components/ChatInput/useCreationWorkspace.test.tsx +++ b/src/browser/components/ChatInput/useCreationWorkspace.test.tsx @@ -1,6 +1,9 @@ import type { APIClient } from "@/browser/contexts/API"; import type { DraftWorkspaceSettings } from "@/browser/hooks/useDraftWorkspaceSettings"; import { + getDraftAutoGenerateKey, + getDraftNameKey, + getDraftRuntimeKey, getInputKey, getInputImagesKey, getModelKey, @@ -489,13 +492,15 @@ describe("useCreationWorkspace", () => { expect(readPersistedStateCalls).toContainEqual([projectModeKey, null]); const modeKey = getModeKey(TEST_WORKSPACE_ID); - const pendingScopeId = getPendingScopeId(TEST_PROJECT_PATH); - const pendingInputKey = getInputKey(pendingScopeId); - const pendingImagesKey = getInputImagesKey(pendingScopeId); expect(updatePersistedStateCalls).toContainEqual([modeKey, "plan"]); - // Thinking is workspace-scoped, but this test doesn't set a project-scoped thinking preference. - expect(updatePersistedStateCalls).toContainEqual([pendingInputKey, ""]); - expect(updatePersistedStateCalls).toContainEqual([pendingImagesKey, undefined]); + + // Draft state is cleared via clearCreationDraftStorage() which uses localStorage directly + const pendingScopeId = getPendingScopeId(TEST_PROJECT_PATH); + expect(localStorage.getItem(getInputKey(pendingScopeId))).toBeNull(); + expect(localStorage.getItem(getInputImagesKey(pendingScopeId))).toBeNull(); + expect(localStorage.getItem(getDraftRuntimeKey(TEST_PROJECT_PATH))).toBeNull(); + expect(localStorage.getItem(getDraftNameKey(TEST_PROJECT_PATH))).toBeNull(); + expect(localStorage.getItem(getDraftAutoGenerateKey(TEST_PROJECT_PATH))).toBeNull(); }); test("handleSend surfaces backend errors and resets state", async () => { From dbb2b6dd5c474410d1173d5e2d7a949a005ad236 Mon Sep 17 00:00:00 2001 From: Ammar Date: Sat, 20 Dec 2025 20:00:37 -0600 Subject: [PATCH 3/4] fix: use updatePersistedState when clearing draft keys Address review feedback - use updatePersistedState instead of direct localStorage.removeItem so that subscribers using usePersistedState with listener: true receive immediate updates. --- .../ChatInput/useCreationWorkspace.test.tsx | 24 ++++++++++---- .../ChatInput/useCreationWorkspace.ts | 9 +++-- src/common/constants/storage.ts | 33 +++++++------------ 3 files changed, 36 insertions(+), 30 deletions(-) diff --git a/src/browser/components/ChatInput/useCreationWorkspace.test.tsx b/src/browser/components/ChatInput/useCreationWorkspace.test.tsx index 9242050761..9456b48f04 100644 --- a/src/browser/components/ChatInput/useCreationWorkspace.test.tsx +++ b/src/browser/components/ChatInput/useCreationWorkspace.test.tsx @@ -494,13 +494,25 @@ describe("useCreationWorkspace", () => { const modeKey = getModeKey(TEST_WORKSPACE_ID); expect(updatePersistedStateCalls).toContainEqual([modeKey, "plan"]); - // Draft state is cleared via clearCreationDraftStorage() which uses localStorage directly + // Draft state is cleared via updatePersistedState so subscribers update immediately const pendingScopeId = getPendingScopeId(TEST_PROJECT_PATH); - expect(localStorage.getItem(getInputKey(pendingScopeId))).toBeNull(); - expect(localStorage.getItem(getInputImagesKey(pendingScopeId))).toBeNull(); - expect(localStorage.getItem(getDraftRuntimeKey(TEST_PROJECT_PATH))).toBeNull(); - expect(localStorage.getItem(getDraftNameKey(TEST_PROJECT_PATH))).toBeNull(); - expect(localStorage.getItem(getDraftAutoGenerateKey(TEST_PROJECT_PATH))).toBeNull(); + expect(updatePersistedStateCalls).toContainEqual([getInputKey(pendingScopeId), undefined]); + expect(updatePersistedStateCalls).toContainEqual([ + getInputImagesKey(pendingScopeId), + undefined, + ]); + expect(updatePersistedStateCalls).toContainEqual([ + getDraftRuntimeKey(TEST_PROJECT_PATH), + undefined, + ]); + expect(updatePersistedStateCalls).toContainEqual([ + getDraftNameKey(TEST_PROJECT_PATH), + undefined, + ]); + expect(updatePersistedStateCalls).toContainEqual([ + getDraftAutoGenerateKey(TEST_PROJECT_PATH), + undefined, + ]); }); test("handleSend surfaces backend errors and resets state", async () => { diff --git a/src/browser/components/ChatInput/useCreationWorkspace.ts b/src/browser/components/ChatInput/useCreationWorkspace.ts index 08bd088f72..f0a81229d3 100644 --- a/src/browser/components/ChatInput/useCreationWorkspace.ts +++ b/src/browser/components/ChatInput/useCreationWorkspace.ts @@ -8,7 +8,7 @@ import { useDraftWorkspaceSettings } from "@/browser/hooks/useDraftWorkspaceSett import { readPersistedState, updatePersistedState } from "@/browser/hooks/usePersistedState"; import { getSendOptionsFromStorage } from "@/browser/utils/messages/sendOptions"; import { - clearCreationDraftStorage, + getCreationDraftKeys, getModelKey, getModeKey, getThinkingLevelKey, @@ -217,8 +217,11 @@ export function useCreationWorkspace({ }); // Sync preferences immediately (before switching) syncCreationPreferences(projectPath, metadata.id); - // Clear draft state now that workspace is created - clearCreationDraftStorage(projectPath); + // Clear draft state now that workspace is created (use updatePersistedState + // so subscribers using usePersistedState({ listener: true }) update immediately) + for (const key of getCreationDraftKeys(projectPath)) { + updatePersistedState(key, undefined); + } // Switch to the workspace IMMEDIATELY after creation to exit splash faster. // The user sees the workspace UI while sendMessage kicks off the stream. diff --git a/src/common/constants/storage.ts b/src/common/constants/storage.ts index c70ec0388e..679da77fe5 100644 --- a/src/common/constants/storage.ts +++ b/src/common/constants/storage.ts @@ -417,27 +417,18 @@ export function migrateWorkspaceStorage(oldWorkspaceId: string, newWorkspaceId: } /** - * Key functions for draft state during workspace creation. - * These are cleared when a workspace is successfully created. + * Get all storage keys that should be cleared when workspace creation completes. + * Returns both project-scoped draft keys and pending-scoped input keys. */ -const DRAFT_KEY_FUNCTIONS: Array<(projectPath: string) => string> = [ - getDraftRuntimeKey, - getDraftNameKey, - getDraftAutoGenerateKey, -]; - -/** - * Clear all draft state for workspace creation. - * Called after a workspace is successfully created. - */ -export function clearCreationDraftStorage(projectPath: string): void { - // Clear project-scoped draft keys - for (const getKey of DRAFT_KEY_FUNCTIONS) { - localStorage.removeItem(getKey(projectPath)); - } - - // Clear pending-scoped input keys +export function getCreationDraftKeys(projectPath: string): string[] { const pendingScopeId = getPendingScopeId(projectPath); - localStorage.removeItem(getInputKey(pendingScopeId)); - localStorage.removeItem(getInputImagesKey(pendingScopeId)); + return [ + // Project-scoped draft keys + getDraftRuntimeKey(projectPath), + getDraftNameKey(projectPath), + getDraftAutoGenerateKey(projectPath), + // Pending-scoped input keys + getInputKey(pendingScopeId), + getInputImagesKey(pendingScopeId), + ]; } From f8d490f23375ded9eb7946bcb86838ee63b4df5b Mon Sep 17 00:00:00 2001 From: Ammar Date: Sun, 21 Dec 2025 00:19:09 -0600 Subject: [PATCH 4/4] refactor: centralize draft name generation keys in storage.ts - Move isGenerating and error keys from inline string literals to getDraftNameGeneratingKey() and getDraftNameErrorKey() functions - Add these ephemeral keys to getCreationDraftKeys() so they're cleared when workspace creation completes - Update test assertions for the new cleared keys --- .../ChatInput/useCreationWorkspace.test.tsx | 10 ++++++++++ src/browser/hooks/useWorkspaceName.ts | 14 ++++++++++--- src/common/constants/storage.ts | 20 +++++++++++++++++++ 3 files changed, 41 insertions(+), 3 deletions(-) diff --git a/src/browser/components/ChatInput/useCreationWorkspace.test.tsx b/src/browser/components/ChatInput/useCreationWorkspace.test.tsx index 9456b48f04..c4925d9ed1 100644 --- a/src/browser/components/ChatInput/useCreationWorkspace.test.tsx +++ b/src/browser/components/ChatInput/useCreationWorkspace.test.tsx @@ -3,6 +3,8 @@ import type { DraftWorkspaceSettings } from "@/browser/hooks/useDraftWorkspaceSe import { getDraftAutoGenerateKey, getDraftNameKey, + getDraftNameGeneratingKey, + getDraftNameErrorKey, getDraftRuntimeKey, getInputKey, getInputImagesKey, @@ -513,6 +515,14 @@ describe("useCreationWorkspace", () => { getDraftAutoGenerateKey(TEST_PROJECT_PATH), undefined, ]); + expect(updatePersistedStateCalls).toContainEqual([ + getDraftNameGeneratingKey(TEST_PROJECT_PATH), + undefined, + ]); + expect(updatePersistedStateCalls).toContainEqual([ + getDraftNameErrorKey(TEST_PROJECT_PATH), + undefined, + ]); }); test("handleSend surfaces backend errors and resets state", async () => { diff --git a/src/browser/hooks/useWorkspaceName.ts b/src/browser/hooks/useWorkspaceName.ts index d768cce14e..6f0b4a0709 100644 --- a/src/browser/hooks/useWorkspaceName.ts +++ b/src/browser/hooks/useWorkspaceName.ts @@ -2,7 +2,12 @@ import { useRef, useCallback, useEffect, useMemo } from "react"; import { useAPI } from "@/browser/contexts/API"; import { useGateway, formatAsGatewayModel } from "@/browser/hooks/useGatewayModels"; import { getKnownModel } from "@/common/constants/knownModels"; -import { getDraftNameKey, getDraftAutoGenerateKey } from "@/common/constants/storage"; +import { + getDraftNameKey, + getDraftAutoGenerateKey, + getDraftNameGeneratingKey, + getDraftNameErrorKey, +} from "@/common/constants/storage"; import { usePersistedState } from "@/browser/hooks/usePersistedState"; export interface UseWorkspaceNameOptions { @@ -88,10 +93,13 @@ export function useWorkspaceName(options: UseWorkspaceNameOptions): UseWorkspace { listener: true } ); const [isGenerating, setIsGenerating] = usePersistedState( - `isGenerating:${projectPath}`, + getDraftNameGeneratingKey(projectPath), false ); - const [error, setError] = usePersistedState(`nameError:${projectPath}`, null); + const [error, setError] = usePersistedState( + getDraftNameErrorKey(projectPath), + null + ); // Track the message that was used for the last successful generation const lastGeneratedForRef = useRef(""); diff --git a/src/common/constants/storage.ts b/src/common/constants/storage.ts index 679da77fe5..77073deb98 100644 --- a/src/common/constants/storage.ts +++ b/src/common/constants/storage.ts @@ -170,6 +170,24 @@ export function getDraftAutoGenerateKey(projectPath: string): string { return `draftAutoGenerate:${projectPath}`; } +/** + * Get the localStorage key for the draft name generation loading state. + * Ephemeral UI state cleared when workspace is created. + * Format: "draftNameGenerating:{projectPath}" + */ +export function getDraftNameGeneratingKey(projectPath: string): string { + return `draftNameGenerating:${projectPath}`; +} + +/** + * Get the localStorage key for the draft name generation error. + * Ephemeral UI state cleared when workspace is created. + * Format: "draftNameError:{projectPath}" + */ +export function getDraftNameErrorKey(projectPath: string): string { + return `draftNameError:${projectPath}`; +} + /** * Get the localStorage key for trunk branch preference for a project * Stores the last used trunk branch when creating a workspace @@ -427,6 +445,8 @@ export function getCreationDraftKeys(projectPath: string): string[] { getDraftRuntimeKey(projectPath), getDraftNameKey(projectPath), getDraftAutoGenerateKey(projectPath), + getDraftNameGeneratingKey(projectPath), + getDraftNameErrorKey(projectPath), // Pending-scoped input keys getInputKey(pendingScopeId), getInputImagesKey(pendingScopeId),