From 498f2d9b933849e07917d6064f5fcef12ebf2312 Mon Sep 17 00:00:00 2001 From: Alessandro Pogliaghi Date: Thu, 11 Jun 2026 15:47:44 +0100 Subject: [PATCH] fix(settings): restore last-used workspace mode after async hydration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The settings store persists lastUsedWorkspaceMode and debugLogsCloudRuns, but it rehydrates asynchronously from the host-backed (tRPC/encrypted) storage. Regressed in #2047: TaskInput previously derived the workspace mode reactively, which re-read the rehydrated value after a relaunch. #2047 converted it to a one-shot useState initializer that runs before hydration completes, so it locked in the default "local" and never adopted the rehydrated value — and on task creation that stale "local" was written back, poisoning the stored preference. Make hydration observable via an onRehydrateStorage _hasHydrated flag (mirroring draftStore) and have TaskInput adopt the rehydrated workspace mode once it lands, unless a cloud repo was forced via props or the user already picked a mode. Add regression tests covering rehydration of the workspace mode, the debug-logs-for-cloud-runs toggle, and the hydration flag. Generated-By: PostHog Code Task-Id: 817c46ca-bccc-4979-bc70-d3587cdb79aa --- .../features/settings/settingsStore.test.ts | 27 +++++++++++++++++++ .../ui/src/features/settings/settingsStore.ts | 9 +++++++ .../task-detail/components/TaskInput.tsx | 11 ++++++++ 3 files changed, 47 insertions(+) diff --git a/packages/ui/src/features/settings/settingsStore.test.ts b/packages/ui/src/features/settings/settingsStore.test.ts index a220df98cd..b645566426 100644 --- a/packages/ui/src/features/settings/settingsStore.test.ts +++ b/packages/ui/src/features/settings/settingsStore.test.ts @@ -75,6 +75,33 @@ describe("feature settingsStore cloud selections", () => { expect(useSettingsStore.getState().allowBypassPermissions).toBe(true); }); + + it.each([ + ["lastUsedWorkspaceMode", "local", "cloud"], + ["debugLogsCloudRuns", false, true], + ] as const)("rehydrates %s", async (field, initial, persisted) => { + getItem.mockResolvedValue( + JSON.stringify({ state: { [field]: persisted }, version: 0 }), + ); + + useSettingsStore.setState({ [field]: initial } as Parameters< + typeof useSettingsStore.setState + >[0]); + + await useSettingsStore.persist.rehydrate(); + + expect(useSettingsStore.getState()[field]).toBe(persisted); + }); + + it("flips _hasHydrated once the persisted snapshot lands", async () => { + getItem.mockResolvedValue(JSON.stringify({ state: {}, version: 0 })); + + useSettingsStore.setState({ _hasHydrated: false }); + + await useSettingsStore.persist.rehydrate(); + + expect(useSettingsStore.getState()._hasHydrated).toBe(true); + }); }); describe("feature settingsStore terminal font", () => { diff --git a/packages/ui/src/features/settings/settingsStore.ts b/packages/ui/src/features/settings/settingsStore.ts index ddf2c314a5..8baf4134cf 100644 --- a/packages/ui/src/features/settings/settingsStore.ts +++ b/packages/ui/src/features/settings/settingsStore.ts @@ -130,6 +130,9 @@ interface SettingsStore { shouldShowHint: (key: string, max?: number) => boolean; recordHintShown: (key: string) => void; markHintLearned: (key: string) => void; + + _hasHydrated: boolean; + setHasHydrated: (hydrated: boolean) => void; } // ---------- Store ---------- @@ -259,6 +262,9 @@ export const useSettingsStore = create()( }, }; }), + + _hasHydrated: false, + setHasHydrated: (hydrated) => set({ _hasHydrated: hydrated }), }), { name: "settings-storage", @@ -309,6 +315,9 @@ export const useSettingsStore = create()( // Onboarding hints hints: state.hints, }), + onRehydrateStorage: () => (state) => { + state?.setHasHydrated(true); + }, merge: (persisted, current) => { const merged = { ...current, diff --git a/packages/ui/src/features/task-detail/components/TaskInput.tsx b/packages/ui/src/features/task-detail/components/TaskInput.tsx index 8086c49656..b228986a11 100644 --- a/packages/ui/src/features/task-detail/components/TaskInput.tsx +++ b/packages/ui/src/features/task-detail/components/TaskInput.tsx @@ -117,6 +117,7 @@ export function TaskInput({ lastUsedInitialTaskMode, setLastUsedReasoningEffort, setLastUsedModel, + _hasHydrated: settingsHydrated, } = useSettingsStore(); const editorRef = useRef(null); @@ -211,7 +212,17 @@ export function TaskInput({ return lastUsedWorkspaceMode || "local"; }); + const didResolveWorkspaceModeRef = useRef(false); + useEffect(() => { + if (didResolveWorkspaceModeRef.current) return; + if (!settingsHydrated) return; + didResolveWorkspaceModeRef.current = true; + if (initialCloudRepository) return; + setWorkspaceModeState(lastUsedWorkspaceMode || "local"); + }, [settingsHydrated, lastUsedWorkspaceMode, initialCloudRepository]); + const setWorkspaceMode = (mode: WorkspaceMode) => { + didResolveWorkspaceModeRef.current = true; setWorkspaceModeState(mode); setLastUsedWorkspaceMode(mode); if (mode !== "cloud") {