/ utils / permissions / getNextPermissionMode.ts
getNextPermissionMode.ts
  1  import { feature } from 'bun:bundle'
  2  import type { ToolPermissionContext } from '../../Tool.js'
  3  import { logForDebugging } from '../debug.js'
  4  import type { PermissionMode } from './PermissionMode.js'
  5  import {
  6    getAutoModeUnavailableReason,
  7    isAutoModeGateEnabled,
  8    transitionPermissionMode,
  9  } from './permissionSetup.js'
 10  
 11  // Checks both the cached isAutoModeAvailable (set at startup by
 12  // verifyAutoModeGateAccess) and the live isAutoModeGateEnabled() — these can
 13  // diverge if the circuit breaker or settings change mid-session. The
 14  // live check prevents transitionPermissionMode from throwing
 15  // (permissionSetup.ts:~559), which would silently crash the shift+tab handler
 16  // and leave the user stuck at the current mode.
 17  function canCycleToAuto(ctx: ToolPermissionContext): boolean {
 18    if (feature('TRANSCRIPT_CLASSIFIER')) {
 19      const gateEnabled = isAutoModeGateEnabled()
 20      const can = !!ctx.isAutoModeAvailable && gateEnabled
 21      if (!can) {
 22        logForDebugging(
 23          `[auto-mode] canCycleToAuto=false: ctx.isAutoModeAvailable=${ctx.isAutoModeAvailable} isAutoModeGateEnabled=${gateEnabled} reason=${getAutoModeUnavailableReason()}`,
 24        )
 25      }
 26      return can
 27    }
 28    return false
 29  }
 30  
 31  /**
 32   * Determines the next permission mode when cycling through modes with Shift+Tab.
 33   */
 34  export function getNextPermissionMode(
 35    toolPermissionContext: ToolPermissionContext,
 36    _teamContext?: { leadAgentId: string },
 37  ): PermissionMode {
 38    switch (toolPermissionContext.mode) {
 39      case 'default':
 40        // Ants skip acceptEdits and plan — auto mode replaces them
 41        if (process.env.USER_TYPE === 'ant') {
 42          if (toolPermissionContext.isBypassPermissionsModeAvailable) {
 43            return 'bypassPermissions'
 44          }
 45          if (canCycleToAuto(toolPermissionContext)) {
 46            return 'auto'
 47          }
 48          return 'default'
 49        }
 50        return 'acceptEdits'
 51  
 52      case 'acceptEdits':
 53        return 'plan'
 54  
 55      case 'plan':
 56        if (toolPermissionContext.isBypassPermissionsModeAvailable) {
 57          return 'bypassPermissions'
 58        }
 59        if (canCycleToAuto(toolPermissionContext)) {
 60          return 'auto'
 61        }
 62        return 'default'
 63  
 64      case 'bypassPermissions':
 65        if (canCycleToAuto(toolPermissionContext)) {
 66          return 'auto'
 67        }
 68        return 'default'
 69  
 70      case 'dontAsk':
 71        // Not exposed in UI cycle yet, but return default if somehow reached
 72        return 'default'
 73  
 74  
 75      default:
 76        // Covers auto (when TRANSCRIPT_CLASSIFIER is enabled) and any future modes — always fall back to default
 77        return 'default'
 78    }
 79  }
 80  
 81  /**
 82   * Computes the next permission mode and prepares the context for it.
 83   * Handles any context cleanup needed for the target mode (e.g., stripping
 84   * dangerous permissions when entering auto mode).
 85   *
 86   * @returns The next mode and the context to use (with dangerous permissions stripped if needed)
 87   */
 88  export function cyclePermissionMode(
 89    toolPermissionContext: ToolPermissionContext,
 90    teamContext?: { leadAgentId: string },
 91  ): { nextMode: PermissionMode; context: ToolPermissionContext } {
 92    const nextMode = getNextPermissionMode(toolPermissionContext, teamContext)
 93    return {
 94      nextMode,
 95      context: transitionPermissionMode(
 96        toolPermissionContext.mode,
 97        nextMode,
 98        toolPermissionContext,
 99      ),
100    }
101  }