perf(ui-tui): narrow overlay subscriptions to focused selectors

Subscribe overlay components to computed theme/session selectors instead of the full UI store so unrelated UI state updates trigger fewer overlay renders.
This commit is contained in:
Brooklyn Nicholson 2026-05-05 15:30:27 -05:00 committed by Teknium
parent ee502e5640
commit 00d25595c1
2 changed files with 27 additions and 23 deletions

View file

@ -1,4 +1,4 @@
import { atom } from 'nanostores' import { atom, computed } from 'nanostores'
import { MOUSE_TRACKING } from '../config/env.js' import { MOUSE_TRACKING } from '../config/env.js'
import { ZERO } from '../domain/usage.js' import { ZERO } from '../domain/usage.js'
@ -30,6 +30,9 @@ const buildUiState = (): UiState => ({
export const $uiState = atom<UiState>(buildUiState()) export const $uiState = atom<UiState>(buildUiState())
export const $uiTheme = computed($uiState, state => state.theme)
export const $uiSessionId = computed($uiState, state => state.sid)
export const getUiState = () => $uiState.get() export const getUiState = () => $uiState.get()
export const patchUiState = (next: Partial<UiState> | ((state: UiState) => UiState)) => export const patchUiState = (next: Partial<UiState> | ((state: UiState) => UiState)) =>

View file

@ -4,7 +4,7 @@ import { useStore } from '@nanostores/react'
import { useGateway } from '../app/gatewayContext.js' import { useGateway } from '../app/gatewayContext.js'
import type { AppOverlaysProps } from '../app/interfaces.js' import type { AppOverlaysProps } from '../app/interfaces.js'
import { $overlayState, patchOverlayState } from '../app/overlayStore.js' import { $overlayState, patchOverlayState } from '../app/overlayStore.js'
import { $uiState } from '../app/uiStore.js' import { $uiSessionId, $uiTheme } from '../app/uiStore.js'
import { FloatBox } from './appChrome.js' import { FloatBox } from './appChrome.js'
import { MaskedPrompt } from './maskedPrompt.js' import { MaskedPrompt } from './maskedPrompt.js'
@ -24,12 +24,12 @@ export function PromptZone({
onSudoSubmit onSudoSubmit
}: Pick<AppOverlaysProps, 'cols' | 'onApprovalChoice' | 'onClarifyAnswer' | 'onSecretSubmit' | 'onSudoSubmit'>) { }: Pick<AppOverlaysProps, 'cols' | 'onApprovalChoice' | 'onClarifyAnswer' | 'onSecretSubmit' | 'onSudoSubmit'>) {
const overlay = useStore($overlayState) const overlay = useStore($overlayState)
const ui = useStore($uiState) const theme = useStore($uiTheme)
if (overlay.approval) { if (overlay.approval) {
return ( return (
<Box flexDirection="column" flexShrink={0} paddingX={1} paddingY={1}> <Box flexDirection="column" flexShrink={0} paddingX={1} paddingY={1}>
<ApprovalPrompt onChoice={onApprovalChoice} req={overlay.approval} t={ui.theme} /> <ApprovalPrompt onChoice={onApprovalChoice} req={overlay.approval} t={theme} />
</Box> </Box>
) )
} }
@ -46,7 +46,7 @@ export function PromptZone({
return ( return (
<Box flexDirection="column" flexShrink={0} paddingX={1} paddingY={1}> <Box flexDirection="column" flexShrink={0} paddingX={1} paddingY={1}>
<ConfirmPrompt onCancel={onCancel} onConfirm={onConfirm} req={req} t={ui.theme} /> <ConfirmPrompt onCancel={onCancel} onConfirm={onConfirm} req={req} t={theme} />
</Box> </Box>
) )
} }
@ -59,7 +59,7 @@ export function PromptZone({
onAnswer={onClarifyAnswer} onAnswer={onClarifyAnswer}
onCancel={() => onClarifyAnswer('')} onCancel={() => onClarifyAnswer('')}
req={overlay.clarify} req={overlay.clarify}
t={ui.theme} t={theme}
/> />
</Box> </Box>
) )
@ -68,7 +68,7 @@ export function PromptZone({
if (overlay.sudo) { if (overlay.sudo) {
return ( return (
<Box flexDirection="column" flexShrink={0} paddingX={1} paddingY={1}> <Box flexDirection="column" flexShrink={0} paddingX={1} paddingY={1}>
<MaskedPrompt cols={cols} icon="🔐" label="sudo password required" onSubmit={onSudoSubmit} t={ui.theme} /> <MaskedPrompt cols={cols} icon="🔐" label="sudo password required" onSubmit={onSudoSubmit} t={theme} />
</Box> </Box>
) )
} }
@ -82,7 +82,7 @@ export function PromptZone({
label={overlay.secret.prompt} label={overlay.secret.prompt}
onSubmit={onSecretSubmit} onSubmit={onSecretSubmit}
sub={`for ${overlay.secret.envVar}`} sub={`for ${overlay.secret.envVar}`}
t={ui.theme} t={theme}
/> />
</Box> </Box>
) )
@ -101,7 +101,8 @@ export function FloatingOverlays({
}: Pick<AppOverlaysProps, 'cols' | 'compIdx' | 'completions' | 'onModelSelect' | 'onPickerSelect' | 'pagerPageSize'>) { }: Pick<AppOverlaysProps, 'cols' | 'compIdx' | 'completions' | 'onModelSelect' | 'onPickerSelect' | 'pagerPageSize'>) {
const { gw } = useGateway() const { gw } = useGateway()
const overlay = useStore($overlayState) const overlay = useStore($overlayState)
const ui = useStore($uiState) const sid = useStore($uiSessionId)
const theme = useStore($uiTheme)
const hasAny = overlay.modelPicker || overlay.pager || overlay.picker || overlay.skillsHub || completions.length const hasAny = overlay.modelPicker || overlay.pager || overlay.picker || overlay.skillsHub || completions.length
@ -119,40 +120,40 @@ export function FloatingOverlays({
return ( return (
<Box alignItems="flex-start" bottom="100%" flexDirection="column" left={0} position="absolute" right={0}> <Box alignItems="flex-start" bottom="100%" flexDirection="column" left={0} position="absolute" right={0}>
{overlay.picker && ( {overlay.picker && (
<FloatBox color={ui.theme.color.border}> <FloatBox color={theme.color.border}>
<SessionPicker <SessionPicker
gw={gw} gw={gw}
onCancel={() => patchOverlayState({ picker: false })} onCancel={() => patchOverlayState({ picker: false })}
onSelect={onPickerSelect} onSelect={onPickerSelect}
t={ui.theme} t={theme}
/> />
</FloatBox> </FloatBox>
)} )}
{overlay.modelPicker && ( {overlay.modelPicker && (
<FloatBox color={ui.theme.color.border}> <FloatBox color={theme.color.border}>
<ModelPicker <ModelPicker
gw={gw} gw={gw}
onCancel={() => patchOverlayState({ modelPicker: false })} onCancel={() => patchOverlayState({ modelPicker: false })}
onSelect={onModelSelect} onSelect={onModelSelect}
sessionId={ui.sid} sessionId={sid}
t={ui.theme} t={theme}
/> />
</FloatBox> </FloatBox>
)} )}
{overlay.skillsHub && ( {overlay.skillsHub && (
<FloatBox color={ui.theme.color.border}> <FloatBox color={theme.color.border}>
<SkillsHub gw={gw} onClose={() => patchOverlayState({ skillsHub: false })} t={ui.theme} /> <SkillsHub gw={gw} onClose={() => patchOverlayState({ skillsHub: false })} t={theme} />
</FloatBox> </FloatBox>
)} )}
{overlay.pager && ( {overlay.pager && (
<FloatBox color={ui.theme.color.border}> <FloatBox color={theme.color.border}>
<Box flexDirection="column" paddingX={1} paddingY={1}> <Box flexDirection="column" paddingX={1} paddingY={1}>
{overlay.pager.title && ( {overlay.pager.title && (
<Box justifyContent="center" marginBottom={1}> <Box justifyContent="center" marginBottom={1}>
<Text bold color={ui.theme.color.primary}> <Text bold color={theme.color.primary}>
{overlay.pager.title} {overlay.pager.title}
</Text> </Text>
</Box> </Box>
@ -163,7 +164,7 @@ export function FloatingOverlays({
))} ))}
<Box marginTop={1}> <Box marginTop={1}>
<OverlayHint t={ui.theme}> <OverlayHint t={theme}>
{overlay.pager.offset + pagerPageSize < overlay.pager.lines.length {overlay.pager.offset + pagerPageSize < overlay.pager.lines.length
? `↑↓/jk line · Enter/Space/PgDn page · b/PgUp back · g/G top/bottom · Esc/q close (${Math.min(overlay.pager.offset + pagerPageSize, overlay.pager.lines.length)}/${overlay.pager.lines.length})` ? `↑↓/jk line · Enter/Space/PgDn page · b/PgUp back · g/G top/bottom · Esc/q close (${Math.min(overlay.pager.offset + pagerPageSize, overlay.pager.lines.length)}/${overlay.pager.lines.length})`
: `end · ↑↓/jk · b/PgUp back · g top · Esc/q close (${overlay.pager.lines.length} lines)`} : `end · ↑↓/jk · b/PgUp back · g top · Esc/q close (${overlay.pager.lines.length} lines)`}
@ -174,23 +175,23 @@ export function FloatingOverlays({
)} )}
{!!completions.length && ( {!!completions.length && (
<FloatBox color={ui.theme.color.primary}> <FloatBox color={theme.color.primary}>
<Box flexDirection="column" width={Math.max(28, cols - 6)}> <Box flexDirection="column" width={Math.max(28, cols - 6)}>
{completions.slice(start, start + viewportSize).map((item, i) => { {completions.slice(start, start + viewportSize).map((item, i) => {
const active = start + i === compIdx const active = start + i === compIdx
return ( return (
<Box <Box
backgroundColor={active ? ui.theme.color.completionCurrentBg : undefined} backgroundColor={active ? theme.color.completionCurrentBg : undefined}
flexDirection="row" flexDirection="row"
key={`${start + i}:${item.text}:${item.display}:${item.meta ?? ''}`} key={`${start + i}:${item.text}:${item.display}:${item.meta ?? ''}`}
width="100%" width="100%"
> >
<Text bold color={ui.theme.color.label}> <Text bold color={theme.color.label}>
{' '} {' '}
{item.display} {item.display}
</Text> </Text>
{item.meta ? <Text color={ui.theme.color.muted}> {item.meta}</Text> : null} {item.meta ? <Text color={theme.color.muted}> {item.meta}</Text> : null}
</Box> </Box>
) )
})} })}