-
Notifications
You must be signed in to change notification settings - Fork 79
refactor(web): convert current-platform-assistant hook to Zustand store (LUM-1718) #31433
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,168 @@ | ||
| /** | ||
| * Zustand store for the per-organization "current platform assistant" | ||
| * selection. Persists the selected assistant ID for each org to | ||
| * localStorage so a reload or new tab restores the prior selection. | ||
| * | ||
| * **Storage model — one localStorage key per org:** | ||
| * | ||
| * Each org's selection is stored under | ||
| * `vellum_current_assistant_id__{orgId}`. A custom `StateStorage` | ||
| * adapter wired into the `persist` middleware reads/writes those keys | ||
| * directly, so the on-disk format stays compatible with the prior | ||
| * hand-rolled implementation. | ||
| * | ||
| * **Cross-tab sync:** | ||
| * | ||
| * The persist middleware doesn't subscribe to `storage` events on its | ||
| * own. We listen for any `storage` event whose key carries the | ||
| * per-org prefix and trigger `persist.rehydrate()` so the store | ||
| * picks up writes from other tabs. | ||
| * | ||
| * References: | ||
| * - {@link https://zustand.docs.pmnd.rs/} | ||
| * - {@link https://zustand.docs.pmnd.rs/integrations/persisting-store-data} | ||
| */ | ||
|
|
||
| import { create } from "zustand"; | ||
| import { | ||
| createJSONStorage, | ||
| persist, | ||
| type StateStorage, | ||
| } from "zustand/middleware"; | ||
|
|
||
| import { createSelectors } from "@/utils/create-selectors.js"; | ||
|
|
||
| export const PLATFORM_ASSISTANT_STORAGE_PREFIX = | ||
| "vellum_current_assistant_id__"; | ||
|
|
||
| export interface CurrentPlatformAssistantState { | ||
| /** orgId → selected assistant ID. Absent entries mean "no selection yet". */ | ||
| byOrg: Record<string, string>; | ||
| } | ||
|
|
||
| export interface CurrentPlatformAssistantActions { | ||
| setAssistantId: (orgId: string, id: string | null) => void; | ||
| getAssistantId: (orgId: string) => string | null; | ||
| } | ||
|
|
||
| export type CurrentPlatformAssistantStore = CurrentPlatformAssistantState & | ||
| CurrentPlatformAssistantActions; | ||
|
|
||
| function readByOrgFromLocalStorage(): Record<string, string> { | ||
| if (typeof window === "undefined") return {}; | ||
| const byOrg: Record<string, string> = {}; | ||
| try { | ||
| for (let i = 0; i < window.localStorage.length; i++) { | ||
| const key = window.localStorage.key(i); | ||
| if (key && key.startsWith(PLATFORM_ASSISTANT_STORAGE_PREFIX)) { | ||
| const orgId = key.slice(PLATFORM_ASSISTANT_STORAGE_PREFIX.length); | ||
| const value = window.localStorage.getItem(key); | ||
| if (value != null) byOrg[orgId] = value; | ||
| } | ||
| } | ||
| } catch { | ||
| // ignore storage failures | ||
| } | ||
| return byOrg; | ||
| } | ||
|
|
||
| function writeByOrgToLocalStorage(byOrg: Record<string, string>): void { | ||
| if (typeof window === "undefined") return; | ||
| try { | ||
| const existing: string[] = []; | ||
| for (let i = 0; i < window.localStorage.length; i++) { | ||
| const key = window.localStorage.key(i); | ||
| if (key && key.startsWith(PLATFORM_ASSISTANT_STORAGE_PREFIX)) { | ||
| existing.push(key); | ||
| } | ||
| } | ||
| for (const key of existing) { | ||
| const orgId = key.slice(PLATFORM_ASSISTANT_STORAGE_PREFIX.length); | ||
| if (byOrg[orgId] == null) { | ||
| window.localStorage.removeItem(key); | ||
| } | ||
| } | ||
| for (const [orgId, id] of Object.entries(byOrg)) { | ||
| window.localStorage.setItem( | ||
| `${PLATFORM_ASSISTANT_STORAGE_PREFIX}${orgId}`, | ||
| id, | ||
| ); | ||
| } | ||
| } catch { | ||
| // ignore storage failures | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Translates the single-name `name` view that `persist` expects into | ||
| * per-org reads and writes against the existing | ||
| * `vellum_current_assistant_id__{orgId}` localStorage keys. | ||
| */ | ||
| const perOrgStorage: StateStorage = { | ||
| getItem: () => { | ||
| const byOrg = readByOrgFromLocalStorage(); | ||
| return JSON.stringify({ state: { byOrg }, version: 0 }); | ||
| }, | ||
| setItem: (_name, value) => { | ||
| let parsed: { state?: { byOrg?: Record<string, string> } }; | ||
| try { | ||
| parsed = JSON.parse(value) as typeof parsed; | ||
| } catch { | ||
| return; | ||
| } | ||
| const byOrg = parsed.state?.byOrg ?? {}; | ||
| writeByOrgToLocalStorage(byOrg); | ||
| }, | ||
| removeItem: () => { | ||
| writeByOrgToLocalStorage({}); | ||
| }, | ||
| }; | ||
|
|
||
| const CURRENT_PLATFORM_ASSISTANT_STORE_NAME = | ||
| "vellum:current-platform-assistant"; | ||
|
|
||
| const useCurrentPlatformAssistantStoreBase = create<CurrentPlatformAssistantStore>()( | ||
| persist( | ||
| (set, get) => ({ | ||
| byOrg: readByOrgFromLocalStorage(), | ||
|
|
||
| setAssistantId: (orgId, id) => | ||
| set((state) => { | ||
| const next = { ...state.byOrg }; | ||
| if (id == null) { | ||
| delete next[orgId]; | ||
| } else { | ||
| next[orgId] = id; | ||
| } | ||
| return { byOrg: next }; | ||
| }), | ||
|
|
||
| getAssistantId: (orgId) => get().byOrg[orgId] ?? null, | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🚩 getAssistantId action is defined but unused — intentional API surface The Was this helpful? React with 👍 or 👎 to provide feedback. |
||
| }), | ||
| { | ||
| name: CURRENT_PLATFORM_ASSISTANT_STORE_NAME, | ||
| storage: createJSONStorage(() => perOrgStorage), | ||
| partialize: (state) => ({ byOrg: state.byOrg }), | ||
| }, | ||
| ), | ||
| ); | ||
|
|
||
| export const useCurrentPlatformAssistantStore = createSelectors( | ||
| useCurrentPlatformAssistantStoreBase, | ||
| ); | ||
|
|
||
| // --------------------------------------------------------------------------- | ||
| // Cross-tab sync | ||
| // --------------------------------------------------------------------------- | ||
|
|
||
| if (typeof window !== "undefined") { | ||
| window.addEventListener("storage", (event) => { | ||
| if (event.key === null) { | ||
| void useCurrentPlatformAssistantStoreBase.persist.rehydrate(); | ||
| return; | ||
| } | ||
| if (event.key.startsWith(PLATFORM_ASSISTANT_STORAGE_PREFIX)) { | ||
| void useCurrentPlatformAssistantStoreBase.persist.rehydrate(); | ||
| } | ||
| }); | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,72 +1,15 @@ | ||
| import { useQuery } from "@tanstack/react-query"; | ||
| import { useCallback, useEffect, useSyncExternalStore } from "react"; | ||
| import { useCallback, useEffect } from "react"; | ||
|
|
||
| import { assistantsListOptions } from "@/generated/api/@tanstack/react-query.gen.js"; | ||
| import type { Assistant } from "@/generated/api/types.gen.js"; | ||
| import { useCurrentPlatformAssistantStore } from "@/domains/settings/current-platform-assistant-store.js"; | ||
| import { useOrganizationStore } from "@/stores/organization-store.js"; | ||
|
|
||
| const PLATFORM_ASSISTANT_STORAGE_PREFIX = "vellum_current_assistant_id__"; | ||
|
|
||
| const PLATFORM_LIST_OPTIONS = assistantsListOptions({ | ||
| query: { hosting: "platform" }, | ||
| }); | ||
|
|
||
| function storageKeyForOrg(orgId: string | null): string | null { | ||
| if (!orgId) return null; | ||
| return `${PLATFORM_ASSISTANT_STORAGE_PREFIX}${orgId}`; | ||
| } | ||
|
|
||
| type Listener = () => void; | ||
| const listeners = new Set<Listener>(); | ||
|
|
||
| function notify(): void { | ||
| for (const listener of listeners) listener(); | ||
| } | ||
|
|
||
| if (typeof window !== "undefined") { | ||
| window.addEventListener("storage", (event: StorageEvent) => { | ||
| if (event.key === null) { | ||
| notify(); | ||
| return; | ||
| } | ||
| if (event.key.startsWith(PLATFORM_ASSISTANT_STORAGE_PREFIX)) { | ||
| notify(); | ||
| } | ||
| }); | ||
| } | ||
|
|
||
| function subscribe(listener: Listener): () => void { | ||
| listeners.add(listener); | ||
| return () => { | ||
| listeners.delete(listener); | ||
| }; | ||
| } | ||
|
|
||
| function getSnapshot(orgId: string | null): string | null { | ||
| const key = storageKeyForOrg(orgId); | ||
| if (!key) return null; | ||
| try { | ||
| return window.localStorage.getItem(key); | ||
| } catch { | ||
| return null; | ||
| } | ||
| } | ||
|
|
||
| function writeStoredId(orgId: string | null, id: string | null): void { | ||
| const key = storageKeyForOrg(orgId); | ||
| if (!key) return; | ||
| try { | ||
| if (id == null) { | ||
| window.localStorage.removeItem(key); | ||
| } else { | ||
| window.localStorage.setItem(key, id); | ||
| } | ||
| } catch { | ||
| // ignore storage failures | ||
| } | ||
| notify(); | ||
| } | ||
|
|
||
| export interface UseCurrentPlatformAssistantResult { | ||
| assistantId: string | null; | ||
| assistant: Assistant | null; | ||
|
|
@@ -78,12 +21,11 @@ export interface UseCurrentPlatformAssistantResult { | |
|
|
||
| export function useCurrentPlatformAssistant(): UseCurrentPlatformAssistantResult { | ||
| const orgId = useOrganizationStore.use.currentOrganizationId(); | ||
| const byOrg = useCurrentPlatformAssistantStore.use.byOrg(); | ||
| const setAssistantIdAction = | ||
| useCurrentPlatformAssistantStore.use.setAssistantId(); | ||
|
Comment on lines
+25
to
+26
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 Action obtained via The hook subscribes to the Prompt for agentsWas this helpful? React with 👍 or 👎 to provide feedback. |
||
|
|
||
| const storedId = useSyncExternalStore( | ||
| subscribe, | ||
| useCallback(() => getSnapshot(orgId), [orgId]), | ||
| () => null, | ||
| ); | ||
| const storedId = orgId ? (byOrg[orgId] ?? null) : null; | ||
|
|
||
| const listQuery = useQuery(PLATFORM_LIST_OPTIONS); | ||
|
|
||
|
|
@@ -109,16 +51,24 @@ export function useCurrentPlatformAssistant(): UseCurrentPlatformAssistantResult | |
| if (!isListLoaded) return; | ||
| if (platformAssistants.length === 0) return; | ||
| if (resolvedId === storedId) return; | ||
| if (resolvedId != null) { | ||
| writeStoredId(orgId, resolvedId); | ||
| if (resolvedId != null && orgId) { | ||
| setAssistantIdAction(orgId, resolvedId); | ||
| } | ||
| }, [isListLoaded, platformAssistants.length, resolvedId, storedId, orgId]); | ||
| }, [ | ||
| isListLoaded, | ||
| platformAssistants.length, | ||
| resolvedId, | ||
| storedId, | ||
| orgId, | ||
| setAssistantIdAction, | ||
| ]); | ||
|
|
||
| const setAssistantId = useCallback( | ||
| (id: string | null) => { | ||
| writeStoredId(orgId, id); | ||
| if (!orgId) return; | ||
| setAssistantIdAction(orgId, id); | ||
| }, | ||
| [orgId], | ||
| [orgId, setAssistantIdAction], | ||
| ); | ||
|
|
||
| return { | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
writeByOrgToLocalStoragetreats the in-memorybyOrgsnapshot as authoritative and removes every prefixed localStorage key not present in that snapshot. In multi-tab usage, a stale tab can write before itsstorage-triggeredrehydrate()runs, causing it to delete assistant selections another tab just wrote for different orgs. The prior implementation only touched the active org key, so this introduces a lost-update regression under concurrent tab writes.Useful? React with 👍 / 👎.