mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-23 16:36:23 +00:00
Two structural fixes from the Desktop performance audit (P2 tier): 1. Scope live tool-diff subscriptions. `ToolEntry` subscribed to the whole `$toolDiffs` map via `useStore`, so one `recordToolDiff` re-rendered every mounted tool row. Add a cached per-toolCallId derived atom (`$toolInlineDiff(id)`, mirroring the existing `$toolDisclosureOpen` pattern); computed() only notifies when that id's diff string changes, so a live patch re-renders one row. 2. Narrow profile / gateway-switch query invalidation. Both the active-profile subscription and `wipeSessionListsForGatewaySwitch` called keyless `queryClient.invalidateQueries()`, refetching account/marketplace/onboarding caches on every switch. Add `invalidateProfileScopedQueries()` with a denylist of profile-independent roots (billing, marketplace-themes, onboarding-model-options, contrib-logs-tail). A denylist is correctness-safe: a root we forget just refetches (cheap), whereas an allowlist that misses a profile-scoped key would paint the previous profile's data. Tests: per-tool notify isolation, and real-QueryClient invalidation partition (profile-scoped invalidated, global left intact, unknown keys invalidated).
48 lines
1.9 KiB
TypeScript
48 lines
1.9 KiB
TypeScript
import { QueryClient, type QueryKey } from '@tanstack/react-query'
|
|
|
|
// Shared React Query client. Lives in its own module (not main.tsx) so non-React
|
|
// code — e.g. the profile store on a gateway swap — can invalidate cached,
|
|
// profile-scoped settings without importing the app entry point.
|
|
export const queryClient = new QueryClient({
|
|
defaultOptions: {
|
|
queries: {
|
|
refetchOnWindowFocus: false,
|
|
staleTime: 60_000
|
|
}
|
|
}
|
|
})
|
|
|
|
// Curried, setState-shaped cache writer for optimistic write-through: keeps
|
|
// mutation sites terse (`setX(next)` or `setX(prev => …)`) over one query key.
|
|
export const writeCache =
|
|
<T>(key: QueryKey) =>
|
|
(next: T | undefined | ((prev: T | undefined) => T | undefined)): void =>
|
|
void queryClient.setQueryData<T>(key, next)
|
|
|
|
// Query-key roots that are NOT profile-scoped: account/billing, the theme
|
|
// marketplace, onboarding, and contrib log tails all read global or
|
|
// account-level state, so a profile/gateway swap must not refetch them. Any
|
|
// other key is treated as profile-scoped and invalidated -- a denylist is
|
|
// correctness-safe here: a root we forget to list just gets refetched (a small
|
|
// cost), whereas an allowlist that misses a profile-scoped key would paint the
|
|
// previous profile's data (a bug).
|
|
const PROFILE_INDEPENDENT_QUERY_ROOTS = new Set<string>([
|
|
'billing',
|
|
'marketplace-themes',
|
|
'marketplace-themes-settings',
|
|
'onboarding-model-options',
|
|
'contrib-logs-tail'
|
|
])
|
|
|
|
// Invalidate profile-scoped query caches on a profile / gateway switch, leaving
|
|
// account/global caches intact. Replaces a keyless invalidateQueries() that
|
|
// refetched everything (billing, marketplace, onboarding) on every switch.
|
|
export function invalidateProfileScopedQueries(): void {
|
|
void queryClient.invalidateQueries({
|
|
predicate: query => {
|
|
const root = query.queryKey[0]
|
|
|
|
return typeof root !== 'string' || !PROFILE_INDEPENDENT_QUERY_ROOTS.has(root)
|
|
}
|
|
})
|
|
}
|