Skip to content

Commit 1602009

Browse files
committed
fix(stores): whitelist durable fields in persist partialize
chat/terminal/panel persist configs leaked actions and transient state into localStorage. Replace the chat full-state spread with an explicit durable whitelist, and add partialize to terminal and panel (which had none) so isResizing and _hasHydrated are no longer persisted. Panel keeps activeTab + panelWidth because the layout.tsx blocking script reads them from panel-state to set data-panel-active-tab before hydration (SSR tab-flash prevention). Harden sim-stores doctrine: persist MUST use an explicit partialize whitelist; never persist transient flags or _hasHydrated.
1 parent f752ec0 commit 1602009

4 files changed

Lines changed: 38 additions & 2 deletions

File tree

.claude/rules/sim-stores.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ export const useFeatureStore = create<FeatureState>()(
5757

5858
1. Use `devtools` middleware (named stores)
5959
2. Use `persist` only when data should survive reload
60-
3. `partialize` to persist only necessary state
60+
3. `persist` MUST use `partialize` with an explicit whitelist of the durable fields. NEVER persist transient flags (`isResizing`, drag/hover state) or `_hasHydrated`, and never spread the whole state (`{ ...state }`) — it leaks actions and transient state into storage
6161
4. `_hasHydrated` pattern for persisted stores needing hydration tracking
6262
5. Immutable updates only
6363
6. `set((state) => ...)` when depending on previous state

apps/sim/stores/chat/store.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -254,8 +254,20 @@ export const useChatStore = create<ChatState>()(
254254
}),
255255
{
256256
name: 'chat-store',
257+
/**
258+
* Persist only the durable chat state — message history (with transient
259+
* blob `previewUrl`s stripped since they are not valid across reloads),
260+
* per-workflow output selections and conversation ids, and the floating
261+
* chat's open state, position, and dimensions. Actions and any transient
262+
* UI flags are intentionally excluded.
263+
*/
257264
partialize: (state) => ({
258-
...state,
265+
isChatOpen: state.isChatOpen,
266+
chatPosition: state.chatPosition,
267+
chatWidth: state.chatWidth,
268+
chatHeight: state.chatHeight,
269+
selectedWorkflowOutputs: state.selectedWorkflowOutputs,
270+
conversationIds: state.conversationIds,
259271
messages: state.messages.map((msg) => ({
260272
...msg,
261273
attachments: msg.attachments?.map((att) => ({

apps/sim/stores/panel/store.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,17 @@ export const usePanelStore = create<PanelState>()(
4040
}),
4141
{
4242
name: 'panel-state',
43+
/**
44+
* Persist only the durable panel preferences. `activeTab` MUST be kept:
45+
* the blocking script in `app/layout.tsx` reads it from this persisted
46+
* `panel-state` entry to set `data-panel-active-tab` before hydration,
47+
* preventing a tab flash. The transient `isResizing` drag flag and the
48+
* `_hasHydrated` hydration marker are excluded.
49+
*/
50+
partialize: (state) => ({
51+
panelWidth: state.panelWidth,
52+
activeTab: state.activeTab,
53+
}),
4354
onRehydrateStorage: () => (state) => {
4455
// Sync CSS variables with stored state after rehydration
4556
if (state && typeof window !== 'undefined') {

apps/sim/stores/terminal/store.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,19 @@ export const useTerminalStore = create<TerminalState>()(
9393
}),
9494
{
9595
name: 'terminal-state',
96+
/**
97+
* Persist only the durable terminal UI preferences. The transient
98+
* `isResizing` drag flag and the `_hasHydrated` hydration marker are
99+
* excluded so they always start fresh on load.
100+
*/
101+
partialize: (state) => ({
102+
terminalHeight: state.terminalHeight,
103+
lastExpandedHeight: state.lastExpandedHeight,
104+
outputPanelWidth: state.outputPanelWidth,
105+
openOnRun: state.openOnRun,
106+
wrapText: state.wrapText,
107+
structuredView: state.structuredView,
108+
}),
96109
/**
97110
* Synchronizes the `--terminal-height` CSS custom property with the
98111
* persisted store value after client-side rehydration.

0 commit comments

Comments
 (0)