fix: 修复 voice provider 的问题
This commit is contained in:
parent
be76720400
commit
1dfe909301
|
|
@ -1,30 +1,36 @@
|
||||||
import { feature } from 'bun:bundle'
|
import { feature } from 'bun:bundle';
|
||||||
import React, {
|
import React, { useContext, useEffect, useEffectEvent, useState, useSyncExternalStore } from 'react';
|
||||||
useContext,
|
import { MailboxProvider } from '../context/mailbox.js';
|
||||||
useEffect,
|
import { VoiceProvider as VoiceProviderValue } from '../context/voice.js';
|
||||||
useEffectEvent,
|
import { useSettingsChange } from '../hooks/useSettingsChange.js';
|
||||||
useState,
|
import { logForDebugging } from '../utils/debug.js';
|
||||||
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 {
|
import {
|
||||||
createDisabledBypassPermissionsContext,
|
createDisabledBypassPermissionsContext,
|
||||||
isBypassPermissionsModeDisabled,
|
isBypassPermissionsModeDisabled,
|
||||||
} from '../utils/permissions/permissionSetup.js'
|
} from '../utils/permissions/permissionSetup.js';
|
||||||
import { applySettingsChange } from '../utils/settings/applySettingsChange.js'
|
import { applySettingsChange } from '../utils/settings/applySettingsChange.js';
|
||||||
import type { SettingSource } from '../utils/settings/constants.js'
|
import type { SettingSource } from '../utils/settings/constants.js';
|
||||||
import { createStore } from './store.js'
|
import { createStore } from './store.js';
|
||||||
|
|
||||||
const VoiceProvider: (props: { children: React.ReactNode }) => React.ReactNode =
|
// DCE: voice context is ant-only. External builds get a noop provider that
|
||||||
feature('VOICE_MODE') ? VoiceProviderValue : ({ children }) => children
|
// still wraps children in VoiceContext so useVoiceState never throws.
|
||||||
import {
|
const VoiceProvider: (props: { children: React.ReactNode }) => React.ReactNode = feature('VOICE_MODE')
|
||||||
type AppState,
|
? VoiceProviderValue
|
||||||
type AppStateStore,
|
: (() => {
|
||||||
getDefaultAppState,
|
const { VoiceContext } = require('../context/voice.js');
|
||||||
} from './AppStateStore.js'
|
const noopStore = createStore({
|
||||||
|
voiceState: 'idle' as const,
|
||||||
|
voiceError: null as string | null,
|
||||||
|
voiceInterimTranscript: '',
|
||||||
|
voiceAudioLevels: [] as number[],
|
||||||
|
voiceWarmingUp: false,
|
||||||
|
});
|
||||||
|
return ({ children }: { children: React.ReactNode }) => (
|
||||||
|
<VoiceContext.Provider value={noopStore}>{children}</VoiceContext.Provider>
|
||||||
|
);
|
||||||
|
})();
|
||||||
|
|
||||||
|
import { type AppState, type AppStateStore, getDefaultAppState } from './AppStateStore.js';
|
||||||
|
|
||||||
// TODO: Remove these re-exports once all callers import directly from
|
// TODO: Remove these re-exports once all callers import directly from
|
||||||
// ./AppStateStore.js. Kept for back-compat during migration so .ts callers
|
// ./AppStateStore.js. Kept for back-compat during migration so .ts callers
|
||||||
|
|
@ -37,40 +43,29 @@ export {
|
||||||
IDLE_SPECULATION_STATE,
|
IDLE_SPECULATION_STATE,
|
||||||
type SpeculationResult,
|
type SpeculationResult,
|
||||||
type SpeculationState,
|
type SpeculationState,
|
||||||
} from './AppStateStore.js'
|
} from './AppStateStore.js';
|
||||||
|
|
||||||
export const AppStoreContext = React.createContext<AppStateStore | null>(null)
|
export const AppStoreContext = React.createContext<AppStateStore | null>(null);
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
children: React.ReactNode
|
children: React.ReactNode;
|
||||||
initialState?: AppState
|
initialState?: AppState;
|
||||||
onChangeAppState?: (args: { newState: AppState; oldState: AppState }) => void
|
onChangeAppState?: (args: { newState: AppState; oldState: AppState }) => void;
|
||||||
}
|
};
|
||||||
|
|
||||||
const HasAppStateContext = React.createContext<boolean>(false)
|
const HasAppStateContext = React.createContext<boolean>(false);
|
||||||
|
|
||||||
export function AppStateProvider({
|
export function AppStateProvider({ children, initialState, onChangeAppState }: Props): React.ReactNode {
|
||||||
children,
|
|
||||||
initialState,
|
|
||||||
onChangeAppState,
|
|
||||||
}: Props): React.ReactNode {
|
|
||||||
// Don't allow nested AppStateProviders.
|
// Don't allow nested AppStateProviders.
|
||||||
const hasAppStateContext = useContext(HasAppStateContext)
|
const hasAppStateContext = useContext(HasAppStateContext);
|
||||||
if (hasAppStateContext) {
|
if (hasAppStateContext) {
|
||||||
throw new Error(
|
throw new Error('AppStateProvider can not be nested within another AppStateProvider');
|
||||||
'AppStateProvider can not be nested within another AppStateProvider',
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Store is created once and never changes -- stable context value means
|
// Store is created once and never changes -- stable context value means
|
||||||
// the provider never triggers re-renders. Consumers subscribe to slices
|
// the provider never triggers re-renders. Consumers subscribe to slices
|
||||||
// via useSyncExternalStore in useAppState(selector).
|
// via useSyncExternalStore in useAppState(selector).
|
||||||
const [store] = useState(() =>
|
const [store] = useState(() => createStore<AppState>(initialState ?? getDefaultAppState(), onChangeAppState));
|
||||||
createStore<AppState>(
|
|
||||||
initialState ?? getDefaultAppState(),
|
|
||||||
onChangeAppState,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
// Check on mount if bypass mode should be disabled
|
// Check on mount if bypass mode should be disabled
|
||||||
// This handles the race condition where remote settings load BEFORE this component mounts,
|
// 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,
|
// 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.
|
// but on the first session the remote fetch may complete before React mounts.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const { toolPermissionContext } = store.getState()
|
const { toolPermissionContext } = store.getState();
|
||||||
if (
|
if (toolPermissionContext.isBypassPermissionsModeAvailable && isBypassPermissionsModeDisabled()) {
|
||||||
toolPermissionContext.isBypassPermissionsModeAvailable &&
|
logForDebugging('Disabling bypass permissions mode on mount (remote settings loaded before mount)');
|
||||||
isBypassPermissionsModeDisabled()
|
|
||||||
) {
|
|
||||||
logForDebugging(
|
|
||||||
'Disabling bypass permissions mode on mount (remote settings loaded before mount)',
|
|
||||||
)
|
|
||||||
store.setState(prev => ({
|
store.setState(prev => ({
|
||||||
...prev,
|
...prev,
|
||||||
toolPermissionContext: createDisabledBypassPermissionsContext(
|
toolPermissionContext: createDisabledBypassPermissionsContext(prev.toolPermissionContext),
|
||||||
prev.toolPermissionContext,
|
}));
|
||||||
),
|
|
||||||
}))
|
|
||||||
}
|
}
|
||||||
// biome-ignore lint/correctness/useExhaustiveDependencies: intentional mount-only effect
|
// biome-ignore lint/correctness/useExhaustiveDependencies: intentional mount-only effect
|
||||||
}, [])
|
}, []);
|
||||||
|
|
||||||
// Listen for external settings changes and sync to AppState.
|
// Listen for external settings changes and sync to AppState.
|
||||||
// This ensures file watcher changes propagate through the app --
|
// This ensures file watcher changes propagate through the app --
|
||||||
// shared with the headless/SDK path via applySettingsChange.
|
// shared with the headless/SDK path via applySettingsChange.
|
||||||
const onSettingsChange = useEffectEvent((source: SettingSource) =>
|
const onSettingsChange = useEffectEvent((source: SettingSource) => applySettingsChange(source, store.setState));
|
||||||
applySettingsChange(source, store.setState),
|
useSettingsChange(onSettingsChange);
|
||||||
)
|
|
||||||
useSettingsChange(onSettingsChange)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<HasAppStateContext.Provider value={true}>
|
<HasAppStateContext.Provider value={true}>
|
||||||
|
|
@ -112,18 +98,16 @@ export function AppStateProvider({
|
||||||
</MailboxProvider>
|
</MailboxProvider>
|
||||||
</AppStoreContext.Provider>
|
</AppStoreContext.Provider>
|
||||||
</HasAppStateContext.Provider>
|
</HasAppStateContext.Provider>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function useAppStore(): AppStateStore {
|
function useAppStore(): AppStateStore {
|
||||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||||
const store = useContext(AppStoreContext)
|
const store = useContext(AppStoreContext);
|
||||||
if (!store) {
|
if (!store) {
|
||||||
throw new ReferenceError(
|
throw new ReferenceError('useAppState/useSetAppState cannot be called outside of an <AppStateProvider />');
|
||||||
'useAppState/useSetAppState cannot be called outside of an <AppStateProvider />',
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
return store
|
return store;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -143,22 +127,22 @@ function useAppStore(): AppStateStore {
|
||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
export function useAppState<T>(selector: (state: AppState) => T): T {
|
export function useAppState<T>(selector: (state: AppState) => T): T {
|
||||||
const store = useAppStore()
|
const store = useAppStore();
|
||||||
|
|
||||||
const get = () => {
|
const get = () => {
|
||||||
const state = store.getState()
|
const state = store.getState();
|
||||||
const selected = selector(state)
|
const selected = selector(state);
|
||||||
|
|
||||||
if (process.env.USER_TYPE === 'ant' && state === selected) {
|
if (process.env.USER_TYPE === 'ant' && state === selected) {
|
||||||
throw new Error(
|
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.`,
|
`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<T>(selector: (state: AppState) => T): T {
|
||||||
* Returns a stable reference that never changes -- components using only
|
* Returns a stable reference that never changes -- components using only
|
||||||
* this hook will never re-render from state changes.
|
* this hook will never re-render from state changes.
|
||||||
*/
|
*/
|
||||||
export function useSetAppState(): (
|
export function useSetAppState(): (updater: (prev: AppState) => AppState) => void {
|
||||||
updater: (prev: AppState) => AppState,
|
return useAppStore().setState;
|
||||||
) => void {
|
|
||||||
return useAppStore().setState
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the store directly (for passing getState/setState to non-React code).
|
* Get the store directly (for passing getState/setState to non-React code).
|
||||||
*/
|
*/
|
||||||
export function useAppStateStore(): AppStateStore {
|
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.
|
* 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.
|
* Useful for components that may be rendered in contexts where AppStateProvider isn't available.
|
||||||
*/
|
*/
|
||||||
export function useAppStateMaybeOutsideOfProvider<T>(
|
export function useAppStateMaybeOutsideOfProvider<T>(selector: (state: AppState) => T): T | undefined {
|
||||||
selector: (state: AppState) => T,
|
const store = useContext(AppStoreContext);
|
||||||
): T | undefined {
|
|
||||||
const store = useContext(AppStoreContext)
|
|
||||||
return useSyncExternalStore(store ? store.subscribe : NOOP_SUBSCRIBE, () =>
|
return useSyncExternalStore(store ? store.subscribe : NOOP_SUBSCRIBE, () =>
|
||||||
store ? selector(store.getState()) : undefined,
|
store ? selector(store.getState()) : undefined,
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user