fix(desktop): setup cards stop redrawing in a loop
useSetupRows built a new row array on every render for the app-owned lists (accent, layout, theme). That recreated the card's stage callback, whose effect wrote $setupChooseStages, which re-rendered the card. The rows are memoized on their inputs now, and stageSetupChoose skips the atom write when a patch changes nothing.
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import type { ConnectorRow, SetupChooseKind, SetupChooseOption } from '@hermes/shared'
|
||||
import { useMemo } from 'react'
|
||||
|
||||
import { MODE_OPTIONS } from '@/app/settings/constants'
|
||||
import { $chatLayoutPicked, assembleChatOnboarding, snapshotChatLayout } from '@/components/onboarding-chat/assembly'
|
||||
@@ -132,15 +133,20 @@ export function useSetupRows(setup: null | SetupChooseSpec, storedId: null | str
|
||||
const connectors = useConnectorRows(setup?.options === null && setup.kind === 'connectors' ? storedId : null)
|
||||
const plugins = useOnboardingPluginList(setup?.options === null && setup.kind === 'plugins' ? storedId : null)
|
||||
|
||||
if (!setup) {
|
||||
return []
|
||||
}
|
||||
// Same inputs must return the same array: the pending card's stage callback and store write key off it.
|
||||
return useMemo(() => {
|
||||
if (!setup) {
|
||||
return NO_ROWS
|
||||
}
|
||||
|
||||
const rows = setup.options ?? APP_ROWS[setup.kind]({ connectors, dark: renderedMode === 'dark', plugins, t })
|
||||
const rows = setup.options ?? APP_ROWS[setup.kind]({ connectors, dark: renderedMode === 'dark', plugins, t })
|
||||
|
||||
return setup.kind === 'theme' && rows ? rows.filter(row => THEME_TILES.includes(row.id)) : rows
|
||||
return setup.kind === 'theme' && rows ? rows.filter(row => THEME_TILES.includes(row.id)) : rows
|
||||
}, [connectors, plugins, renderedMode, setup, t])
|
||||
}
|
||||
|
||||
const NO_ROWS: SetupRow[] = []
|
||||
|
||||
export function useSetupLabel(
|
||||
kind: SetupChooseKind,
|
||||
options: null | SetupRow[],
|
||||
|
||||
@@ -181,6 +181,21 @@ describe('answerSetupCard', () => {
|
||||
expect(hasClarifyRequest('session-a')).toBe(false)
|
||||
expect(look).toBe('#0000ff')
|
||||
})
|
||||
|
||||
it('does not notify subscribers when a staged patch changes nothing', () => {
|
||||
const preview = () => undefined
|
||||
const listener = vi.fn()
|
||||
|
||||
stageSetupChoose('req-a', { labels: ACCENTS, preview })
|
||||
const before = $setupChooseStages.get()
|
||||
const unlisten = $setupChooseStages.listen(listener)
|
||||
|
||||
stageSetupChoose('req-a', { labels: ACCENTS, preview })
|
||||
unlisten()
|
||||
|
||||
expect($setupChooseStages.get()).toBe(before)
|
||||
expect(listener).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe('normalizeChoices', () => {
|
||||
|
||||
@@ -283,7 +283,19 @@ export const setupChooseStage = (requestId: string): SetupChooseStage =>
|
||||
$setupChooseStages.get()[requestId] ?? EMPTY_SETUP_STAGE
|
||||
|
||||
export function stageSetupChoose(requestId: string, patch: Partial<SetupChooseStage>): void {
|
||||
$setupChooseStages.set({ ...$setupChooseStages.get(), [requestId]: { ...setupChooseStage(requestId), ...patch } })
|
||||
const current = setupChooseStage(requestId)
|
||||
|
||||
// A no-op patch keeps the atom reference so subscribers do not rerender.
|
||||
const unchanged = Object.entries(patch).every(([key, value]) =>
|
||||
// SAFETY: the entries of a Partial<SetupChooseStage> carry only SetupChooseStage keys.
|
||||
Object.is(current[key as keyof SetupChooseStage], value)
|
||||
)
|
||||
|
||||
if (unchanged && requestId in $setupChooseStages.get()) {
|
||||
return
|
||||
}
|
||||
|
||||
$setupChooseStages.set({ ...$setupChooseStages.get(), [requestId]: { ...current, ...patch } })
|
||||
}
|
||||
|
||||
export function commitSetupChoose(requestId: string): void {
|
||||
|
||||
Reference in New Issue
Block a user