mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-01-31 20:03:37 +00:00
Merge branch 'v0.9.0rc' into opencode-support
This commit is contained in:
@@ -21,8 +21,9 @@ interface ProviderIconDefinition {
|
||||
|
||||
const PROVIDER_ICON_DEFINITIONS: Record<ProviderIconKey, ProviderIconDefinition> = {
|
||||
anthropic: {
|
||||
viewBox: '0 0 24 24',
|
||||
path: 'M17.3041 3.541h-3.6718l6.696 16.918H24Zm-10.6082 0L0 20.459h3.7442l1.3693-3.5527h7.0052l1.3693 3.5528h3.7442L10.5363 3.5409Zm-.3712 10.2232 2.2914-5.9456 2.2914 5.9456Z',
|
||||
viewBox: '0 0 248 248',
|
||||
// Official Claude logo from claude.ai favicon
|
||||
path: 'M52.4285 162.873L98.7844 136.879L99.5485 134.602L98.7844 133.334H96.4921L88.7237 132.862L62.2346 132.153L39.3113 131.207L17.0249 130.026L11.4214 128.844L6.2 121.873L6.7094 118.447L11.4214 115.257L18.171 115.847L33.0711 116.911L55.485 118.447L71.6586 119.392L95.728 121.873H99.5485L100.058 120.337L98.7844 119.392L97.7656 118.447L74.5877 102.732L49.4995 86.1905L36.3823 76.62L29.3779 71.7757L25.8121 67.2858L24.2839 57.3608L30.6515 50.2716L39.3113 50.8623L41.4763 51.4531L50.2636 58.1879L68.9842 72.7209L93.4357 90.6804L97.0015 93.6343L98.4374 92.6652L98.6571 91.9801L97.0015 89.2625L83.757 65.2772L69.621 40.8192L63.2534 30.6579L61.5978 24.632C60.9565 22.1032 60.579 20.0111 60.579 17.4246L67.8381 7.49965L71.9133 6.19995L81.7193 7.49965L85.7946 11.0443L91.9074 24.9865L101.714 46.8451L116.996 76.62L121.453 85.4816L123.873 93.6343L124.764 96.1155H126.292V94.6976L127.566 77.9197L129.858 57.3608L132.15 30.8942L132.915 23.4505L136.608 14.4708L143.994 9.62643L149.725 12.344L154.437 19.0788L153.8 23.4505L150.998 41.6463L145.522 70.1215L141.957 89.2625H143.994L146.414 86.7813L156.093 74.0206L172.266 53.698L179.398 45.6635L187.803 36.802L193.152 32.5484H203.34L210.726 43.6549L207.415 55.1159L196.972 68.3492L188.312 79.5739L175.896 96.2095L168.191 109.585L168.882 110.689L170.738 110.53L198.755 104.504L213.91 101.787L231.994 98.7149L240.144 102.496L241.036 106.395L237.852 114.311L218.495 119.037L195.826 123.645L162.07 131.592L161.696 131.893L162.137 132.547L177.36 133.925L183.855 134.279H199.774L229.447 136.524L237.215 141.605L241.8 147.867L241.036 152.711L229.065 158.737L213.019 154.956L175.45 145.977L162.587 142.787H160.805V143.85L171.502 154.366L191.242 172.089L215.82 195.011L217.094 200.682L213.91 205.172L210.599 204.699L188.949 188.394L180.544 181.069L161.696 165.118H160.422V166.772L164.752 173.152L187.803 207.771L188.949 218.405L187.294 221.832L181.308 223.959L174.813 222.777L161.187 203.754L147.305 182.486L136.098 163.345L134.745 164.2L128.075 235.42L125.019 239.082L117.887 241.8L111.902 237.31L108.718 229.984L111.902 215.452L115.722 196.547L118.779 181.541L121.58 162.873L123.291 156.636L123.14 156.219L121.773 156.449L107.699 175.752L86.304 204.699L69.3663 222.777L65.291 224.431L58.2867 220.768L58.9235 214.27L62.8713 208.48L86.304 178.705L100.44 160.155L109.551 149.507L109.462 147.967L108.959 147.924L46.6977 188.512L35.6182 189.93L30.7788 185.44L31.4156 178.115L33.7079 175.752L52.4285 162.873Z',
|
||||
},
|
||||
openai: {
|
||||
viewBox: '0 0 158.7128 157.296',
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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);
|
||||
@@ -368,11 +378,23 @@ export function AddFeatureDialog({
|
||||
thinkingLevel: 'none', // Cursor handles thinking internally
|
||||
});
|
||||
} else {
|
||||
// Claude profile
|
||||
// Claude profile - ensure model is always set from profile
|
||||
const profileModel = profile.model;
|
||||
if (!profileModel || !['haiku', 'sonnet', 'opus'].includes(profileModel)) {
|
||||
console.warn(
|
||||
`[ProfileSelect] Invalid or missing model "${profileModel}" for profile "${profile.name}", defaulting to sonnet`
|
||||
);
|
||||
}
|
||||
setNewFeature({
|
||||
...newFeature,
|
||||
model: profile.model || 'sonnet',
|
||||
thinkingLevel: profile.thinkingLevel || 'none',
|
||||
model:
|
||||
profileModel && ['haiku', 'sonnet', 'opus'].includes(profileModel)
|
||||
? profileModel
|
||||
: 'sonnet',
|
||||
thinkingLevel:
|
||||
profile.thinkingLevel && profile.thinkingLevel !== 'none'
|
||||
? profile.thinkingLevel
|
||||
: 'none',
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -382,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
|
||||
@@ -607,6 +632,14 @@ export function AddFeatureDialog({
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{newModelAllowsReasoning && (
|
||||
<ReasoningEffortSelector
|
||||
selectedEffort={newFeature.reasoningEffort}
|
||||
onEffortSelect={(effort) =>
|
||||
setNewFeature({ ...newFeature, reasoningEffort: effort })
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</TabsContent>
|
||||
|
||||
@@ -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'),
|
||||
});
|
||||
};
|
||||
|
||||
@@ -256,11 +265,23 @@ export function EditFeatureDialog({
|
||||
thinkingLevel: 'none', // Cursor handles thinking internally
|
||||
});
|
||||
} else {
|
||||
// Claude profile
|
||||
// Claude profile - ensure model is always set from profile
|
||||
const profileModel = profile.model;
|
||||
if (!profileModel || !['haiku', 'sonnet', 'opus'].includes(profileModel)) {
|
||||
console.warn(
|
||||
`[ProfileSelect] Invalid or missing model "${profileModel}" for profile "${profile.name}", defaulting to sonnet`
|
||||
);
|
||||
}
|
||||
setEditingFeature({
|
||||
...editingFeature,
|
||||
model: profile.model || 'sonnet',
|
||||
thinkingLevel: profile.thinkingLevel || 'none',
|
||||
model:
|
||||
profileModel && ['haiku', 'sonnet', 'opus'].includes(profileModel)
|
||||
? profileModel
|
||||
: 'sonnet',
|
||||
thinkingLevel:
|
||||
profile.thinkingLevel && profile.thinkingLevel !== 'none'
|
||||
? profile.thinkingLevel
|
||||
: 'none',
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -300,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;
|
||||
}
|
||||
@@ -622,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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { ModelAlias } from '@/store/app-store';
|
||||
import type { ModelProvider, ThinkingLevel, ReasoningEffort } from '@automaker/types';
|
||||
import { CURSOR_MODEL_MAP, CODEX_MODEL_MAP } from '@automaker/types';
|
||||
import { Brain, Zap, Scale, Cpu, Rocket, Sparkles } from 'lucide-react';
|
||||
import { AnthropicIcon, CursorIcon, OpenAIIcon } from '@/components/ui/provider-icon';
|
||||
|
||||
export type ModelOption = {
|
||||
id: string; // Claude models use ModelAlias, Cursor models use "cursor-{id}"
|
||||
@@ -58,47 +59,39 @@ export const CODEX_MODELS: ModelOption[] = [
|
||||
{
|
||||
id: CODEX_MODEL_MAP.gpt52Codex,
|
||||
label: 'GPT-5.2-Codex',
|
||||
description: 'Most advanced agentic coding model (default for ChatGPT users).',
|
||||
description: 'Most advanced agentic coding model for complex software engineering.',
|
||||
badge: 'Premium',
|
||||
provider: 'codex',
|
||||
hasThinking: true,
|
||||
},
|
||||
{
|
||||
id: CODEX_MODEL_MAP.gpt5Codex,
|
||||
label: 'GPT-5-Codex',
|
||||
description: 'Purpose-built for Codex CLI (default for CLI users).',
|
||||
badge: 'Balanced',
|
||||
id: CODEX_MODEL_MAP.gpt51CodexMax,
|
||||
label: 'GPT-5.1-Codex-Max',
|
||||
description: 'Optimized for long-horizon, agentic coding tasks in Codex.',
|
||||
badge: 'Premium',
|
||||
provider: 'codex',
|
||||
hasThinking: true,
|
||||
},
|
||||
{
|
||||
id: CODEX_MODEL_MAP.gpt5CodexMini,
|
||||
label: 'GPT-5-Codex-Mini',
|
||||
description: 'Faster workflows for code Q&A and editing.',
|
||||
id: CODEX_MODEL_MAP.gpt51CodexMini,
|
||||
label: 'GPT-5.1-Codex-Mini',
|
||||
description: 'Smaller, more cost-effective version for faster workflows.',
|
||||
badge: 'Speed',
|
||||
provider: 'codex',
|
||||
hasThinking: false,
|
||||
},
|
||||
{
|
||||
id: CODEX_MODEL_MAP.codex1,
|
||||
label: 'Codex-1',
|
||||
description: 'o3-based model optimized for software engineering.',
|
||||
badge: 'Premium',
|
||||
id: CODEX_MODEL_MAP.gpt52,
|
||||
label: 'GPT-5.2',
|
||||
description: 'Best general agentic model for tasks across industries and domains.',
|
||||
badge: 'Balanced',
|
||||
provider: 'codex',
|
||||
hasThinking: true,
|
||||
},
|
||||
{
|
||||
id: CODEX_MODEL_MAP.codexMiniLatest,
|
||||
label: 'Codex-Mini-Latest',
|
||||
description: 'o4-mini-based model for faster workflows.',
|
||||
badge: 'Balanced',
|
||||
provider: 'codex',
|
||||
hasThinking: false,
|
||||
},
|
||||
{
|
||||
id: CODEX_MODEL_MAP.gpt5,
|
||||
label: 'GPT-5',
|
||||
description: 'GPT-5 base flagship model.',
|
||||
id: CODEX_MODEL_MAP.gpt51,
|
||||
label: 'GPT-5.1',
|
||||
description: 'Great for coding and agentic tasks across domains.',
|
||||
badge: 'Balanced',
|
||||
provider: 'codex',
|
||||
hasThinking: true,
|
||||
@@ -150,4 +143,7 @@ export const PROFILE_ICONS: Record<string, React.ComponentType<{ className?: str
|
||||
Cpu,
|
||||
Rocket,
|
||||
Sparkles,
|
||||
Anthropic: AnthropicIcon,
|
||||
Cursor: CursorIcon,
|
||||
Codex: OpenAIIcon,
|
||||
};
|
||||
|
||||
@@ -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 (codex-gpt-5.2-codex)
|
||||
onModelSelect('codex-gpt-5.2-codex');
|
||||
} else if (provider === 'claude' && selectedProvider !== 'claude') {
|
||||
// Switch to Claude's default model
|
||||
onModelSelect('sonnet');
|
||||
|
||||
@@ -2,7 +2,12 @@ import { Label } from '@/components/ui/label';
|
||||
import { Brain, UserCircle, Terminal } from 'lucide-react';
|
||||
import { cn } from '@/lib/utils';
|
||||
import type { ModelAlias, ThinkingLevel, AIProfile, CursorModelId } from '@automaker/types';
|
||||
import { CURSOR_MODEL_MAP, profileHasThinking, PROVIDER_PREFIXES } from '@automaker/types';
|
||||
import {
|
||||
CURSOR_MODEL_MAP,
|
||||
profileHasThinking,
|
||||
PROVIDER_PREFIXES,
|
||||
getCodexModelLabel,
|
||||
} from '@automaker/types';
|
||||
import { PROFILE_ICONS } from './model-constants';
|
||||
|
||||
/**
|
||||
@@ -14,6 +19,9 @@ function getProfileModelDisplay(profile: AIProfile): string {
|
||||
const modelConfig = CURSOR_MODEL_MAP[cursorModel];
|
||||
return modelConfig?.label || cursorModel;
|
||||
}
|
||||
if (profile.provider === 'codex') {
|
||||
return getCodexModelLabel(profile.codexModel || 'codex-gpt-5.2-codex');
|
||||
}
|
||||
// Claude
|
||||
return profile.model || 'sonnet';
|
||||
}
|
||||
@@ -26,6 +34,10 @@ function getProfileThinkingDisplay(profile: AIProfile): string | null {
|
||||
// For Cursor, thinking is embedded in the model
|
||||
return profileHasThinking(profile) ? 'thinking' : null;
|
||||
}
|
||||
if (profile.provider === 'codex') {
|
||||
// For Codex, thinking is embedded in the model
|
||||
return profileHasThinking(profile) ? 'thinking' : null;
|
||||
}
|
||||
// Claude
|
||||
return profile.thinkingLevel && profile.thinkingLevel !== 'none' ? profile.thinkingLevel : null;
|
||||
}
|
||||
|
||||
@@ -8,7 +8,12 @@ import {
|
||||
import { Brain, Terminal } from 'lucide-react';
|
||||
import { cn } from '@/lib/utils';
|
||||
import type { ModelAlias, ThinkingLevel, AIProfile, CursorModelId } from '@automaker/types';
|
||||
import { CURSOR_MODEL_MAP, profileHasThinking, PROVIDER_PREFIXES } from '@automaker/types';
|
||||
import {
|
||||
CURSOR_MODEL_MAP,
|
||||
profileHasThinking,
|
||||
PROVIDER_PREFIXES,
|
||||
getCodexModelLabel,
|
||||
} from '@automaker/types';
|
||||
import { PROFILE_ICONS } from './model-constants';
|
||||
|
||||
/**
|
||||
@@ -20,6 +25,9 @@ function getProfileModelDisplay(profile: AIProfile): string {
|
||||
const modelConfig = CURSOR_MODEL_MAP[cursorModel];
|
||||
return modelConfig?.label || cursorModel;
|
||||
}
|
||||
if (profile.provider === 'codex') {
|
||||
return getCodexModelLabel(profile.codexModel || 'codex-gpt-5.2-codex');
|
||||
}
|
||||
// Claude
|
||||
return profile.model || 'sonnet';
|
||||
}
|
||||
@@ -32,6 +40,10 @@ function getProfileThinkingDisplay(profile: AIProfile): string | null {
|
||||
// For Cursor, thinking is embedded in the model
|
||||
return profileHasThinking(profile) ? 'thinking' : null;
|
||||
}
|
||||
if (profile.provider === 'codex') {
|
||||
// For Codex, thinking is embedded in the model
|
||||
return profileHasThinking(profile) ? 'thinking' : null;
|
||||
}
|
||||
// Claude
|
||||
return profile.thinkingLevel && profile.thinkingLevel !== 'none' ? profile.thinkingLevel : null;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useState } from 'react';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { HotkeyButton } from '@/components/ui/hotkey-button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
@@ -53,15 +53,33 @@ export function ProfileForm({
|
||||
icon: profile.icon || 'Brain',
|
||||
});
|
||||
|
||||
// Sync formData with profile prop when it changes
|
||||
useEffect(() => {
|
||||
setFormData({
|
||||
name: profile.name || '',
|
||||
description: profile.description || '',
|
||||
provider: (profile.provider || 'claude') as ModelProvider,
|
||||
// Claude-specific
|
||||
model: profile.model || ('sonnet' as ModelAlias),
|
||||
thinkingLevel: profile.thinkingLevel || ('none' as ThinkingLevel),
|
||||
// Cursor-specific
|
||||
cursorModel: profile.cursorModel || ('auto' as CursorModelId),
|
||||
// Codex-specific - use a valid CodexModelId from CODEX_MODEL_MAP
|
||||
codexModel: profile.codexModel || (CODEX_MODEL_MAP.gpt52Codex as CodexModelId),
|
||||
icon: profile.icon || 'Brain',
|
||||
});
|
||||
}, [profile]);
|
||||
|
||||
const supportsThinking = formData.provider === 'claude' && modelSupportsThinking(formData.model);
|
||||
|
||||
const handleProviderChange = (provider: ModelProvider) => {
|
||||
setFormData({
|
||||
...formData,
|
||||
provider,
|
||||
// Reset to defaults when switching providers
|
||||
// Only reset Claude fields when switching TO Claude; preserve otherwise
|
||||
model: provider === 'claude' ? 'sonnet' : formData.model,
|
||||
thinkingLevel: provider === 'claude' ? 'none' : formData.thinkingLevel,
|
||||
// Reset cursor/codex models when switching to that provider
|
||||
cursorModel: provider === 'cursor' ? 'auto' : formData.cursorModel,
|
||||
codexModel:
|
||||
provider === 'codex' ? (CODEX_MODEL_MAP.gpt52Codex as CodexModelId) : formData.codexModel,
|
||||
@@ -95,6 +113,15 @@ export function ProfileForm({
|
||||
return;
|
||||
}
|
||||
|
||||
// Ensure model is always set for Claude profiles
|
||||
const validModels: ModelAlias[] = ['haiku', 'sonnet', 'opus'];
|
||||
const finalModel =
|
||||
formData.provider === 'claude'
|
||||
? validModels.includes(formData.model)
|
||||
? formData.model
|
||||
: 'sonnet'
|
||||
: undefined;
|
||||
|
||||
const baseProfile = {
|
||||
name: formData.name.trim(),
|
||||
description: formData.description.trim(),
|
||||
@@ -116,7 +143,7 @@ export function ProfileForm({
|
||||
} else {
|
||||
onSave({
|
||||
...baseProfile,
|
||||
model: formData.model,
|
||||
model: finalModel as ModelAlias,
|
||||
thinkingLevel: supportsThinking ? formData.thinkingLevel : 'none',
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { GripVertical, Lock, Pencil, Trash2, Brain, Bot, Terminal } from 'lucide-react';
|
||||
import { GripVertical, Lock, Pencil, Trash2 } from 'lucide-react';
|
||||
import { useSortable } from '@dnd-kit/sortable';
|
||||
import { CSS } from '@dnd-kit/utilities';
|
||||
import type { AIProfile } from '@automaker/types';
|
||||
import { CURSOR_MODEL_MAP, profileHasThinking } from '@automaker/types';
|
||||
import { CURSOR_MODEL_MAP, profileHasThinking, getCodexModelLabel } from '@automaker/types';
|
||||
import { PROFILE_ICONS } from '../constants';
|
||||
import { AnthropicIcon, CursorIcon, OpenAIIcon } from '@/components/ui/provider-icon';
|
||||
|
||||
interface SortableProfileCardProps {
|
||||
profile: AIProfile;
|
||||
@@ -24,7 +25,13 @@ export function SortableProfileCard({ profile, onEdit, onDelete }: SortableProfi
|
||||
opacity: isDragging ? 0.5 : 1,
|
||||
};
|
||||
|
||||
const IconComponent = profile.icon ? PROFILE_ICONS[profile.icon] : Brain;
|
||||
const getDefaultIcon = () => {
|
||||
if (profile.provider === 'cursor') return CursorIcon;
|
||||
if (profile.provider === 'codex') return OpenAIIcon;
|
||||
return AnthropicIcon;
|
||||
};
|
||||
|
||||
const IconComponent = profile.icon ? PROFILE_ICONS[profile.icon] : getDefaultIcon();
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -72,11 +79,17 @@ export function SortableProfileCard({ profile, onEdit, onDelete }: SortableProfi
|
||||
{/* Provider badge */}
|
||||
<span className="text-xs px-2 py-0.5 rounded-full border border-border text-muted-foreground bg-muted/50 flex items-center gap-1">
|
||||
{profile.provider === 'cursor' ? (
|
||||
<Terminal className="w-3 h-3" />
|
||||
<CursorIcon className="w-3 h-3" />
|
||||
) : profile.provider === 'codex' ? (
|
||||
<OpenAIIcon className="w-3 h-3" />
|
||||
) : (
|
||||
<Bot className="w-3 h-3" />
|
||||
<AnthropicIcon className="w-3 h-3" />
|
||||
)}
|
||||
{profile.provider === 'cursor' ? 'Cursor' : 'Claude'}
|
||||
{profile.provider === 'cursor'
|
||||
? 'Cursor'
|
||||
: profile.provider === 'codex'
|
||||
? 'Codex'
|
||||
: 'Claude'}
|
||||
</span>
|
||||
|
||||
{/* Model badge */}
|
||||
@@ -85,7 +98,9 @@ export function SortableProfileCard({ profile, onEdit, onDelete }: SortableProfi
|
||||
? CURSOR_MODEL_MAP[profile.cursorModel || 'auto']?.label ||
|
||||
profile.cursorModel ||
|
||||
'auto'
|
||||
: profile.model || 'sonnet'}
|
||||
: profile.provider === 'codex'
|
||||
? getCodexModelLabel(profile.codexModel || 'codex-gpt-5.2-codex')
|
||||
: profile.model || 'sonnet'}
|
||||
</span>
|
||||
|
||||
{/* Thinking badge - works for both providers */}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Brain, Zap, Scale, Cpu, Rocket, Sparkles } from 'lucide-react';
|
||||
import type { ModelAlias, ThinkingLevel } from '@/store/app-store';
|
||||
import { AnthropicIcon, CursorIcon, OpenAIIcon } from '@/components/ui/provider-icon';
|
||||
|
||||
// Icon mapping for profiles
|
||||
export const PROFILE_ICONS: Record<string, React.ComponentType<{ className?: string }>> = {
|
||||
@@ -9,6 +10,9 @@ export const PROFILE_ICONS: Record<string, React.ComponentType<{ className?: str
|
||||
Cpu,
|
||||
Rocket,
|
||||
Sparkles,
|
||||
Anthropic: AnthropicIcon,
|
||||
Cursor: CursorIcon,
|
||||
Codex: OpenAIIcon,
|
||||
};
|
||||
|
||||
// Available icons for selection
|
||||
@@ -19,6 +23,9 @@ export const ICON_OPTIONS = [
|
||||
{ name: 'Cpu', icon: Cpu },
|
||||
{ name: 'Rocket', icon: Rocket },
|
||||
{ name: 'Sparkles', icon: Sparkles },
|
||||
{ name: 'Anthropic', icon: AnthropicIcon },
|
||||
{ name: 'Cursor', icon: CursorIcon },
|
||||
{ name: 'Codex', icon: OpenAIIcon },
|
||||
];
|
||||
|
||||
// Model options for the form
|
||||
|
||||
@@ -1,110 +1,37 @@
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Checkbox } from '@/components/ui/checkbox';
|
||||
import { FileCode, ShieldCheck, Globe, ImageIcon } from 'lucide-react';
|
||||
import { FileCode, Globe, ImageIcon } from 'lucide-react';
|
||||
import { cn } from '@/lib/utils';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select';
|
||||
import type { CodexApprovalPolicy, CodexSandboxMode } from '@automaker/types';
|
||||
import { OpenAIIcon } from '@/components/ui/provider-icon';
|
||||
|
||||
interface CodexSettingsProps {
|
||||
autoLoadCodexAgents: boolean;
|
||||
codexSandboxMode: CodexSandboxMode;
|
||||
codexApprovalPolicy: CodexApprovalPolicy;
|
||||
codexEnableWebSearch: boolean;
|
||||
codexEnableImages: boolean;
|
||||
onAutoLoadCodexAgentsChange: (enabled: boolean) => void;
|
||||
onCodexSandboxModeChange: (mode: CodexSandboxMode) => void;
|
||||
onCodexApprovalPolicyChange: (policy: CodexApprovalPolicy) => void;
|
||||
onCodexEnableWebSearchChange: (enabled: boolean) => void;
|
||||
onCodexEnableImagesChange: (enabled: boolean) => void;
|
||||
}
|
||||
|
||||
const CARD_TITLE = 'Codex CLI Settings';
|
||||
const CARD_SUBTITLE = 'Configure Codex instructions, capabilities, and execution safety defaults.';
|
||||
const CARD_SUBTITLE = 'Configure Codex instructions and capabilities.';
|
||||
const AGENTS_TITLE = 'Auto-load AGENTS.md Instructions';
|
||||
const AGENTS_DESCRIPTION = 'Automatically inject project instructions from';
|
||||
const AGENTS_PATH = '.codex/AGENTS.md';
|
||||
const AGENTS_SUFFIX = 'on each Codex run.';
|
||||
const WEB_SEARCH_TITLE = 'Enable Web Search';
|
||||
const WEB_SEARCH_DESCRIPTION =
|
||||
'Allow Codex to search the web for current information using --search flag.';
|
||||
const WEB_SEARCH_DESCRIPTION = 'Allow Codex to search the web for current information.';
|
||||
const IMAGES_TITLE = 'Enable Image Support';
|
||||
const IMAGES_DESCRIPTION = 'Allow Codex to process images attached to prompts using -i flag.';
|
||||
const SANDBOX_TITLE = 'Sandbox Policy';
|
||||
const APPROVAL_TITLE = 'Approval Policy';
|
||||
const SANDBOX_SELECT_LABEL = 'Select sandbox policy';
|
||||
const APPROVAL_SELECT_LABEL = 'Select approval policy';
|
||||
|
||||
const SANDBOX_OPTIONS: Array<{
|
||||
value: CodexSandboxMode;
|
||||
label: string;
|
||||
description: string;
|
||||
}> = [
|
||||
{
|
||||
value: 'read-only',
|
||||
label: 'Read-only',
|
||||
description: 'Only allow safe, non-mutating commands.',
|
||||
},
|
||||
{
|
||||
value: 'workspace-write',
|
||||
label: 'Workspace write',
|
||||
description: 'Allow file edits inside the project workspace.',
|
||||
},
|
||||
{
|
||||
value: 'danger-full-access',
|
||||
label: 'Full access',
|
||||
description: 'Allow unrestricted commands (use with care).',
|
||||
},
|
||||
];
|
||||
|
||||
const APPROVAL_OPTIONS: Array<{
|
||||
value: CodexApprovalPolicy;
|
||||
label: string;
|
||||
description: string;
|
||||
}> = [
|
||||
{
|
||||
value: 'untrusted',
|
||||
label: 'Untrusted',
|
||||
description: 'Ask for approval for most commands.',
|
||||
},
|
||||
{
|
||||
value: 'on-failure',
|
||||
label: 'On failure',
|
||||
description: 'Ask only if a command fails in the sandbox.',
|
||||
},
|
||||
{
|
||||
value: 'on-request',
|
||||
label: 'On request',
|
||||
description: 'Let the agent decide when to ask.',
|
||||
},
|
||||
{
|
||||
value: 'never',
|
||||
label: 'Never',
|
||||
description: 'Never ask for approval (least restrictive).',
|
||||
},
|
||||
];
|
||||
const IMAGES_DESCRIPTION = 'Allow Codex to process images attached to prompts.';
|
||||
|
||||
export function CodexSettings({
|
||||
autoLoadCodexAgents,
|
||||
codexSandboxMode,
|
||||
codexApprovalPolicy,
|
||||
codexEnableWebSearch,
|
||||
codexEnableImages,
|
||||
onAutoLoadCodexAgentsChange,
|
||||
onCodexSandboxModeChange,
|
||||
onCodexApprovalPolicyChange,
|
||||
onCodexEnableWebSearchChange,
|
||||
onCodexEnableImagesChange,
|
||||
}: CodexSettingsProps) {
|
||||
const sandboxOption = SANDBOX_OPTIONS.find((option) => option.value === codexSandboxMode);
|
||||
const approvalOption = APPROVAL_OPTIONS.find((option) => option.value === codexApprovalPolicy);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
@@ -189,61 +116,6 @@ export function CodexSettings({
|
||||
<p className="text-xs text-muted-foreground/80 leading-relaxed">{IMAGES_DESCRIPTION}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="group flex items-start space-x-3 p-3 rounded-xl hover:bg-accent/30 transition-colors duration-200 -mx-3">
|
||||
<div className="w-10 h-10 mt-0.5 rounded-xl flex items-center justify-center shrink-0 bg-brand-500/10">
|
||||
<ShieldCheck className="w-5 h-5 text-brand-500" />
|
||||
</div>
|
||||
<div className="flex-1 space-y-4">
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
<div>
|
||||
<Label className="text-foreground font-medium">{SANDBOX_TITLE}</Label>
|
||||
<p className="text-xs text-muted-foreground/80 leading-relaxed">
|
||||
{sandboxOption?.description}
|
||||
</p>
|
||||
</div>
|
||||
<Select
|
||||
value={codexSandboxMode}
|
||||
onValueChange={(value) => onCodexSandboxModeChange(value as CodexSandboxMode)}
|
||||
>
|
||||
<SelectTrigger className="w-[180px] h-8" data-testid="codex-sandbox-select">
|
||||
<SelectValue aria-label={SANDBOX_SELECT_LABEL} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{SANDBOX_OPTIONS.map((option) => (
|
||||
<SelectItem key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
<div>
|
||||
<Label className="text-foreground font-medium">{APPROVAL_TITLE}</Label>
|
||||
<p className="text-xs text-muted-foreground/80 leading-relaxed">
|
||||
{approvalOption?.description}
|
||||
</p>
|
||||
</div>
|
||||
<Select
|
||||
value={codexApprovalPolicy}
|
||||
onValueChange={(value) => onCodexApprovalPolicyChange(value as CodexApprovalPolicy)}
|
||||
>
|
||||
<SelectTrigger className="w-[180px] h-8" data-testid="codex-approval-select">
|
||||
<SelectValue aria-label={APPROVAL_SELECT_LABEL} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{APPROVAL_OPTIONS.map((option) => (
|
||||
<SelectItem key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -27,35 +27,30 @@ interface CodexModelInfo {
|
||||
}
|
||||
|
||||
const CODEX_MODEL_INFO: Record<CodexModelId, CodexModelInfo> = {
|
||||
'gpt-5.2-codex': {
|
||||
id: 'gpt-5.2-codex',
|
||||
'codex-gpt-5.2-codex': {
|
||||
id: 'codex-gpt-5.2-codex',
|
||||
label: 'GPT-5.2-Codex',
|
||||
description: 'Most advanced agentic coding model for complex software engineering',
|
||||
},
|
||||
'gpt-5-codex': {
|
||||
id: 'gpt-5-codex',
|
||||
label: 'GPT-5-Codex',
|
||||
description: 'Purpose-built for Codex CLI with versatile tool use',
|
||||
'codex-gpt-5.1-codex-max': {
|
||||
id: 'codex-gpt-5.1-codex-max',
|
||||
label: 'GPT-5.1-Codex-Max',
|
||||
description: 'Optimized for long-horizon, agentic coding tasks in Codex',
|
||||
},
|
||||
'gpt-5-codex-mini': {
|
||||
id: 'gpt-5-codex-mini',
|
||||
label: 'GPT-5-Codex-Mini',
|
||||
description: 'Faster workflows optimized for low-latency code Q&A and editing',
|
||||
'codex-gpt-5.1-codex-mini': {
|
||||
id: 'codex-gpt-5.1-codex-mini',
|
||||
label: 'GPT-5.1-Codex-Mini',
|
||||
description: 'Smaller, more cost-effective version for faster workflows',
|
||||
},
|
||||
'codex-1': {
|
||||
id: 'codex-1',
|
||||
label: 'Codex-1',
|
||||
description: 'Version of o3 optimized for software engineering',
|
||||
'codex-gpt-5.2': {
|
||||
id: 'codex-gpt-5.2',
|
||||
label: 'GPT-5.2',
|
||||
description: 'Best general agentic model for tasks across industries and domains',
|
||||
},
|
||||
'codex-mini-latest': {
|
||||
id: 'codex-mini-latest',
|
||||
label: 'Codex-Mini-Latest',
|
||||
description: 'Version of o4-mini for Codex, optimized for faster workflows',
|
||||
},
|
||||
'gpt-5': {
|
||||
id: 'gpt-5',
|
||||
label: 'GPT-5',
|
||||
description: 'GPT-5 base flagship model',
|
||||
'codex-gpt-5.1': {
|
||||
id: 'codex-gpt-5.1',
|
||||
label: 'GPT-5.1',
|
||||
description: 'Great for coding and agentic tasks across domains',
|
||||
},
|
||||
};
|
||||
|
||||
@@ -164,6 +159,11 @@ export function CodexModelConfiguration({
|
||||
}
|
||||
|
||||
function supportsReasoningEffort(modelId: string): boolean {
|
||||
const reasoningModels = ['gpt-5.2-codex', 'gpt-5-codex', 'gpt-5', 'codex-1'];
|
||||
const reasoningModels = [
|
||||
'codex-gpt-5.2-codex',
|
||||
'codex-gpt-5.1-codex-max',
|
||||
'codex-gpt-5.2',
|
||||
'codex-gpt-5.1',
|
||||
];
|
||||
return reasoningModels.includes(modelId);
|
||||
}
|
||||
|
||||
@@ -181,13 +181,9 @@ export function CodexSettingsTab() {
|
||||
|
||||
<CodexSettings
|
||||
autoLoadCodexAgents={codexAutoLoadAgents}
|
||||
codexSandboxMode={codexSandboxMode}
|
||||
codexApprovalPolicy={codexApprovalPolicy}
|
||||
codexEnableWebSearch={codexEnableWebSearch}
|
||||
codexEnableImages={codexEnableImages}
|
||||
onAutoLoadCodexAgentsChange={setCodexAutoLoadAgents}
|
||||
onCodexSandboxModeChange={setCodexSandboxMode}
|
||||
onCodexApprovalPolicyChange={setCodexApprovalPolicy}
|
||||
onCodexEnableWebSearchChange={setCodexEnableWebSearch}
|
||||
onCodexEnableImagesChange={setCodexEnableImages}
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user