import { CODEX_PERMISSION_MODES } from '@hapi/protocol/modes'; import type { CodexPermissionMode } from '@hapi/protocol/types'; import type { ReasoningEffort } from '../appServerTypes'; import type { CodexPersonality, EnhancedMode } from '../loop'; import type { SlashCommand } from '@/modules/common/slashCommands'; import { parseReasoningEffortValue } from './reasoningEffort'; export const MAX_CODEX_GOAL_OBJECTIVE_CHARS = 4_000; const CODEX_PERSONALITIES = ['friendly', 'pragmatic', 'none'] as const satisfies readonly CodexPersonality[]; const UNSUPPORTED_CODEX_BUILTIN_COMMANDS = new Set([ 'compat', 'diff', 'init', 'login', 'logout', 'mcp', 'new', 'prompts', 'quit', 'redo', 'review', 'undo' ]); export type CodexSlashResolution = | { kind: 'passthrough' } | { kind: 'handled'; message: string; updates?: { collaborationMode?: EnhancedMode['collaborationMode']; permissionMode?: CodexPermissionMode; model?: string | null; modelReasoningEffort?: ReasoningEffort | null; serviceTier?: string | null; proactiveMultiAgent?: boolean; personality?: CodexPersonality; }; } | { kind: 'replace'; text: string; message?: string; updates?: { collaborationMode?: EnhancedMode['collaborationMode']; permissionMode?: CodexPermissionMode; model?: string | null; modelReasoningEffort?: ReasoningEffort | null; serviceTier?: string | null; proactiveMultiAgent?: boolean; personality?: CodexPersonality; }; } | { kind: 'goal'; action: 'show' | 'set' | 'pause' | 'resume' | 'clear'; objective?: string; message?: string; }; export function resolveCodexSlashCommand( text: string, state: { commands?: readonly SlashCommand[]; permissionMode: CodexPermissionMode; collaborationMode: EnhancedMode['collaborationMode']; model?: string; modelReasoningEffort?: ReasoningEffort; serviceTier?: string | null; proactiveMultiAgent?: boolean; personality?: CodexPersonality; } ): CodexSlashResolution { const match = /^\s*\/([a-z0-9:_-]+)(?:\s+([\s\S]*))?$/i.exec(text); if (!match) return { kind: 'passthrough' }; const command = match[1]?.toLowerCase(); const rest = match[2]?.trim() ?? ''; if (!command) return { kind: 'passthrough' }; const custom = state.commands?.find((candidate) => candidate.source !== 'builtin' && candidate.name.toLowerCase() === command ); if (custom?.content) { return { kind: 'replace', text: rest ? `${custom.content}\n\nUser arguments: ${rest}` : custom.content, message: `Expanded /${custom.name}` }; } if (command === 'plan') { const lowerRest = rest.toLowerCase(); if (lowerRest === 'off' || lowerRest === 'default' || lowerRest === 'exit' || lowerRest === 'disable') { return { kind: 'handled', message: 'Codex plan mode disabled', updates: { collaborationMode: 'default' } }; } if (rest) { return { kind: 'replace', text: rest, message: 'Codex plan mode enabled', updates: { collaborationMode: 'plan' } }; } return { kind: 'handled', message: 'Codex plan mode enabled', updates: { collaborationMode: 'plan' } }; } if (command === 'agent') { const value = rest.toLowerCase(); if (value === 'status') { return { kind: 'handled', message: `Codex proactive multi-agent mode: ${state.proactiveMultiAgent ? 'on' : 'off'}` }; } if (value && !['on', 'enable', 'enabled', 'off', 'disable', 'disabled'].includes(value)) { return { kind: 'handled', message: 'Usage: /agent [on|off|status]' }; } const enabled = value ? ['on', 'enable', 'enabled'].includes(value) : !state.proactiveMultiAgent; return { kind: 'handled', message: `Codex proactive multi-agent mode ${enabled ? 'enabled' : 'disabled'}`, updates: { proactiveMultiAgent: enabled } }; } if (command === 'goal') { const lowerRest = rest.toLowerCase(); if (!rest) { return { kind: 'goal', action: 'show' }; } if (lowerRest === 'clear') { return { kind: 'goal', action: 'clear' }; } if (lowerRest === 'pause') { return { kind: 'goal', action: 'pause' }; } if (lowerRest === 'resume') { return { kind: 'goal', action: 'resume' }; } const objective = rest.trim(); if (!objective) { return { kind: 'handled', message: 'Goal objective must not be empty.' }; } if ([...objective].length > MAX_CODEX_GOAL_OBJECTIVE_CHARS) { return { kind: 'handled', message: `Goal objective must be at most ${MAX_CODEX_GOAL_OBJECTIVE_CHARS} characters.` }; } return { kind: 'goal', action: 'set', objective }; } if (command === 'default' || command === 'execute') { return { kind: 'handled', message: 'Codex collaboration mode: default', updates: { collaborationMode: 'default' } }; } if (command === 'status') { return { kind: 'handled', message: [ '**Codex status**', '', `- permission: \`${state.permissionMode}\``, `- collaboration: \`${state.collaborationMode}\``, `- model: \`${state.model ?? 'auto'}\``, `- reasoning: \`${state.modelReasoningEffort ?? 'default'}\``, `- personality: \`${state.personality ?? 'unset'}\`` ].join('\n') }; } if (command === 'personality') { if (!rest) { return { kind: 'handled', message: `Codex personality: ${state.personality ?? 'unset (Codex config / thread sticky)'}` }; } // turn/start.personality sticks for subsequent turns; omitting later does not // restore config.toml. Only explicit friendly|pragmatic|none are valid. if (rest === 'default' || rest === 'auto' || rest === 'clear') { return { kind: 'handled', message: 'Codex personality is sticky on the thread; set friendly, pragmatic, or none (cannot restore config.toml by clearing)' }; } if (!(CODEX_PERSONALITIES as readonly string[]).includes(rest)) { return { kind: 'handled', message: `Unknown Codex personality: ${rest}` }; } return { kind: 'handled', message: `Codex personality set to ${rest}`, updates: { personality: rest as CodexPersonality } }; } if (command === 'model') { if (!rest) { return { kind: 'handled', message: `Codex model: ${state.model ?? 'auto'}` }; } const model = rest === 'auto' ? null : rest; return { kind: 'handled', message: `Codex model set to ${model ?? 'auto'}`, updates: { model } }; } if (command === 'reasoning' || command === 'effort') { if (!rest) { return { kind: 'handled', message: `Codex reasoning effort: ${state.modelReasoningEffort ?? 'default'}` }; } if (rest === 'default' || rest === 'auto') { return { kind: 'handled', message: 'Codex reasoning effort set to default', updates: { modelReasoningEffort: null } }; } const effort = parseReasoningEffortValue(rest); return { kind: 'handled', message: `Codex reasoning effort set to ${effort}`, updates: { modelReasoningEffort: effort } }; } if (command === 'fast') { const arg = rest.toLowerCase(); if (arg === '' || arg === 'on') { return { kind: 'handled', message: 'Codex Fast mode enabled', updates: { serviceTier: 'fast' } }; } if (arg === 'off') { return { kind: 'handled', message: 'Codex Fast mode disabled', updates: { serviceTier: 'standard' } }; } if (arg === 'status') { const on = state.serviceTier === 'fast'; return { kind: 'handled', message: `Codex Fast mode: ${on ? 'on' : 'off'}` }; } return { kind: 'handled', message: 'Usage: /fast [on|off|status]' }; } if (command === 'permissions' || command === 'permission') { if (!rest) { return { kind: 'handled', message: `Codex permission mode: ${state.permissionMode}` }; } if (!(CODEX_PERMISSION_MODES as readonly string[]).includes(rest)) { return { kind: 'handled', message: `Unknown Codex permission mode: ${rest}` }; } return { kind: 'handled', message: `Codex permission mode set to ${rest}`, updates: { permissionMode: rest as CodexPermissionMode } }; } if (command === 'help') { return { kind: 'handled', message: [ '**Supported Codex slash commands**', '', '- `/plan [prompt]` — enable plan mode, optionally send prompt', '- `/plan off` — return to default mode', '- `/goal [objective]` — set or view the persistent goal', '- `/goal pause|resume|clear` — update the current goal', '- `/clear` — reset current Codex thread context', '- `/compact` — compact current Codex thread context', '- `/status` — show current Codex session config', '- `/model [name|auto]` — show or set model', '- `/reasoning [level|default]` — show or set reasoning effort', '- `/personality [friendly|pragmatic|none]` — show or set response style (sticky on thread)', '- `/fast [on|off|status]` — toggle Fast mode (GPT-5.5 / GPT-5.4, ChatGPT login)', '- `/permissions [default|read-only|safe-yolo|yolo]` — show or set permission mode', '', 'Custom `/commands` from `.codex/prompts` are expanded before sending.' ].join('\n') }; } if (UNSUPPORTED_CODEX_BUILTIN_COMMANDS.has(command)) { return { kind: 'handled', message: `/${command} is a Codex CLI command that is not supported in HAPI sessions yet.` }; } return { kind: 'passthrough' }; }