feat: add reasoning effort support for Codex models

- Add ReasoningEffortSelector component for UI selection
- Integrate reasoning effort in feature creation/editing dialogs
- Add reasoning effort support to phase model selector
- Update agent service and board actions to handle reasoning effort
- Add reasoning effort fields to feature and settings types
- Update model selector and agent info panel with reasoning effort display
- Enhance agent context parser for reasoning effort processing

Reasoning effort allows fine-tuned control over Codex model reasoning
capabilities, providing options from 'none' to 'xhigh' for different
task complexity requirements.
This commit is contained in:
DhanushSantosh
2026-01-08 20:43:36 +05:30
parent 8a9715adef
commit 4dcf54146c
13 changed files with 361 additions and 59 deletions

View File

@@ -6,7 +6,7 @@
import path from 'path';
import * as secureFs from '../lib/secure-fs.js';
import type { EventEmitter } from '../lib/events.js';
import type { ExecuteOptions, ThinkingLevel } from '@automaker/types';
import type { ExecuteOptions, ThinkingLevel, ReasoningEffort } from '@automaker/types';
import {
readImageAsBase64,
buildPromptWithImages,
@@ -56,6 +56,7 @@ interface Session {
workingDirectory: string;
model?: string;
thinkingLevel?: ThinkingLevel; // Thinking level for Claude models
reasoningEffort?: ReasoningEffort; // Reasoning effort for Codex models
sdkSessionId?: string; // Claude SDK session ID for conversation continuity
promptQueue: QueuedPrompt[]; // Queue of prompts to auto-run after current task
}
@@ -145,6 +146,7 @@ export class AgentService {
imagePaths,
model,
thinkingLevel,
reasoningEffort,
}: {
sessionId: string;
message: string;
@@ -152,6 +154,7 @@ export class AgentService {
imagePaths?: string[];
model?: string;
thinkingLevel?: ThinkingLevel;
reasoningEffort?: ReasoningEffort;
}) {
const session = this.sessions.get(sessionId);
if (!session) {
@@ -164,7 +167,7 @@ export class AgentService {
throw new Error('Agent is already processing a message');
}
// Update session model and thinking level if provided
// Update session model, thinking level, and reasoning effort if provided
if (model) {
session.model = model;
await this.updateSession(sessionId, { model });
@@ -172,6 +175,9 @@ export class AgentService {
if (thinkingLevel !== undefined) {
session.thinkingLevel = thinkingLevel;
}
if (reasoningEffort !== undefined) {
session.reasoningEffort = reasoningEffort;
}
// Validate vision support before processing images
const effectiveModel = model || session.model;
@@ -265,8 +271,9 @@ export class AgentService {
: baseSystemPrompt;
// Build SDK options using centralized configuration
// Use thinking level from request, or fall back to session's stored thinking level
// Use thinking level and reasoning effort from request, or fall back to session's stored values
const effectiveThinkingLevel = thinkingLevel ?? session.thinkingLevel;
const effectiveReasoningEffort = reasoningEffort ?? session.reasoningEffort;
const sdkOptions = createChatOptions({
cwd: effectiveWorkDir,
model: model,
@@ -299,6 +306,8 @@ export class AgentService {
settingSources: sdkOptions.settingSources,
sdkSessionId: session.sdkSessionId, // Pass SDK session ID for resuming
mcpServers: Object.keys(mcpServers).length > 0 ? mcpServers : undefined, // Pass MCP servers configuration
thinkingLevel: effectiveThinkingLevel, // Pass thinking level for Claude models
reasoningEffort: effectiveReasoningEffort, // Pass reasoning effort for Codex models
};
// Build prompt content with images

View File

@@ -1,6 +1,8 @@
// @ts-nocheck
import { useEffect, useState } from 'react';
import { Feature, ThinkingLevel, useAppStore } from '@/store/app-store';
import type { ReasoningEffort } from '@automaker/types';
import { getProviderFromModel } from '@/lib/utils';
import {
AgentTaskInfo,
parseAgentContext,
@@ -37,6 +39,22 @@ function formatThinkingLevel(level: ThinkingLevel | undefined): string {
return labels[level];
}
/**
* Formats reasoning effort for compact display
*/
function formatReasoningEffort(effort: ReasoningEffort | undefined): string {
if (!effort || effort === 'none') return '';
const labels: Record<ReasoningEffort, string> = {
none: '',
minimal: 'Min',
low: 'Low',
medium: 'Med',
high: 'High',
xhigh: 'XHigh',
};
return labels[effort];
}
interface AgentInfoPanelProps {
feature: Feature;
contextContent?: string;
@@ -106,6 +124,10 @@ export function AgentInfoPanel({
}, [feature.id, feature.status, contextContent, isCurrentAutoTask]);
// Model/Preset Info for Backlog Cards
if (showAgentInfo && feature.status === 'backlog') {
const provider = getProviderFromModel(feature.model);
const isCodex = provider === 'codex';
const isClaude = provider === 'claude';
return (
<div className="mb-3 space-y-2 overflow-hidden">
<div className="flex items-center gap-2 text-[11px] flex-wrap">
@@ -116,7 +138,7 @@ export function AgentInfoPanel({
})()}
<span className="font-medium">{formatModelName(feature.model ?? DEFAULT_MODEL)}</span>
</div>
{feature.thinkingLevel && feature.thinkingLevel !== 'none' ? (
{isClaude && feature.thinkingLevel && feature.thinkingLevel !== 'none' ? (
<div className="flex items-center gap-1 text-purple-400">
<Brain className="w-3 h-3" />
<span className="font-medium">
@@ -124,6 +146,14 @@ export function AgentInfoPanel({
</span>
</div>
) : null}
{isCodex && feature.reasoningEffort && feature.reasoningEffort !== 'none' ? (
<div className="flex items-center gap-1 text-purple-400">
<Brain className="w-3 h-3" />
<span className="font-medium">
{formatReasoningEffort(feature.reasoningEffort as ReasoningEffort)}
</span>
</div>
) : null}
</div>
</div>
);

View File

@@ -41,9 +41,12 @@ import {
PlanningMode,
Feature,
} from '@/store/app-store';
import type { ReasoningEffort } from '@automaker/types';
import { codexModelHasThinking, supportsReasoningEffort } from '@automaker/types';
import {
ModelSelector,
ThinkingLevelSelector,
ReasoningEffortSelector,
ProfileQuickSelect,
TestingTabContent,
PrioritySelector,
@@ -78,6 +81,7 @@ type FeatureData = {
skipTests: boolean;
model: AgentModel;
thinkingLevel: ThinkingLevel;
reasoningEffort: ReasoningEffort;
branchName: string; // Can be empty string to use current branch
priority: number;
planningMode: PlanningMode;
@@ -134,6 +138,7 @@ export function AddFeatureDialog({
skipTests: false,
model: 'opus' as ModelAlias,
thinkingLevel: 'none' as ThinkingLevel,
reasoningEffort: 'none' as ReasoningEffort,
branchName: '',
priority: 2 as number, // Default to medium priority
});
@@ -220,6 +225,9 @@ export function AddFeatureDialog({
const normalizedThinking = modelSupportsThinking(selectedModel)
? newFeature.thinkingLevel
: 'none';
const normalizedReasoning = supportsReasoningEffort(selectedModel)
? newFeature.reasoningEffort
: 'none';
// Use current branch if toggle is on
// If currentBranch is provided (non-primary worktree), use it
@@ -260,6 +268,7 @@ export function AddFeatureDialog({
skipTests: newFeature.skipTests,
model: selectedModel,
thinkingLevel: normalizedThinking,
reasoningEffort: normalizedReasoning,
branchName: finalBranchName,
priority: newFeature.priority,
planningMode,
@@ -281,6 +290,7 @@ export function AddFeatureDialog({
model: 'opus',
priority: 2,
thinkingLevel: 'none',
reasoningEffort: 'none',
branchName: '',
});
setUseCurrentBranch(true);
@@ -394,6 +404,9 @@ export function AddFeatureDialog({
const newModelAllowsThinking =
!isCurrentModelCursor && modelSupportsThinking(newFeature.model || 'sonnet');
// Codex models that support reasoning effort - show reasoning selector
const newModelAllowsReasoning = supportsReasoningEffort(newFeature.model || '');
return (
<Dialog open={open} onOpenChange={handleDialogClose}>
<DialogContent
@@ -619,6 +632,14 @@ export function AddFeatureDialog({
}
/>
)}
{newModelAllowsReasoning && (
<ReasoningEffortSelector
selectedEffort={newFeature.reasoningEffort}
onEffortSelect={(effort) =>
setNewFeature({ ...newFeature, reasoningEffort: effort })
}
/>
)}
</>
)}
</TabsContent>

View File

@@ -41,9 +41,11 @@ import {
useAppStore,
PlanningMode,
} from '@/store/app-store';
import type { ReasoningEffort } from '@automaker/types';
import {
ModelSelector,
ThinkingLevelSelector,
ReasoningEffortSelector,
ProfileQuickSelect,
TestingTabContent,
PrioritySelector,
@@ -60,7 +62,7 @@ import {
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
import type { DescriptionHistoryEntry } from '@automaker/types';
import { DependencyTreeDialog } from './dependency-tree-dialog';
import { isCursorModel, PROVIDER_PREFIXES } from '@automaker/types';
import { isCursorModel, PROVIDER_PREFIXES, supportsReasoningEffort } from '@automaker/types';
const logger = createLogger('EditFeatureDialog');
@@ -76,6 +78,7 @@ interface EditFeatureDialogProps {
skipTests: boolean;
model: ModelAlias;
thinkingLevel: ThinkingLevel;
reasoningEffort: ReasoningEffort;
imagePaths: DescriptionImagePath[];
textFilePaths: DescriptionTextFilePath[];
branchName: string; // Can be empty string to use current branch
@@ -180,6 +183,9 @@ export function EditFeatureDialog({
const normalizedThinking: ThinkingLevel = modelSupportsThinking(selectedModel)
? (editingFeature.thinkingLevel ?? 'none')
: 'none';
const normalizedReasoning: ReasoningEffort = supportsReasoningEffort(selectedModel)
? (editingFeature.reasoningEffort ?? 'none')
: 'none';
// Use current branch if toggle is on
// If currentBranch is provided (non-primary worktree), use it
@@ -195,6 +201,7 @@ export function EditFeatureDialog({
skipTests: editingFeature.skipTests ?? false,
model: selectedModel,
thinkingLevel: normalizedThinking,
reasoningEffort: normalizedReasoning,
imagePaths: editingFeature.imagePaths ?? [],
textFilePaths: editingFeature.textFilePaths ?? [],
branchName: finalBranchName,
@@ -233,15 +240,17 @@ export function EditFeatureDialog({
if (!editingFeature) return;
// For Cursor models, thinking is handled by the model itself
// For Claude models, check if it supports extended thinking
// For Codex models, use reasoning effort instead
const isCursor = isCursorModel(model);
const supportsThinking = modelSupportsThinking(model);
const supportsReasoning = supportsReasoningEffort(model);
setEditingFeature({
...editingFeature,
model: model as ModelAlias,
thinkingLevel: isCursor
? 'none'
: modelSupportsThinking(model)
? editingFeature.thinkingLevel
: 'none',
thinkingLevel:
isCursor || !supportsThinking ? 'none' : (editingFeature.thinkingLevel ?? 'none'),
reasoningEffort: !supportsReasoning ? 'none' : (editingFeature.reasoningEffort ?? 'none'),
});
};
@@ -312,6 +321,9 @@ export function EditFeatureDialog({
const editModelAllowsThinking =
!isCurrentModelCursor && modelSupportsThinking(editingFeature?.model);
// Codex models that support reasoning effort - show reasoning selector
const editModelAllowsReasoning = supportsReasoningEffort(editingFeature?.model || '');
if (!editingFeature) {
return null;
}
@@ -634,6 +646,18 @@ export function EditFeatureDialog({
testIdPrefix="edit-thinking-level"
/>
)}
{editModelAllowsReasoning && (
<ReasoningEffortSelector
selectedEffort={editingFeature.reasoningEffort ?? 'none'}
onEffortSelect={(effort) =>
setEditingFeature({
...editingFeature,
reasoningEffort: effort,
})
}
testIdPrefix="edit-reasoning-effort"
/>
)}
</>
)}
</TabsContent>

View File

@@ -8,6 +8,7 @@ import {
PlanningMode,
useAppStore,
} from '@/store/app-store';
import type { ReasoningEffort } from '@automaker/types';
import { FeatureImagePath as DescriptionImagePath } from '@/components/ui/description-image-dropzone';
import { getElectronAPI } from '@/lib/electron';
import { toast } from 'sonner';
@@ -222,6 +223,7 @@ export function useBoardActions({
skipTests: boolean;
model: ModelAlias;
thinkingLevel: ThinkingLevel;
reasoningEffort: ReasoningEffort;
imagePaths: DescriptionImagePath[];
branchName: string;
priority: number;

View File

@@ -1,6 +1,7 @@
export * from './model-constants';
export * from './model-selector';
export * from './thinking-level-selector';
export * from './reasoning-effort-selector';
export * from './profile-quick-select';
export * from './profile-select';
export * from './testing-tab-content';

View File

@@ -45,8 +45,8 @@ export function ModelSelector({
// Switch to Cursor's default model (from global settings)
onModelSelect(`${PROVIDER_PREFIXES.cursor}${cursorDefaultModel}`);
} else if (provider === 'codex' && selectedProvider !== 'codex') {
// Switch to Codex's default model (gpt-5.2)
onModelSelect('gpt-5.2');
// Switch to Codex's default model (gpt-5.2-codex)
onModelSelect('gpt-5.2-codex');
} else if (provider === 'claude' && selectedProvider !== 'claude') {
// Switch to Claude's default model
onModelSelect('sonnet');

View File

@@ -0,0 +1,47 @@
import { Label } from '@/components/ui/label';
import { Brain } from 'lucide-react';
import { cn } from '@/lib/utils';
import type { ReasoningEffort } from '@automaker/types';
import { REASONING_EFFORT_LEVELS, REASONING_EFFORT_LABELS } from './model-constants';
interface ReasoningEffortSelectorProps {
selectedEffort: ReasoningEffort;
onEffortSelect: (effort: ReasoningEffort) => void;
testIdPrefix?: string;
}
export function ReasoningEffortSelector({
selectedEffort,
onEffortSelect,
testIdPrefix = 'reasoning-effort',
}: ReasoningEffortSelectorProps) {
return (
<div className="space-y-2 pt-2 border-t border-border">
<Label className="flex items-center gap-2 text-sm">
<Brain className="w-3.5 h-3.5 text-muted-foreground" />
Reasoning Effort
</Label>
<div className="flex gap-2 flex-wrap">
{REASONING_EFFORT_LEVELS.map((effort) => (
<button
key={effort}
type="button"
onClick={() => onEffortSelect(effort)}
className={cn(
'flex-1 px-3 py-2 rounded-md border text-sm font-medium transition-colors min-w-[60px]',
selectedEffort === effort
? 'bg-primary text-primary-foreground border-primary'
: 'bg-background hover:bg-accent border-input'
)}
data-testid={`${testIdPrefix}-${effort}`}
>
{REASONING_EFFORT_LABELS[effort]}
</button>
))}
</div>
<p className="text-xs text-muted-foreground">
Higher efforts give more reasoning tokens for complex problems.
</p>
</div>
);
}

View File

@@ -4,9 +4,11 @@ import { useAppStore } from '@/store/app-store';
import type {
ModelAlias,
CursorModelId,
CodexModelId,
GroupedModel,
PhaseModelEntry,
ThinkingLevel,
ReasoningEffort,
} from '@automaker/types';
import {
stripProviderPrefix,
@@ -15,6 +17,7 @@ import {
isGroupSelected,
getSelectedVariant,
isCursorModel,
codexModelHasThinking,
} from '@automaker/types';
import {
CLAUDE_MODELS,
@@ -22,6 +25,8 @@ import {
CODEX_MODELS,
THINKING_LEVELS,
THINKING_LEVEL_LABELS,
REASONING_EFFORT_LEVELS,
REASONING_EFFORT_LABELS,
} from '@/components/views/board-view/shared/model-constants';
import { Check, ChevronsUpDown, Star, ChevronRight } from 'lucide-react';
import { AnthropicIcon, CursorIcon, OpenAIIcon } from '@/components/ui/provider-icon';
@@ -69,14 +74,17 @@ export function PhaseModelSelector({
const [open, setOpen] = React.useState(false);
const [expandedGroup, setExpandedGroup] = React.useState<string | null>(null);
const [expandedClaudeModel, setExpandedClaudeModel] = React.useState<ModelAlias | null>(null);
const [expandedCodexModel, setExpandedCodexModel] = React.useState<CodexModelId | null>(null);
const commandListRef = React.useRef<HTMLDivElement>(null);
const expandedTriggerRef = React.useRef<HTMLDivElement>(null);
const expandedClaudeTriggerRef = React.useRef<HTMLDivElement>(null);
const expandedCodexTriggerRef = React.useRef<HTMLDivElement>(null);
const { enabledCursorModels, favoriteModels, toggleFavoriteModel } = useAppStore();
// Extract model and thinking level from value
// Extract model and thinking/reasoning levels from value
const selectedModel = value.model;
const selectedThinkingLevel = value.thinkingLevel || 'none';
const selectedReasoningEffort = value.reasoningEffort || 'none';
// Close expanded group when trigger scrolls out of view
React.useEffect(() => {
@@ -124,6 +132,29 @@ export function PhaseModelSelector({
return () => observer.disconnect();
}, [expandedClaudeModel]);
// Close expanded Codex model popover when trigger scrolls out of view
React.useEffect(() => {
const triggerElement = expandedCodexTriggerRef.current;
const listElement = commandListRef.current;
if (!triggerElement || !listElement || !expandedCodexModel) return;
const observer = new IntersectionObserver(
(entries) => {
const entry = entries[0];
if (!entry.isIntersecting) {
setExpandedCodexModel(null);
}
},
{
root: listElement,
threshold: 0.1,
}
);
observer.observe(triggerElement);
return () => observer.disconnect();
}, [expandedCodexModel]);
// Filter Cursor models to only show enabled ones
const availableCursorModels = CURSOR_MODELS.filter((model) => {
const cursorId = stripProviderPrefix(model.id) as CursorModelId;
@@ -241,55 +272,183 @@ export function PhaseModelSelector({
return { favorites: favs, claude: cModels, cursor: curModels, codex: codModels };
}, [favoriteModels, availableCursorModels]);
// Render Codex model item (no thinking level needed)
// Render Codex model item with secondary popover for reasoning effort (only for models that support it)
const renderCodexModelItem = (model: (typeof CODEX_MODELS)[0]) => {
const isSelected = selectedModel === model.id;
const isFavorite = favoriteModels.includes(model.id);
const hasReasoning = codexModelHasThinking(model.id as CodexModelId);
const isExpanded = expandedCodexModel === model.id;
const currentReasoning = isSelected ? selectedReasoningEffort : 'none';
// If model doesn't support reasoning, render as simple selector (like Cursor models)
if (!hasReasoning) {
return (
<CommandItem
key={model.id}
value={model.label}
onSelect={() => {
onChange({ model: model.id as CodexModelId });
setOpen(false);
}}
className="group flex items-center justify-between py-2"
>
<div className="flex items-center gap-3 overflow-hidden">
<OpenAIIcon
className={cn(
'h-4 w-4 shrink-0',
isSelected ? 'text-primary' : 'text-muted-foreground'
)}
/>
<div className="flex flex-col truncate">
<span className={cn('truncate font-medium', isSelected && 'text-primary')}>
{model.label}
</span>
<span className="truncate text-xs text-muted-foreground">{model.description}</span>
</div>
</div>
<div className="flex items-center gap-1 ml-2">
<Button
variant="ghost"
size="icon"
className={cn(
'h-6 w-6 hover:bg-transparent hover:text-yellow-500 focus:ring-0',
isFavorite
? 'text-yellow-500 opacity-100'
: 'opacity-0 group-hover:opacity-100 text-muted-foreground'
)}
onClick={(e) => {
e.stopPropagation();
toggleFavoriteModel(model.id);
}}
>
<Star className={cn('h-3.5 w-3.5', isFavorite && 'fill-current')} />
</Button>
{isSelected && <Check className="h-4 w-4 text-primary shrink-0" />}
</div>
</CommandItem>
);
}
// Model supports reasoning - show popover with reasoning effort options
return (
<CommandItem
key={model.id}
value={model.label}
onSelect={() => {
onChange({ model: model.id });
setOpen(false);
}}
className="group flex items-center justify-between py-2"
onSelect={() => setExpandedCodexModel(isExpanded ? null : (model.id as CodexModelId))}
className="p-0 data-[selected=true]:bg-transparent"
>
<div className="flex items-center gap-3 overflow-hidden">
<OpenAIIcon
className={cn(
'h-4 w-4 shrink-0',
isSelected ? 'text-primary' : 'text-muted-foreground'
)}
/>
<div className="flex flex-col truncate">
<span className={cn('truncate font-medium', isSelected && 'text-primary')}>
{model.label}
</span>
<span className="truncate text-xs text-muted-foreground">{model.description}</span>
</div>
</div>
<Popover
open={isExpanded}
onOpenChange={(isOpen) => {
if (!isOpen) {
setExpandedCodexModel(null);
}
}}
>
<PopoverTrigger asChild>
<div
ref={isExpanded ? expandedCodexTriggerRef : undefined}
className={cn(
'w-full group flex items-center justify-between py-2 px-2 rounded-sm cursor-pointer',
'hover:bg-accent',
isExpanded && 'bg-accent'
)}
>
<div className="flex items-center gap-3 overflow-hidden">
<OpenAIIcon
className={cn(
'h-4 w-4 shrink-0',
isSelected ? 'text-primary' : 'text-muted-foreground'
)}
/>
<div className="flex flex-col truncate">
<span className={cn('truncate font-medium', isSelected && 'text-primary')}>
{model.label}
</span>
<span className="truncate text-xs text-muted-foreground">
{isSelected && currentReasoning !== 'none'
? `Reasoning: ${REASONING_EFFORT_LABELS[currentReasoning]}`
: model.description}
</span>
</div>
</div>
<div className="flex items-center gap-1 ml-2">
<Button
variant="ghost"
size="icon"
className={cn(
'h-6 w-6 hover:bg-transparent hover:text-yellow-500 focus:ring-0',
isFavorite
? 'text-yellow-500 opacity-100'
: 'opacity-0 group-hover:opacity-100 text-muted-foreground'
)}
onClick={(e) => {
e.stopPropagation();
toggleFavoriteModel(model.id);
}}
<div className="flex items-center gap-1 ml-2">
<Button
variant="ghost"
size="icon"
className={cn(
'h-6 w-6 hover:bg-transparent hover:text-yellow-500 focus:ring-0',
isFavorite
? 'text-yellow-500 opacity-100'
: 'opacity-0 group-hover:opacity-100 text-muted-foreground'
)}
onClick={(e) => {
e.stopPropagation();
toggleFavoriteModel(model.id);
}}
>
<Star className={cn('h-3.5 w-3.5', isFavorite && 'fill-current')} />
</Button>
{isSelected && <Check className="h-4 w-4 text-primary shrink-0" />}
<ChevronRight
className={cn(
'h-4 w-4 text-muted-foreground transition-transform',
isExpanded && 'rotate-90'
)}
/>
</div>
</div>
</PopoverTrigger>
<PopoverContent
side="right"
align="start"
className="w-[220px] p-1"
sideOffset={8}
collisionPadding={16}
onCloseAutoFocus={(e) => e.preventDefault()}
>
<Star className={cn('h-3.5 w-3.5', isFavorite && 'fill-current')} />
</Button>
{isSelected && <Check className="h-4 w-4 text-primary shrink-0" />}
</div>
<div className="space-y-1">
<div className="px-2 py-1.5 text-xs font-medium text-muted-foreground border-b border-border/50 mb-1">
Reasoning Effort
</div>
{REASONING_EFFORT_LEVELS.map((effort) => (
<button
key={effort}
onClick={() => {
onChange({
model: model.id as CodexModelId,
reasoningEffort: effort,
});
setExpandedCodexModel(null);
setOpen(false);
}}
className={cn(
'w-full flex items-center justify-between px-2 py-2 rounded-sm text-sm',
'hover:bg-accent cursor-pointer transition-colors',
isSelected && currentReasoning === effort && 'bg-accent text-accent-foreground'
)}
>
<div className="flex flex-col items-start">
<span className="font-medium">{REASONING_EFFORT_LABELS[effort]}</span>
<span className="text-xs text-muted-foreground">
{effort === 'none' && 'No reasoning capability'}
{effort === 'minimal' && 'Minimal reasoning'}
{effort === 'low' && 'Light reasoning'}
{effort === 'medium' && 'Moderate reasoning'}
{effort === 'high' && 'Deep reasoning'}
{effort === 'xhigh' && 'Maximum reasoning'}
</span>
</div>
{isSelected && currentReasoning === effort && (
<Check className="h-3.5 w-3.5 text-primary" />
)}
</button>
))}
</div>
</PopoverContent>
</Popover>
</CommandItem>
);
};

View File

@@ -38,12 +38,13 @@ export function formatModelName(model: string): string {
if (model.includes('sonnet')) return 'Sonnet 4.5';
if (model.includes('haiku')) return 'Haiku 4.5';
// Codex/GPT models
// Codex/GPT models - specific formatting
if (model === 'gpt-5.2-codex') return 'GPT-5.2 Codex';
if (model === 'gpt-5.2') return 'GPT-5.2';
if (model === 'gpt-5.1-codex-max') return 'GPT-5.1 Max';
if (model === 'gpt-5.1-codex') return 'GPT-5.1 Codex';
if (model === 'gpt-5.1-codex-mini') return 'GPT-5.1 Mini';
if (model === 'gpt-5.1') return 'GPT-5.1';
// Generic fallbacks for other GPT models
if (model.startsWith('gpt-')) return model.toUpperCase();
if (model.match(/^o\d/)) return model.toUpperCase(); // o1, o3, etc.

View File

@@ -9,13 +9,14 @@ export function cn(...inputs: ClassValue[]) {
/**
* Determine if the current model supports extended thinking controls
* Note: This is for Claude's "thinking levels" only, not Codex's "reasoning effort"
*/
export function modelSupportsThinking(_model?: ModelAlias | string): boolean {
if (!_model) return true;
// Check if it's a Codex model with thinking support
// Codex models don't support Claude thinking levels - they use reasoning effort instead
if (_model.startsWith('gpt-') && _model in CODEX_MODEL_CONFIG_MAP) {
return codexModelHasThinking(_model as any);
return false;
}
// All Claude models support thinking

View File

@@ -3,6 +3,7 @@
*/
import type { PlanningMode, ThinkingLevel } from './settings.js';
import type { ReasoningEffort } from './provider.js';
/**
* A single entry in the description history
@@ -49,6 +50,7 @@ export interface Feature {
branchName?: string; // Name of the feature branch (undefined = use current worktree)
skipTests?: boolean;
thinkingLevel?: ThinkingLevel;
reasoningEffort?: ReasoningEffort;
planningMode?: PlanningMode;
requirePlanApproval?: boolean;
planSpec?: {

View File

@@ -11,6 +11,7 @@ import type { CursorModelId } from './cursor-models.js';
import { CURSOR_MODEL_MAP, getAllCursorModelIds } from './cursor-models.js';
import type { PromptCustomization } from './prompts.js';
import type { CodexSandboxMode, CodexApprovalPolicy } from './codex.js';
import type { ReasoningEffort } from './provider.js';
// Re-export ModelAlias for convenience
export type { ModelAlias };
@@ -108,14 +109,18 @@ const DEFAULT_CODEX_ADDITIONAL_DIRS: string[] = [];
/**
* PhaseModelEntry - Configuration for a single phase model
*
* Encapsulates both the model selection and optional thinking level
* for Claude models. Cursor models handle thinking internally.
* Encapsulates the model selection and optional reasoning/thinking capabilities:
* - Claude models: Use thinkingLevel for extended thinking
* - Codex models: Use reasoningEffort for reasoning intensity
* - Cursor models: Handle thinking internally
*/
export interface PhaseModelEntry {
/** The model to use (Claude alias or Cursor model ID) */
model: ModelAlias | CursorModelId;
/** The model to use (Claude alias, Cursor model ID, or Codex model ID) */
model: ModelAlias | CursorModelId | CodexModelId;
/** Extended thinking level (only applies to Claude models, defaults to 'none') */
thinkingLevel?: ThinkingLevel;
/** Reasoning effort level (only applies to Codex models, defaults to 'none') */
reasoningEffort?: ReasoningEffort;
}
/**