diff --git a/src/state/AppState.tsx b/src/state/AppState.tsx
index 5a58ca29b..e89cdba70 100644
--- a/src/state/AppState.tsx
+++ b/src/state/AppState.tsx
@@ -1,30 +1,36 @@
-import { feature } from 'bun:bundle'
-import React, {
- useContext,
- useEffect,
- useEffectEvent,
- useState,
- useSyncExternalStore,
-} from 'react'
-import { MailboxProvider } from '../context/mailbox.js'
-import { VoiceProvider as VoiceProviderValue } from '../context/voice.js'
-import { useSettingsChange } from '../hooks/useSettingsChange.js'
-import { logForDebugging } from '../utils/debug.js'
+import { feature } from 'bun:bundle';
+import React, { useContext, useEffect, useEffectEvent, useState, useSyncExternalStore } from 'react';
+import { MailboxProvider } from '../context/mailbox.js';
+import { VoiceProvider as VoiceProviderValue } from '../context/voice.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'
+} 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';
-const VoiceProvider: (props: { children: React.ReactNode }) => React.ReactNode =
- feature('VOICE_MODE') ? VoiceProviderValue : ({ children }) => children
-import {
- type AppState,
- type AppStateStore,
- getDefaultAppState,
-} from './AppStateStore.js'
+// DCE: voice context is ant-only. External builds get a noop provider that
+// still wraps children in VoiceContext so useVoiceState never throws.
+const VoiceProvider: (props: { children: React.ReactNode }) => React.ReactNode = feature('VOICE_MODE')
+ ? VoiceProviderValue
+ : (() => {
+ 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}
+ );
+ })();
+
+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
@@ -37,40 +43,29 @@ export {
IDLE_SPECULATION_STATE,
type SpeculationResult,
type SpeculationState,
-} from './AppStateStore.js'
+} from './AppStateStore.js';
-export const AppStoreContext = React.createContext(null)
+export const AppStoreContext = React.createContext(null);
type Props = {
- children: React.ReactNode
- initialState?: AppState
- onChangeAppState?: (args: { newState: AppState; oldState: AppState }) => void
-}
+ children: React.ReactNode;
+ initialState?: AppState;
+ onChangeAppState?: (args: { newState: AppState; oldState: AppState }) => void;
+};
-const HasAppStateContext = React.createContext(false)
+const HasAppStateContext = React.createContext(false);
-export function AppStateProvider({
- children,
- initialState,
- onChangeAppState,
-}: Props): React.ReactNode {
+export function AppStateProvider({ children, initialState, onChangeAppState }: Props): React.ReactNode {
// Don't allow nested AppStateProviders.
- const hasAppStateContext = useContext(HasAppStateContext)
+ const hasAppStateContext = useContext(HasAppStateContext);
if (hasAppStateContext) {
- throw new Error(
- 'AppStateProvider can not be nested within another AppStateProvider',
- )
+ 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,
- ),
- )
+ 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,
@@ -78,31 +73,22 @@ export function AppStateProvider({
// 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)',
- )
+ 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,
- ),
- }))
+ toolPermissionContext: createDisabledBypassPermissionsContext(prev.toolPermissionContext),
+ }));
}
// biome-ignore lint/correctness/useExhaustiveDependencies: intentional mount-only effect
- }, [])
+ }, []);
// 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)
+ const onSettingsChange = useEffectEvent((source: SettingSource) => applySettingsChange(source, store.setState));
+ useSettingsChange(onSettingsChange);
return (
@@ -112,18 +98,16 @@ export function AppStateProvider({
- )
+ );
}
function useAppStore(): AppStateStore {
// eslint-disable-next-line react-hooks/rules-of-hooks
- const store = useContext(AppStoreContext)
+ const store = useContext(AppStoreContext);
if (!store) {
- throw new ReferenceError(
- 'useAppState/useSetAppState cannot be called outside of an ',
- )
+ throw new ReferenceError('useAppState/useSetAppState cannot be called outside of an ');
}
- return store
+ return store;
}
/**
@@ -143,22 +127,22 @@ function useAppStore(): AppStateStore {
* ```
*/
export function useAppState(selector: (state: AppState) => T): T {
- const store = useAppStore()
+ const store = useAppStore();
const get = () => {
- const state = store.getState()
- const selected = selector(state)
+ 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 selected;
+ };
- return useSyncExternalStore(store.subscribe, get, get)
+ return useSyncExternalStore(store.subscribe, get, get);
}
/**
@@ -166,30 +150,26 @@ export function useAppState(selector: (state: AppState) => T): T {
* 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
+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()
+ return useAppStore();
}
-const NOOP_SUBSCRIBE = () => () => {}
+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)
+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,
- )
+ );
}