({ children, initialState, onChangeAppState }: Props)
| 57 | const HasAppStateContext = React.createContext<boolean>(false); |
| 58 | |
| 59 | export function AppStateProvider({ children, initialState, onChangeAppState }: Props): React.ReactNode { |
| 60 | // Don't allow nested AppStateProviders. |
| 61 | const hasAppStateContext = useContext(HasAppStateContext); |
| 62 | if (hasAppStateContext) { |
| 63 | throw new Error('AppStateProvider can not be nested within another AppStateProvider'); |
| 64 | } |
| 65 | |
| 66 | // Store is created once and never changes -- stable context value means |
| 67 | // the provider never triggers re-renders. Consumers subscribe to slices |
| 68 | // via useSyncExternalStore in useAppState(selector). |
| 69 | const [store] = useState(() => createStore<AppState>(initialState ?? getDefaultAppState(), onChangeAppState)); |
| 70 | |
| 71 | // Check on mount if bypass mode should be disabled |
| 72 | // This handles the race condition where remote settings load BEFORE this component mounts, |
| 73 | // meaning the settings change notification was sent when no listeners were subscribed. |
| 74 | // On subsequent sessions, the cached remote-settings.json is read during initial setup, |
| 75 | // but on the first session the remote fetch may complete before React mounts. |
| 76 | useEffect(() => { |
| 77 | const { toolPermissionContext } = store.getState(); |
| 78 | if (toolPermissionContext.isBypassPermissionsModeAvailable && isBypassPermissionsModeDisabled()) { |
| 79 | logForDebugging('Disabling bypass permissions mode on mount (remote settings loaded before mount)'); |
| 80 | store.setState(prev => ({ |
| 81 | ...prev, |
| 82 | toolPermissionContext: createDisabledBypassPermissionsContext(prev.toolPermissionContext), |
| 83 | })); |
| 84 | } |
| 85 | }, []); |
| 86 | |
| 87 | // Listen for external settings changes and sync to AppState. |
| 88 | // This ensures file watcher changes propagate through the app -- |
| 89 | // shared with the headless/SDK path via applySettingsChange. |
| 90 | const onSettingsChange = useEffectEvent((source: SettingSource) => applySettingsChange(source, store.setState)); |
| 91 | useSettingsChange(onSettingsChange); |
| 92 | |
| 93 | return ( |
| 94 | <HasAppStateContext.Provider value={true}> |
| 95 | <AppStoreContext.Provider value={store}> |
| 96 | <MailboxProvider> |
| 97 | <VoiceProvider>{children}</VoiceProvider> |
| 98 | </MailboxProvider> |
| 99 | </AppStoreContext.Provider> |
| 100 | </HasAppStateContext.Provider> |
| 101 | ); |
| 102 | } |
| 103 | |
| 104 | function useAppStore(): AppStateStore { |
| 105 | // eslint-disable-next-line react-hooks/rules-of-hooks |
nothing calls this directly
no test coverage detected