From 1dfe909301d4da66e9082d574bfbf06a99363bc4 Mon Sep 17 00:00:00 2001 From: claude-code-best Date: Mon, 18 May 2026 22:54:11 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=20voice=20provider=20?= =?UTF-8?q?=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/state/AppState.tsx | 152 ++++++++++++++++++----------------------- 1 file changed, 66 insertions(+), 86 deletions(-) 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, - ) + ); }