diff --git a/cli/src/api/apiMachine.ts b/cli/src/api/apiMachine.ts index 24fbf89d..110044f4 100644 --- a/cli/src/api/apiMachine.ts +++ b/cli/src/api/apiMachine.ts @@ -102,7 +102,7 @@ export class ApiMachineClient { setRPCHandlers({ spawnSession, stopSession, requestShutdown }: MachineRpcHandlers): void { this.rpcHandlerManager.registerHandler('spawn-happy-session', async (params: any) => { - const { directory, sessionId, resumeSessionId, machineId, approvedNewDirectoryCreation, agent, model, yolo, token, sessionType, worktreeName } = params || {} + const { directory, sessionId, resumeSessionId, machineId, approvedNewDirectoryCreation, agent, model, modelReasoningEffort, yolo, token, sessionType, worktreeName } = params || {} if (!directory) { throw new Error('Directory is required') @@ -116,6 +116,7 @@ export class ApiMachineClient { approvedNewDirectoryCreation, agent, model, + modelReasoningEffort, yolo, token, sessionType, diff --git a/cli/src/codex/loop.ts b/cli/src/codex/loop.ts index bff31774..5860d0c0 100644 --- a/cli/src/codex/loop.ts +++ b/cli/src/codex/loop.ts @@ -14,6 +14,7 @@ export interface EnhancedMode { permissionMode: PermissionMode; model?: string; collaborationMode: CodexCollaborationMode; + modelReasoningEffort?: string; } interface LoopOptions { diff --git a/cli/src/codex/runCodex.ts b/cli/src/codex/runCodex.ts index 55fd1c56..3708159e 100644 --- a/cli/src/codex/runCodex.ts +++ b/cli/src/codex/runCodex.ts @@ -21,6 +21,7 @@ export async function runCodex(opts: { permissionMode?: PermissionMode; resumeSessionId?: string; model?: string; + modelReasoningEffort?: string; }): Promise { const workingDirectory = getInvokedCwd(); const startedBy = opts.startedBy ?? 'terminal'; @@ -45,6 +46,7 @@ export async function runCodex(opts: { const messageQueue = new MessageQueue2((mode) => hashObject({ permissionMode: mode.permissionMode, model: mode.model, + modelReasoningEffort: mode.modelReasoningEffort, collaborationMode: mode.collaborationMode })); @@ -53,6 +55,7 @@ export async function runCodex(opts: { let currentPermissionMode: PermissionMode = opts.permissionMode ?? 'default'; let currentModel = opts.model; + const currentModelReasoningEffort = opts.modelReasoningEffort; let currentCollaborationMode: EnhancedMode['collaborationMode'] = 'default'; const lifecycle = createRunnerLifecycle({ @@ -105,6 +108,7 @@ export async function runCodex(opts: { const enhancedMode: EnhancedMode = { permissionMode: messagePermissionMode ?? 'default', model: currentModel, + modelReasoningEffort: currentModelReasoningEffort, collaborationMode: currentCollaborationMode }; const formattedText = formatMessageWithAttachments(message.content.text, message.content.attachments); diff --git a/cli/src/codex/utils/appServerConfig.test.ts b/cli/src/codex/utils/appServerConfig.test.ts index 7930728d..de651439 100644 --- a/cli/src/codex/utils/appServerConfig.test.ts +++ b/cli/src/codex/utils/appServerConfig.test.ts @@ -64,6 +64,22 @@ describe('appServerConfig', () => { }); }); + it('passes model reasoning effort via thread config', () => { + const params = buildThreadStartParams({ + mode: { permissionMode: 'default', modelReasoningEffort: 'xhigh', collaborationMode: 'default' }, + mcpServers + }); + + expect(params.config).toEqual({ + 'mcp_servers.hapi': { + command: 'node', + args: ['mcp'] + }, + developer_instructions: codexSystemPrompt, + model_reasoning_effort: 'xhigh' + }); + }); + it('builds turn params with mode defaults', () => { const params = buildTurnStartParams({ threadId: 'thread-1', diff --git a/cli/src/codex/utils/appServerConfig.ts b/cli/src/codex/utils/appServerConfig.ts index 00eb1165..439966ee 100644 --- a/cli/src/codex/utils/appServerConfig.ts +++ b/cli/src/codex/utils/appServerConfig.ts @@ -84,7 +84,8 @@ export function buildThreadStartParams(args: { } = resolveInstructions(args); const configWithInstructions = { ...config, - developer_instructions: resolvedDeveloperInstructions + developer_instructions: resolvedDeveloperInstructions, + ...(args.mode.modelReasoningEffort ? { model_reasoning_effort: args.mode.modelReasoningEffort } : {}) }; const params: ThreadStartParams = { diff --git a/cli/src/commands/codex.ts b/cli/src/commands/codex.ts index 15ec84f9..0848c85f 100644 --- a/cli/src/commands/codex.ts +++ b/cli/src/commands/codex.ts @@ -18,6 +18,7 @@ export const codexCommand: CommandDefinition = { permissionMode?: CodexPermissionMode resumeSessionId?: string model?: string + modelReasoningEffort?: string } = {} const unknownArgs: string[] = [] @@ -44,6 +45,12 @@ export const codexCommand: CommandDefinition = { } options.model = model unknownArgs.push('--model', model) + } else if (arg === '--model-reasoning-effort') { + const effort = commandArgs[++i] + if (!effort) { + throw new Error('Missing --model-reasoning-effort value') + } + options.modelReasoningEffort = effort } else { unknownArgs.push(arg) } diff --git a/cli/src/modules/common/rpcTypes.ts b/cli/src/modules/common/rpcTypes.ts index 15a3602c..c3a8d034 100644 --- a/cli/src/modules/common/rpcTypes.ts +++ b/cli/src/modules/common/rpcTypes.ts @@ -6,6 +6,7 @@ export interface SpawnSessionOptions { approvedNewDirectoryCreation?: boolean agent?: 'claude' | 'codex' | 'cursor' | 'gemini' | 'opencode' model?: string + modelReasoningEffort?: string yolo?: boolean token?: string sessionType?: 'simple' | 'worktree' diff --git a/cli/src/runner/run.ts b/cli/src/runner/run.ts index 058d2743..a87eda69 100644 --- a/cli/src/runner/run.ts +++ b/cli/src/runner/run.ts @@ -361,6 +361,9 @@ export async function startRunner(): Promise { if (options.model && agent !== 'opencode') { args.push('--model', options.model); } + if (options.modelReasoningEffort && agent === 'codex') { + args.push('--model-reasoning-effort', options.modelReasoningEffort); + } if (yolo) { args.push('--yolo'); } diff --git a/hub/src/sync/rpcGateway.ts b/hub/src/sync/rpcGateway.ts index ca508636..a2fce60f 100644 --- a/hub/src/sync/rpcGateway.ts +++ b/hub/src/sync/rpcGateway.ts @@ -109,6 +109,7 @@ export class RpcGateway { directory: string, agent: 'claude' | 'codex' | 'cursor' | 'gemini' | 'opencode' = 'claude', model?: string, + modelReasoningEffort?: string, yolo?: boolean, sessionType?: 'simple' | 'worktree', worktreeName?: string, @@ -118,7 +119,7 @@ export class RpcGateway { const result = await this.machineRpc( machineId, 'spawn-happy-session', - { type: 'spawn-in-directory', directory, agent, model, yolo, sessionType, worktreeName, resumeSessionId } + { type: 'spawn-in-directory', directory, agent, model, modelReasoningEffort, yolo, sessionType, worktreeName, resumeSessionId } ) if (result && typeof result === 'object') { const obj = result as Record diff --git a/hub/src/sync/syncEngine.ts b/hub/src/sync/syncEngine.ts index 063e441a..6e159d6d 100644 --- a/hub/src/sync/syncEngine.ts +++ b/hub/src/sync/syncEngine.ts @@ -310,12 +310,13 @@ export class SyncEngine { directory: string, agent: 'claude' | 'codex' | 'cursor' | 'gemini' | 'opencode' = 'claude', model?: string, + modelReasoningEffort?: string, yolo?: boolean, sessionType?: 'simple' | 'worktree', worktreeName?: string, resumeSessionId?: string ): Promise<{ type: 'success'; sessionId: string } | { type: 'error'; message: string }> { - return await this.rpcGateway.spawnSession(machineId, directory, agent, model, yolo, sessionType, worktreeName, resumeSessionId) + return await this.rpcGateway.spawnSession(machineId, directory, agent, model, modelReasoningEffort, yolo, sessionType, worktreeName, resumeSessionId) } async resumeSession(sessionId: string, namespace: string): Promise { diff --git a/hub/src/web/routes/machines.ts b/hub/src/web/routes/machines.ts index 626c5f95..cbc410af 100644 --- a/hub/src/web/routes/machines.ts +++ b/hub/src/web/routes/machines.ts @@ -8,6 +8,7 @@ const spawnBodySchema = z.object({ directory: z.string().min(1), agent: z.enum(['claude', 'codex', 'cursor', 'gemini', 'opencode']).optional(), model: z.string().optional(), + modelReasoningEffort: z.string().optional(), yolo: z.boolean().optional(), sessionType: z.enum(['simple', 'worktree']).optional(), worktreeName: z.string().optional() @@ -54,6 +55,7 @@ export function createMachinesRoutes(getSyncEngine: () => SyncEngine | null): Ho parsed.data.directory, parsed.data.agent, parsed.data.model, + parsed.data.modelReasoningEffort, parsed.data.yolo, parsed.data.sessionType, parsed.data.worktreeName diff --git a/web/src/api/client.ts b/web/src/api/client.ts index 065c6597..9c54ab07 100644 --- a/web/src/api/client.ts +++ b/web/src/api/client.ts @@ -381,13 +381,14 @@ export class ApiClient { directory: string, agent?: 'claude' | 'codex' | 'cursor' | 'gemini' | 'opencode', model?: string, + modelReasoningEffort?: string, yolo?: boolean, sessionType?: 'simple' | 'worktree', worktreeName?: string ): Promise { return await this.request(`/api/machines/${encodeURIComponent(machineId)}/spawn`, { method: 'POST', - body: JSON.stringify({ directory, agent, model, yolo, sessionType, worktreeName }) + body: JSON.stringify({ directory, agent, model, modelReasoningEffort, yolo, sessionType, worktreeName }) }) } diff --git a/web/src/components/NewSession/ReasoningEffortSelector.tsx b/web/src/components/NewSession/ReasoningEffortSelector.tsx new file mode 100644 index 00000000..aea9f435 --- /dev/null +++ b/web/src/components/NewSession/ReasoningEffortSelector.tsx @@ -0,0 +1,37 @@ +import type { AgentType, CodexReasoningEffort } from './types' +import { CODEX_REASONING_EFFORT_OPTIONS } from './types' +import { useTranslation } from '@/lib/use-translation' + +export function ReasoningEffortSelector(props: { + agent: AgentType + value: CodexReasoningEffort + isDisabled: boolean + onChange: (value: CodexReasoningEffort) => void +}) { + const { t } = useTranslation() + + if (props.agent !== 'codex') { + return null + } + + return ( +
+ + +
+ ) +} diff --git a/web/src/components/NewSession/index.tsx b/web/src/components/NewSession/index.tsx index 60efd325..407999cf 100644 --- a/web/src/components/NewSession/index.tsx +++ b/web/src/components/NewSession/index.tsx @@ -7,12 +7,13 @@ import { useSessions } from '@/hooks/queries/useSessions' import { useActiveSuggestions, type Suggestion } from '@/hooks/useActiveSuggestions' import { useDirectorySuggestions } from '@/hooks/useDirectorySuggestions' import { useRecentPaths } from '@/hooks/useRecentPaths' -import type { AgentType, SessionType } from './types' +import type { AgentType, CodexReasoningEffort, SessionType } from './types' import { ActionButtons } from './ActionButtons' import { AgentSelector } from './AgentSelector' import { DirectorySection } from './DirectorySection' import { MachineSelector } from './MachineSelector' import { ModelSelector } from './ModelSelector' +import { ReasoningEffortSelector } from './ReasoningEffortSelector' import { loadPreferredAgent, loadPreferredYoloMode, @@ -43,6 +44,7 @@ export function NewSession(props: { const [pathExistence, setPathExistence] = useState>({}) const [agent, setAgent] = useState(loadPreferredAgent) const [model, setModel] = useState('auto') + const [modelReasoningEffort, setModelReasoningEffort] = useState('default') const [yoloMode, setYoloMode] = useState(loadPreferredYoloMode) const [sessionType, setSessionType] = useState('simple') const [worktreeName, setWorktreeName] = useState('') @@ -220,11 +222,15 @@ export function NewSession(props: { setError(null) try { const resolvedModel = model !== 'auto' && agent !== 'opencode' ? model : undefined + const resolvedModelReasoningEffort = agent === 'codex' && modelReasoningEffort !== 'default' + ? modelReasoningEffort + : undefined const result = await spawnSession({ machineId, directory: directory.trim(), agent, model: resolvedModel, + modelReasoningEffort: resolvedModelReasoningEffort, yolo: yoloMode, sessionType, worktreeName: sessionType === 'worktree' ? (worktreeName.trim() || undefined) : undefined @@ -294,6 +300,12 @@ export function NewSession(props: { isDisabled={isFormDisabled} onModelChange={setModel} /> + = { claude: [ @@ -27,3 +28,11 @@ export const MODEL_OPTIONS: Record