import { feature } from 'bun:bundle'; import React, { useContext, useEffect, useEffectEvent, useState, useSyncExternalStore } from 'react'; import { MailboxProvider } from '../context/mailbox.js'; import { useSettingsChange } from '../hooks/useSettingsChange.js'; import { logForDebugging } from '../utils/debug.js'; import { createDisabledBypassPermissionsContext, isBypassPermissionsModeDisabled, } from '../utils/permissions/permissionSetup.js'; import { applySettingsChange } from '../utils/settings/applySettingsChange.js'; import type { SettingSource } from '../utils/settings/constants.js'; import { createStore } from './store.js'; // DCE: voice context is ant-only. External builds get a noop provider that // still wraps children in VoiceContext so useVoiceState never throws. /* eslint-disable @typescript-eslint/no-require-imports */ const VoiceProvider: (props: { children: React.ReactNode }) => React.ReactNode = feature('VOICE_MODE') ? require('../context/voice.js').VoiceProvider : (() => { const { VoiceContext } = require('../context/voice.js'); const noopStore = createStore({ voiceState: 'idle' as const, voiceError: null as string | null, voiceInterimTranscript: '', voiceAudioLevels: [] as number[], voiceWarmingUp: false, }); return ({ children }: { children: React.ReactNode }) => ( {children} ); })(); /* eslint-enable @typescript-eslint/no-require-imports */ import { type AppState, type AppStateStore, getDefaultAppState } from './AppStateStore.js'; // TODO: Remove these re-exports once all callers import directly from // ./AppStateStore.js. Kept for back-compat during migration so .ts callers // can incrementally move off the .tsx import and stop pulling React. export { type AppState, type AppStateStore, type CompletionBoundary, getDefaultAppState, IDLE_SPECULATION_STATE, type SpeculationResult, type SpeculationState, } from './AppStateStore.js'; export const AppStoreContext = React.createContext(null); type Props = { children: React.ReactNode; initialState?: AppState; onChangeAppState?: (args: { newState: AppState; oldState: AppState }) => void; }; const HasAppStateContext = React.createContext(false); export function AppStateProvider({ children, initialState, onChangeAppState }: Props): React.ReactNode { // Don't allow nested AppStateProviders. const hasAppStateContext = useContext(HasAppStateContext); if (hasAppStateContext) { throw new Error('AppStateProvider can not be nested within another AppStateProvider'); } // Store is created once and never changes -- stable context value means // the provider never triggers re-renders. Consumers subscribe to slices // via useSyncExternalStore in useAppState(selector). const [store] = useState(() => createStore(initialState ?? getDefaultAppState(), onChangeAppState)); // Check on mount if bypass mode should be disabled // This handles the race condition where remote settings load BEFORE this component mounts, // meaning the settings change notification was sent when no listeners were subscribed. // On subsequent sessions, the cached remote-settings.json is read during initial setup, // but on the first session the remote fetch may complete before React mounts. useEffect(() => { const { toolPermissionContext } = store.getState(); if (toolPermissionContext.isBypassPermissionsModeAvailable && isBypassPermissionsModeDisabled()) { logForDebugging('Disabling bypass permissions mode on mount (remote settings loaded before mount)'); store.setState(prev => ({ ...prev, toolPermissionContext: createDisabledBypassPermissionsContext(prev.toolPermissionContext), })); } }, []); // Listen for external settings changes and sync to AppState. // This ensures file watcher changes propagate through the app -- // shared with the headless/SDK path via applySettingsChange. const onSettingsChange = useEffectEvent((source: SettingSource) => applySettingsChange(source, store.setState)); useSettingsChange(onSettingsChange); return ( {children} ); } function useAppStore(): AppStateStore { // eslint-disable-next-line react-hooks/rules-of-hooks const store = useContext(AppStoreContext); if (!store) { throw new ReferenceError('useAppState/useSetAppState cannot be called outside of an '); } return store; } /** * Subscribe to a slice of AppState. Only re-renders when the selected value * changes (compared via Object.is). * * For multiple independent fields, call the hook multiple times: * ``` * const verbose = useAppState(s => s.verbose) * const model = useAppState(s => s.mainLoopModel) * ``` * * Do NOT return new objects from the selector -- Object.is will always see * them as changed. Instead, select an existing sub-object reference: * ``` * const { text, promptId } = useAppState(s => s.promptSuggestion) // good * ``` */ export function useAppState(selector: (state: AppState) => T): T { const store = useAppStore(); const get = () => { const state = store.getState(); const selected = selector(state); if (process.env.USER_TYPE === 'ant' && state === selected) { throw new Error( `Your selector in \`useAppState(${selector.toString()})\` returned the original state, which is not allowed. You must instead return a property for optimised rendering.`, ); } return selected; }; return useSyncExternalStore(store.subscribe, get, get); } /** * Get the setAppState updater without subscribing to any state. * Returns a stable reference that never changes -- components using only * this hook will never re-render from state changes. */ export function useSetAppState(): (updater: (prev: AppState) => AppState) => void { return useAppStore().setState; } /** * Get the store directly (for passing getState/setState to non-React code). */ export function useAppStateStore(): AppStateStore { return useAppStore(); } const NOOP_SUBSCRIBE = () => () => {}; /** * Safe version of useAppState that returns undefined if called outside of AppStateProvider. * Useful for components that may be rendered in contexts where AppStateProvider isn't available. */ export function useAppStateMaybeOutsideOfProvider(selector: (state: AppState) => T): T | undefined { const store = useContext(AppStoreContext); return useSyncExternalStore(store ? store.subscribe : NOOP_SUBSCRIBE, () => store ? selector(store.getState()) : undefined, ); }