From 70c2f0811d84cf82b68864bf8f2b6eec423afc13 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Padez?= Date: Fri, 7 Aug 2026 08:48:44 +0000 Subject: [PATCH] stop swallowing dashboard persist failures MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The optimistic cache made a refused write invisible: the UI stayed correct until the next reload, at which point the change was simply gone. That is tolerable for a pane size and not for a chat panel's agent name, which is the address a peer agent is delivered to. Roll back only if the cache still holds exactly what we wrote — writes to one key overlap freely (a window resize fires one per group) and rolling back over a later successful write would turn one failure into two. Co-Authored-By: Claude Opus 5 --- src/workspaces/state/src/useDashboardState.ts | 44 ++++++++++++++++++- 1 file changed, 42 insertions(+), 2 deletions(-) diff --git a/src/workspaces/state/src/useDashboardState.ts b/src/workspaces/state/src/useDashboardState.ts index a821257e..2b7e3de7 100644 --- a/src/workspaces/state/src/useDashboardState.ts +++ b/src/workspaces/state/src/useDashboardState.ts @@ -1,5 +1,6 @@ import { useCallback, useEffect, useRef } from 'react'; import { useQuery, useQueryClient } from '@tanstack/react-query'; +import { toast } from 'sonner'; import { useClient } from 'hooks/useClient'; import { useAuth } from 'hooks/useAuth'; import type { UserState } from './useSettings'; @@ -29,7 +30,9 @@ export function useDashboardState(key: string, defaultValue: T) { const currentState = queryClient.getQueryData(QUERY_KEY) ?? {}; if (!(key in currentState)) { queryClient.setQueryData(QUERY_KEY, { ...currentState, [key]: defaultValue }); - clientRef.current.patch('/dashboards', { [key]: defaultValue }).catch(() => { }); + clientRef.current + .patch('/dashboards', { [key]: defaultValue }) + .catch((err: unknown) => revert(queryClient, key, defaultValue, undefined, err)); } }, [isSuccess, key, defaultValue, queryClient]); @@ -43,10 +46,47 @@ export function useDashboardState(key: string, defaultValue: T) { queryClient.setQueryData(QUERY_KEY, { ...currentState, [key]: newValue }); - clientRef.current.patch('/dashboards', { [key]: newValue }).catch(() => { }); + clientRef.current + .patch('/dashboards', { [key]: newValue }) + .catch((err: unknown) => + revert(queryClient, key, newValue, key in currentState ? currentValue : undefined, err), + ); }, [key, defaultValue, queryClient], ); return { key, value, setValue, isLoaded: isSuccess }; } + +/** + * Undo an optimistic write the server refused, and say so. + * + * The optimistic cache is what made these failures invisible: the UI stayed correct until the next + * reload, at which point the change was simply gone. That is worst for the things this store holds that + * are not merely cosmetic — a chat panel's `config.agentName` is the address a peer agent is delivered + * to, so a silently-dropped write leaves a panel that answers to a name it will not have tomorrow. + * + * Reverts only if the cache still holds exactly what we wrote. Writes to one key overlap freely (a + * window resize fires one per group), and rolling back over a later successful write would turn one + * failure into two. + */ +function revert( + queryClient: ReturnType, + key: string, + attempted: unknown, + previous: unknown, + err: unknown, +): void { + const state = queryClient.getQueryData(QUERY_KEY) ?? {}; + if (state[key] === attempted) { + const next = { ...state }; + if (previous === undefined) delete next[key]; + else next[key] = previous; + queryClient.setQueryData(QUERY_KEY, next); + } + + console.error(`[dashboard-state] failed to persist "${key}"`, err); + toast.error('Could not save your layout', { + description: `"${key}" was not written and has been rolled back. ${err instanceof Error ? err.message : String(err)}`, + }); +}