156 lines
4.7 KiB
TypeScript
156 lines
4.7 KiB
TypeScript
import { feature } from 'bun:bundle'
|
|
import { useEffect, useRef } from 'react'
|
|
import {
|
|
type AppState,
|
|
useAppState,
|
|
useAppStateStore,
|
|
useSetAppState,
|
|
} from 'src/state/AppState.js'
|
|
import type { ToolPermissionContext } from 'src/Tool.js'
|
|
import { getIsRemoteMode } from '../../bootstrap/state.js'
|
|
import {
|
|
createDisabledBypassPermissionsContext,
|
|
shouldDisableBypassPermissions,
|
|
verifyAutoModeGateAccess,
|
|
} from './permissionSetup.js'
|
|
|
|
let bypassPermissionsCheckRan = false
|
|
|
|
export async function checkAndDisableBypassPermissionsIfNeeded(
|
|
toolPermissionContext: ToolPermissionContext,
|
|
setAppState: (f: (prev: AppState) => AppState) => void,
|
|
): Promise<void> {
|
|
// Check if bypassPermissions should be disabled based on Statsig gate
|
|
// Do this only once, before the first query, to ensure we have the latest gate value
|
|
if (bypassPermissionsCheckRan) {
|
|
return
|
|
}
|
|
bypassPermissionsCheckRan = true
|
|
|
|
if (!toolPermissionContext.isBypassPermissionsModeAvailable) {
|
|
return
|
|
}
|
|
|
|
const shouldDisable = await shouldDisableBypassPermissions()
|
|
if (!shouldDisable) {
|
|
return
|
|
}
|
|
|
|
setAppState(prev => {
|
|
return {
|
|
...prev,
|
|
toolPermissionContext: createDisabledBypassPermissionsContext(
|
|
prev.toolPermissionContext,
|
|
),
|
|
}
|
|
})
|
|
}
|
|
|
|
/**
|
|
* Reset the run-once flag for checkAndDisableBypassPermissionsIfNeeded.
|
|
* Call this after /login so the gate check re-runs with the new org.
|
|
*/
|
|
export function resetBypassPermissionsCheck(): void {
|
|
bypassPermissionsCheckRan = false
|
|
}
|
|
|
|
export function useKickOffCheckAndDisableBypassPermissionsIfNeeded(): void {
|
|
const toolPermissionContext = useAppState(s => s.toolPermissionContext)
|
|
const setAppState = useSetAppState()
|
|
|
|
// Run once, when the component mounts
|
|
useEffect(() => {
|
|
if (getIsRemoteMode()) return
|
|
void checkAndDisableBypassPermissionsIfNeeded(
|
|
toolPermissionContext,
|
|
setAppState,
|
|
)
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, [])
|
|
}
|
|
|
|
let autoModeCheckRan = false
|
|
|
|
export async function checkAndDisableAutoModeIfNeeded(
|
|
toolPermissionContext: ToolPermissionContext,
|
|
setAppState: (f: (prev: AppState) => AppState) => void,
|
|
fastMode?: boolean,
|
|
): Promise<void> {
|
|
if (feature('TRANSCRIPT_CLASSIFIER')) {
|
|
if (autoModeCheckRan) {
|
|
return
|
|
}
|
|
autoModeCheckRan = true
|
|
|
|
const { updateContext, notification } = await verifyAutoModeGateAccess(
|
|
toolPermissionContext,
|
|
fastMode,
|
|
)
|
|
setAppState(prev => {
|
|
// Apply the transform to CURRENT context, not the stale snapshot we
|
|
// passed to verifyAutoModeGateAccess. The async GrowthBook await inside
|
|
// can be outrun by a mid-turn shift-tab; spreading a stale context here
|
|
// would revert the user's mode change.
|
|
const nextCtx = updateContext(prev.toolPermissionContext)
|
|
const newState =
|
|
nextCtx === prev.toolPermissionContext
|
|
? prev
|
|
: { ...prev, toolPermissionContext: nextCtx }
|
|
if (!notification) return newState
|
|
return {
|
|
...newState,
|
|
notifications: {
|
|
...newState.notifications,
|
|
queue: [
|
|
...newState.notifications.queue,
|
|
{
|
|
key: 'auto-mode-gate-notification',
|
|
text: notification,
|
|
color: 'warning' as const,
|
|
priority: 'high' as const,
|
|
},
|
|
],
|
|
},
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Reset the run-once flag for checkAndDisableAutoModeIfNeeded.
|
|
* Call this after /login so the gate check re-runs with the new org.
|
|
*/
|
|
export function resetAutoModeGateCheck(): void {
|
|
autoModeCheckRan = false
|
|
}
|
|
|
|
export function useKickOffCheckAndDisableAutoModeIfNeeded(): void {
|
|
const mainLoopModel = useAppState(s => s.mainLoopModel)
|
|
const mainLoopModelForSession = useAppState(s => s.mainLoopModelForSession)
|
|
const fastMode = useAppState(s => s.fastMode)
|
|
const setAppState = useSetAppState()
|
|
const store = useAppStateStore()
|
|
const isFirstRunRef = useRef(true)
|
|
|
|
// Runs on mount (startup check) AND whenever the model or fast mode changes
|
|
// (kick-out / carousel-restore). Watching both model fields covers /model,
|
|
// Cmd+P picker, /config, and bridge onSetModel paths; fastMode covers
|
|
// /fast on|off for the tengu_auto_mode_config.disableFastMode circuit
|
|
// breaker. The print.ts headless paths are covered by the sync
|
|
// isAutoModeGateEnabled() check.
|
|
useEffect(() => {
|
|
if (getIsRemoteMode()) return
|
|
if (isFirstRunRef.current) {
|
|
isFirstRunRef.current = false
|
|
} else {
|
|
resetAutoModeGateCheck()
|
|
}
|
|
void checkAndDisableAutoModeIfNeeded(
|
|
store.getState().toolPermissionContext,
|
|
setAppState,
|
|
fastMode,
|
|
)
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, [mainLoopModel, mainLoopModelForSession, fastMode])
|
|
}
|