diff --git a/src/commands/effort/effort.tsx b/src/commands/effort/effort.tsx
index 0dadd606..1cbc83d1 100644
--- a/src/commands/effort/effort.tsx
+++ b/src/commands/effort/effort.tsx
@@ -4,7 +4,8 @@ import { useMainLoopModel } from '../../hooks/useMainLoopModel.js';
import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent } from '../../services/analytics/index.js';
import { useAppState, useSetAppState } from '../../state/AppState.js';
import type { LocalJSXCommandOnDone } from '../../types/command.js';
-import { type EffortValue, getDisplayedEffortLevel, getEffortEnvOverride, getEffortValueDescription, isEffortLevel, toPersistableEffort } from '../../utils/effort.js';
+import { type EffortValue, getDisplayedEffortLevel, getEffortEnvOverride, getEffortValueDescription, isEffortLevel, isOpenAIEffortLevel, modelUsesOpenAIEffort, toPersistableEffort } from '../../utils/effort.js';
+import { EffortPicker } from '../../components/EffortPicker.js';
import { updateSettingsForSource } from '../../utils/settings/settings.js';
const COMMON_HELP_ARGS = ['help', '-h', '--help'];
type EffortCommandResult = {
@@ -109,12 +110,15 @@ export function executeEffort(args: string): EffortCommandResult {
if (normalized === 'auto' || normalized === 'unset') {
return unsetEffortLevel();
}
- if (!isEffortLevel(normalized)) {
- return {
- message: `Invalid argument: ${args}. Valid options are: low, medium, high, max, auto`
- };
+ if (isEffortLevel(normalized)) {
+ return setEffortValue(normalized);
}
- return setEffortValue(normalized);
+ if (isOpenAIEffortLevel(normalized)) {
+ return setEffortValue(normalized);
+ }
+ return {
+ message: `Invalid argument: ${args}. Valid options are: low, medium, high, max, xhigh, auto`
+ };
}
function ShowCurrentEffort(t0) {
const {
@@ -174,10 +178,44 @@ export async function call(onDone: LocalJSXCommandOnDone, _context: unknown, arg
onDone('Usage: /effort [low|medium|high|max|auto]\n\nEffort levels:\n- low: Quick, straightforward implementation\n- medium: Balanced approach with standard testing\n- high: Comprehensive implementation with extensive testing\n- max: Maximum capability with deepest reasoning (Opus 4.6 only)\n- auto: Use the default effort level for your model');
return;
}
- if (!args || args === 'current' || args === 'status') {
+ if (args === 'current' || args === 'status') {
return ;
}
+ if (!args) {
+ return ;
+ }
const result = executeEffort(args);
return ;
}
+
+function EffortPickerWrapper({ onDone }: { onDone: LocalJSXCommandOnDone }) {
+ const setAppState = useSetAppState();
+ const model = useMainLoopModel();
+ const usesOpenAIEffort = modelUsesOpenAIEffort(model);
+
+ function handleSelect(effort: EffortValue | undefined) {
+ const persistable = toPersistableEffort(effort);
+ if (persistable !== undefined) {
+ updateSettingsForSource('userSettings', {
+ effortLevel: persistable
+ });
+ }
+ logEvent('tengu_effort_command', {
+ effort: (effort ?? 'auto') as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS
+ });
+ setAppState(prev => ({
+ ...prev,
+ effortValue: effort
+ }));
+ const description = effort ? getEffortValueDescription(effort) : 'Use default effort level for your model';
+ const suffix = persistable !== undefined ? '' : ' (this session only)';
+ onDone(`Set effort level to ${effort ?? 'auto'}${suffix}: ${description}`);
+ }
+
+ function handleCancel() {
+ onDone('Cancelled');
+ }
+
+ return ;
+}
//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJuYW1lcyI6WyJSZWFjdCIsInVzZU1haW5Mb29wTW9kZWwiLCJBbmFseXRpY3NNZXRhZGF0YV9JX1ZFUklGSUVEX1RISVNfSVNfTk9UX0NPREVfT1JfRklMRVBBVEhTIiwibG9nRXZlbnQiLCJ1c2VBcHBTdGF0ZSIsInVzZVNldEFwcFN0YXRlIiwiTG9jYWxKU1hDb21tYW5kT25Eb25lIiwiRWZmb3J0VmFsdWUiLCJnZXREaXNwbGF5ZWRFZmZvcnRMZXZlbCIsImdldEVmZm9ydEVudk92ZXJyaWRlIiwiZ2V0RWZmb3J0VmFsdWVEZXNjcmlwdGlvbiIsImlzRWZmb3J0TGV2ZWwiLCJ0b1BlcnNpc3RhYmxlRWZmb3J0IiwidXBkYXRlU2V0dGluZ3NGb3JTb3VyY2UiLCJDT01NT05fSEVMUF9BUkdTIiwiRWZmb3J0Q29tbWFuZFJlc3VsdCIsIm1lc3NhZ2UiLCJlZmZvcnRVcGRhdGUiLCJ2YWx1ZSIsInNldEVmZm9ydFZhbHVlIiwiZWZmb3J0VmFsdWUiLCJwZXJzaXN0YWJsZSIsInVuZGVmaW5lZCIsInJlc3VsdCIsImVmZm9ydExldmVsIiwiZXJyb3IiLCJlZmZvcnQiLCJlbnZPdmVycmlkZSIsImVudlJhdyIsInByb2Nlc3MiLCJlbnYiLCJDTEFVREVfQ09ERV9FRkZPUlRfTEVWRUwiLCJkZXNjcmlwdGlvbiIsInN1ZmZpeCIsInNob3dDdXJyZW50RWZmb3J0IiwiYXBwU3RhdGVFZmZvcnQiLCJtb2RlbCIsImVmZmVjdGl2ZVZhbHVlIiwibGV2ZWwiLCJ1bnNldEVmZm9ydExldmVsIiwiZXhlY3V0ZUVmZm9ydCIsImFyZ3MiLCJub3JtYWxpemVkIiwidG9Mb3dlckNhc2UiLCJTaG93Q3VycmVudEVmZm9ydCIsInQwIiwib25Eb25lIiwiX3RlbXAiLCJzIiwiQXBwbHlFZmZvcnRBbmRDbG9zZSIsIiQiLCJfYyIsInNldEFwcFN0YXRlIiwidDEiLCJ0MiIsInByZXYiLCJ1c2VFZmZlY3QiLCJjYWxsIiwiX2NvbnRleHQiLCJQcm9taXNlIiwiUmVhY3ROb2RlIiwidHJpbSIsImluY2x1ZGVzIl0sInNvdXJjZXMiOlsiZWZmb3J0LnRzeCJdLCJzb3VyY2VzQ29udGVudCI6WyJpbXBvcnQgKiBhcyBSZWFjdCBmcm9tICdyZWFjdCdcbmltcG9ydCB7IHVzZU1haW5Mb29wTW9kZWwgfSBmcm9tICcuLi8uLi9ob29rcy91c2VNYWluTG9vcE1vZGVsLmpzJ1xuaW1wb3J0IHtcbiAgdHlwZSBBbmFseXRpY3NNZXRhZGF0YV9JX1ZFUklGSUVEX1RISVNfSVNfTk9UX0NPREVfT1JfRklMRVBBVEhTLFxuICBsb2dFdmVudCxcbn0gZnJvbSAnLi4vLi4vc2VydmljZXMvYW5hbHl0aWNzL2luZGV4LmpzJ1xuaW1wb3J0IHsgdXNlQXBwU3RhdGUsIHVzZVNldEFwcFN0YXRlIH0gZnJvbSAnLi4vLi4vc3RhdGUvQXBwU3RhdGUuanMnXG5pbXBvcnQgdHlwZSB7IExvY2FsSlNYQ29tbWFuZE9uRG9uZSB9IGZyb20gJy4uLy4uL3R5cGVzL2NvbW1hbmQuanMnXG5pbXBvcnQge1xuICB0eXBlIEVmZm9ydFZhbHVlLFxuICBnZXREaXNwbGF5ZWRFZmZvcnRMZXZlbCxcbiAgZ2V0RWZmb3J0RW52T3ZlcnJpZGUsXG4gIGdldEVmZm9ydFZhbHVlRGVzY3JpcHRpb24sXG4gIGlzRWZmb3J0TGV2ZWwsXG4gIHRvUGVyc2lzdGFibGVFZmZvcnQsXG59IGZyb20gJy4uLy4uL3V0aWxzL2VmZm9ydC5qcydcbmltcG9ydCB7IHVwZGF0ZVNldHRpbmdzRm9yU291cmNlIH0gZnJvbSAnLi4vLi4vdXRpbHMvc2V0dGluZ3Mvc2V0dGluZ3MuanMnXG5cbmNvbnN0IENPTU1PTl9IRUxQX0FSR1MgPSBbJ2hlbHAnLCAnLWgnLCAnLS1oZWxwJ11cblxudHlwZSBFZmZvcnRDb21tYW5kUmVzdWx0ID0ge1xuICBtZXNzYWdlOiBzdHJpbmdcbiAgZWZmb3J0VXBkYXRlPzogeyB2YWx1ZTogRWZmb3J0VmFsdWUgfCB1bmRlZmluZWQgfVxufVxuXG5mdW5jdGlvbiBzZXRFZmZvcnRWYWx1ZShlZmZvcnRWYWx1ZTogRWZmb3J0VmFsdWUpOiBFZmZvcnRDb21tYW5kUmVzdWx0IHtcbiAgY29uc3QgcGVyc2lzdGFibGUgPSB0b1BlcnNpc3RhYmxlRWZmb3J0KGVmZm9ydFZhbHVlKVxuICBpZiAocGVyc2lzdGFibGUgIT09IHVuZGVmaW5lZCkge1xuICAgIGNvbnN0IHJlc3VsdCA9IHVwZGF0ZVNldHRpbmdzRm9yU291cmNlKCd1c2VyU2V0dGluZ3MnLCB7XG4gICAgICBlZmZvcnRMZXZlbDogcGVyc2lzdGFibGUsXG4gICAgfSlcbiAgICBpZiAocmVzdWx0LmVycm9yKSB7XG4gICAgICByZXR1cm4ge1xuICAgICAgICBtZXNzYWdlOiBgRmFpbGVkIHRvIHNldCBlZmZvcnQgbGV2ZWw6ICR7cmVzdWx0LmVycm9yLm1lc3NhZ2V9YCxcbiAgICAgIH1cbiAgICB9XG4gIH1cbiAgbG9nRXZlbnQoJ3Rlbmd1X2VmZm9ydF9jb21tYW5kJywge1xuICAgIGVmZm9ydDpcbiAgICAgIGVmZm9ydFZhbHVlIGFzIEFuYWx5dGljc01ldGFkYXRhX0lfVkVSSUZJRURfVEhJU19JU19OT1RfQ09ERV9PUl9GSUxFUEFUSFMsXG4gIH0pXG5cbiAgLy8gRW52IHZhciB3aW5zIGF0IHJlc29sdmVBcHBsaWVkRWZmb3J0IHRpbWUuIE9ubHkgZmxhZyBpdCB3aGVuIGl0IGFjdHVhbGx5XG4gIC8vIGNvbmZsaWN0cyDigJQgaWYgZW52IG1hdGNoZXMgd2hhdCB0aGUgdXNlciBqdXN0IGFza2VkIGZvciwgdGhlIG91dGNvbWUgaXNcbiAgLy8gdGhlIHNhbWUsIHNvIFwiU2V0IGVmZm9ydCB0byBYXCIgaXMgdHJ1ZSBhbmQgdGhlIG5vdGUgaXMgbm9pc2UuXG4gIGNvbnN0IGVudk92ZXJyaWRlID0gZ2V0RWZmb3J0RW52T3ZlcnJpZGUoKVxuICBpZiAoZW52T3ZlcnJpZGUgIT09IHVuZGVmaW5lZCAmJiBlbnZPdmVycmlkZSAhPT0gZWZmb3J0VmFsdWUpIHtcbiAgICBjb25zdCBlbnZSYXcgPSBwcm9jZXNzLmVudi5DTEFVREVfQ09ERV9FRkZPUlRfTEVWRUxcbiAgICBpZiAocGVyc2lzdGFibGUgPT09IHVuZGVmaW5lZCkge1xuICAgICAgcmV0dXJuIHtcbiAgICAgICAgbWVzc2FnZTogYE5vdCBhcHBsaWVkOiBDTEFVREVfQ09ERV9FRkZPUlRfTEVWRUw9JHtlbnZSYXd9IG92ZXJyaWRlcyBlZmZvcnQgdGhpcyBzZXNzaW9uLCBhbmQgJHtlZmZvcnRWYWx1ZX0gaXMgc2Vzc2lvbi1vbmx5IChub3RoaW5nIHNhdmVkKWAsXG4gICAgICAgIGVmZm9ydFVwZGF0ZTogeyB2YWx1ZTogZWZmb3J0VmFsdWUgfSxcbiAgICAgIH1cbiAgICB9XG4gICAgcmV0dXJuIHtcbiAgICAgIG1lc3NhZ2U6IGBDTEFVREVfQ09ERV9FRkZPUlRfTEVWRUw9JHtlbnZSYXd9IG92ZXJyaWRlcyB0aGlzIHNlc3Npb24g4oCUIGNsZWFyIGl0IGFuZCAke2VmZm9ydFZhbHVlfSB0YWtlcyBvdmVyYCxcbiAgICAgIGVmZm9ydFVwZGF0ZTogeyB2YWx1ZTogZWZmb3J0VmFsdWUgfSxcbiAgICB9XG4gIH1cblxuICBjb25zdCBkZXNjcmlwdGlvbiA9IGdldEVmZm9ydFZhbHVlRGVzY3JpcHRpb24oZWZmb3J0VmFsdWUpXG4gIGNvbnN0IHN1ZmZpeCA9IHBlcnNpc3RhYmxlICE9PSB1bmRlZmluZWQgPyAnJyA6ICcgKHRoaXMgc2Vzc2lvbiBvbmx5KSdcbiAgcmV0dXJuIHtcbiAgICBtZXNzYWdlOiBgU2V0IGVmZm9ydCBsZXZlbCB0byAke2VmZm9ydFZhbHVlfSR7c3VmZml4fTogJHtkZXNjcmlwdGlvbn1gLFxuICAgIGVmZm9ydFVwZGF0ZTogeyB2YWx1ZTogZWZmb3J0VmFsdWUgfSxcbiAgfVxufVxuXG5leHBvcnQgZnVuY3Rpb24gc2hvd0N1cnJlbnRFZmZvcnQoXG4gIGFwcFN0YXRlRWZmb3J0OiBFZmZvcnRWYWx1ZSB8IHVuZGVmaW5lZCxcbiAgbW9kZWw6IHN0cmluZyxcbik6IEVmZm9ydENvbW1hbmRSZXN1bHQge1xuICBjb25zdCBlbnZPdmVycmlkZSA9IGdldEVmZm9ydEVudk92ZXJyaWRlKClcbiAgY29uc3QgZWZmZWN0aXZlVmFsdWUgPVxuICAgIGVudk92ZXJyaWRlID09PSBudWxsID8gdW5kZWZpbmVkIDogKGVudk92ZXJyaWRlID8/IGFwcFN0YXRlRWZmb3J0KVxuICBpZiAoZWZmZWN0aXZlVmFsdWUgPT09IHVuZGVmaW5lZCkge1xuICAgIGNvbnN0IGxldmVsID0gZ2V0RGlzcGxheWVkRWZmb3J0TGV2ZWwobW9kZWwsIGFwcFN0YXRlRWZmb3J0KVxuICAgIHJldHVybiB7IG1lc3NhZ2U6IGBFZmZvcnQgbGV2ZWw6IGF1dG8gKGN1cnJlbnRseSAke2xldmVsfSlgIH1cbiAgfVxuICBjb25zdCBkZXNjcmlwdGlvbiA9IGdldEVmZm9ydFZhbHVlRGVzY3JpcHRpb24oZWZmZWN0aXZlVmFsdWUpXG4gIHJldHVybiB7XG4gICAgbWVzc2FnZTogYEN1cnJlbnQgZWZmb3J0IGxldmVsOiAke2VmZmVjdGl2ZVZhbHVlfSAoJHtkZXNjcmlwdGlvbn0pYCxcbiAgfVxufVxuXG5mdW5jdGlvbiB1bnNldEVmZm9ydExldmVsKCk6IEVmZm9ydENvbW1hbmRSZXN1bHQge1xuICBjb25zdCByZXN1bHQgPSB1cGRhdGVTZXR0aW5nc0ZvclNvdXJjZSgndXNlclNldHRpbmdzJywge1xuICAgIGVmZm9ydExldmVsOiB1bmRlZmluZWQsXG4gIH0pXG4gIGlmIChyZXN1bHQuZXJyb3IpIHtcbiAgICByZXR1cm4ge1xuICAgICAgbWVzc2FnZTogYEZhaWxlZCB0byBzZXQgZWZmb3J0IGxldmVsOiAke3Jlc3VsdC5lcnJvci5tZXNzYWdlfWAsXG4gICAgfVxuICB9XG4gIGxvZ0V2ZW50KCd0ZW5ndV9lZmZvcnRfY29tbWFuZCcsIHtcbiAgICBlZmZvcnQ6XG4gICAgICAnYXV0bycgYXMgQW5hbHl0aWNzTWV0YWRhdGFfSV9WRVJJRklFRF9USElTX0lTX05PVF9DT0RFX09SX0ZJTEVQQVRIUyxcbiAgfSlcbiAgLy8gZW52PWF1dG8vdW5zZXQgKG51bGwpIG1hdGNoZXMgd2hhdCAvZWZmb3J0IGF1dG8gYXNrcyBmb3IsIHNvIG9ubHkgd2FyblxuICAvLyB3aGVuIGVudiBpcyBwaW5uaW5nIGEgc3BlY2lmaWMgbGV2ZWwgdGhhdCB3aWxsIGtlZXAgb3ZlcnJpZGluZy5cbiAgY29uc3QgZW52T3ZlcnJpZGUgPSBnZXRFZmZvcnRFbnZPdmVycmlkZSgpXG4gIGlmIChlbnZPdmVycmlkZSAhPT0gdW5kZWZpbmVkICYmIGVudk92ZXJyaWRlICE9PSBudWxsKSB7XG4gICAgY29uc3QgZW52UmF3ID0gcHJvY2Vzcy5lbnYuQ0xBVURFX0NPREVfRUZGT1JUX0xFVkVMXG4gICAgcmV0dXJuIHtcbiAgICAgIG1lc3NhZ2U6IGBDbGVhcmVkIGVmZm9ydCBmcm9tIHNldHRpbmdzLCBidXQgQ0xBVURFX0NPREVfRUZGT1JUX0xFVkVMPSR7ZW52UmF3fSBzdGlsbCBjb250cm9scyB0aGlzIHNlc3Npb25gLFxuICAgICAgZWZmb3J0VXBkYXRlOiB7IHZhbHVlOiB1bmRlZmluZWQgfSxcbiAgICB9XG4gIH1cbiAgcmV0dXJuIHtcbiAgICBtZXNzYWdlOiAnRWZmb3J0IGxldmVsIHNldCB0byBhdXRvJyxcbiAgICBlZmZvcnRVcGRhdGU6IHsgdmFsdWU6IHVuZGVmaW5lZCB9LFxuICB9XG59XG5cbmV4cG9ydCBmdW5jdGlvbiBleGVjdXRlRWZmb3J0KGFyZ3M6IHN0cmluZyk6IEVmZm9ydENvbW1hbmRSZXN1bHQge1xuICBjb25zdCBub3JtYWxpemVkID0gYXJncy50b0xvd2VyQ2FzZSgpXG4gIGlmIChub3JtYWxpemVkID09PSAnYXV0bycgfHwgbm9ybWFsaXplZCA9PT0gJ3Vuc2V0Jykge1xuICAgIHJldHVybiB1bnNldEVmZm9ydExldmVsKClcbiAgfVxuXG4gIGlmICghaXNFZmZvcnRMZXZlbChub3JtYWxpemVkKSkge1xuICAgIHJldHVybiB7XG4gICAgICBtZXNzYWdlOiBgSW52YWxpZCBhcmd1bWVudDogJHthcmdzfS4gVmFsaWQgb3B0aW9ucyBhcmU6IGxvdywgbWVkaXVtLCBoaWdoLCBtYXgsIGF1dG9gLFxuICAgIH1cbiAgfVxuXG4gIHJldHVybiBzZXRFZmZvcnRWYWx1ZShub3JtYWxpemVkKVxufVxuXG5mdW5jdGlvbiBTaG93Q3VycmVudEVmZm9ydCh7XG4gIG9uRG9uZSxcbn06IHtcbiAgb25Eb25lOiAocmVzdWx0OiBzdHJpbmcpID0+IHZvaWRcbn0pOiBSZWFjdC5SZWFjdE5vZGUge1xuICBjb25zdCBlZmZvcnRWYWx1ZSA9IHVzZUFwcFN0YXRlKHMgPT4gcy5lZmZvcnRWYWx1ZSlcbiAgY29uc3QgbW9kZWwgPSB1c2VNYWluTG9vcE1vZGVsKClcbiAgY29uc3QgeyBtZXNzYWdlIH0gPSBzaG93Q3VycmVudEVmZm9ydChlZmZvcnRWYWx1ZSwgbW9kZWwpXG4gIG9uRG9uZShtZXNzYWdlKVxuICByZXR1cm4gbnVsbFxufVxuXG5mdW5jdGlvbiBBcHBseUVmZm9ydEFuZENsb3NlKHtcbiAgcmVzdWx0LFxuICBvbkRvbmUsXG59OiB7XG4gIHJlc3VsdDogRWZmb3J0Q29tbWFuZFJlc3VsdFxuICBvbkRvbmU6IChyZXN1bHQ6IHN0cmluZykgPT4gdm9pZFxufSk6IFJlYWN0LlJlYWN0Tm9kZSB7XG4gIGNvbnN0IHNldEFwcFN0YXRlID0gdXNlU2V0QXBwU3RhdGUoKVxuICBjb25zdCB7IGVmZm9ydFVwZGF0ZSwgbWVzc2FnZSB9ID0gcmVzdWx0XG4gIFJlYWN0LnVzZUVmZmVjdCgoKSA9PiB7XG4gICAgaWYgKGVmZm9ydFVwZGF0ZSkge1xuICAgICAgc2V0QXBwU3RhdGUocHJldiA9PiAoe1xuICAgICAgICAuLi5wcmV2LFxuICAgICAgICBlZmZvcnRWYWx1ZTogZWZmb3J0VXBkYXRlLnZhbHVlLFxuICAgICAgfSkpXG4gICAgfVxuICAgIG9uRG9uZShtZXNzYWdlKVxuICB9LCBbc2V0QXBwU3RhdGUsIGVmZm9ydFVwZGF0ZSwgbWVzc2FnZSwgb25Eb25lXSlcbiAgcmV0dXJuIG51bGxcbn1cblxuZXhwb3J0IGFzeW5jIGZ1bmN0aW9uIGNhbGwoXG4gIG9uRG9uZTogTG9jYWxKU1hDb21tYW5kT25Eb25lLFxuICBfY29udGV4dDogdW5rbm93bixcbiAgYXJncz86IHN0cmluZyxcbik6IFByb21pc2U8UmVhY3QuUmVhY3ROb2RlPiB7XG4gIGFyZ3MgPSBhcmdzPy50cmltKCkgfHwgJydcblxuICBpZiAoQ09NTU9OX0hFTFBfQVJHUy5pbmNsdWRlcyhhcmdzKSkge1xuICAgIG9uRG9uZShcbiAgICAgICdVc2FnZTogL2VmZm9ydCBbbG93fG1lZGl1bXxoaWdofG1heHxhdXRvXVxcblxcbkVmZm9ydCBsZXZlbHM6XFxuLSBsb3c6IFF1aWNrLCBzdHJhaWdodGZvcndhcmQgaW1wbGVtZW50YXRpb25cXG4tIG1lZGl1bTogQmFsYW5jZWQgYXBwcm9hY2ggd2l0aCBzdGFuZGFyZCB0ZXN0aW5nXFxuLSBoaWdoOiBDb21wcmVoZW5zaXZlIGltcGxlbWVudGF0aW9uIHdpdGggZXh0ZW5zaXZlIHRlc3RpbmdcXG4tIG1heDogTWF4aW11bSBjYXBhYmlsaXR5IHdpdGggZGVlcGVzdCByZWFzb25pbmcgKE9wdXMgNC42IG9ubHkpXFxuLSBhdXRvOiBVc2UgdGhlIGRlZmF1bHQgZWZmb3J0IGxldmVsIGZvciB5b3VyIG1vZGVsJyxcbiAgICApXG4gICAgcmV0dXJuXG4gIH1cblxuICBpZiAoIWFyZ3MgfHwgYXJncyA9PT0gJ2N1cnJlbnQnIHx8IGFyZ3MgPT09ICdzdGF0dXMnKSB7XG4gICAgcmV0dXJuIDxTaG93Q3VycmVudEVmZm9ydCBvbkRvbmU9e29uRG9uZX0gLz5cbiAgfVxuXG4gIGNvbnN0IHJlc3VsdCA9IGV4ZWN1dGVFZmZvcnQoYXJncylcbiAgcmV0dXJuIDxBcHBseUVmZm9ydEFuZENsb3NlIHJlc3VsdD17cmVzdWx0fSBvbkRvbmU9e29uRG9uZX0gLz5cbn1cbiJdLCJtYXBwaW5ncyI6IjtBQUFBLE9BQU8sS0FBS0EsS0FBSyxNQUFNLE9BQU87QUFDOUIsU0FBU0MsZ0JBQWdCLFFBQVEsaUNBQWlDO0FBQ2xFLFNBQ0UsS0FBS0MsMERBQTBELEVBQy9EQyxRQUFRLFFBQ0gsbUNBQW1DO0FBQzFDLFNBQVNDLFdBQVcs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
\ No newline at end of file
diff --git a/src/components/CustomSelect/use-select-navigation.ts b/src/components/CustomSelect/use-select-navigation.ts
index 7ecb4e71..544bbfa7 100644
--- a/src/components/CustomSelect/use-select-navigation.ts
+++ b/src/components/CustomSelect/use-select-navigation.ts
@@ -84,44 +84,44 @@ const reducer = (state: State, action: Action): State => {
return state
}
- // Wrap to first item if at the end
- const next = item.next || state.optionMap.first
+ // If there's a next item in the list, go to it
+ if (item.next) {
+ const needsToScroll = item.next.index >= state.visibleToIndex
- if (!next) {
+ if (!needsToScroll) {
+ return {
+ ...state,
+ focusedValue: item.next.value,
+ }
+ }
+
+ const nextVisibleToIndex = Math.min(
+ state.optionMap.size,
+ state.visibleToIndex + 1,
+ )
+
+ const nextVisibleFromIndex = nextVisibleToIndex - state.visibleOptionCount
+
+ return {
+ ...state,
+ focusedValue: item.next.value,
+ visibleFromIndex: nextVisibleFromIndex,
+ visibleToIndex: nextVisibleToIndex,
+ }
+ }
+
+ // No next item - wrap to first item
+ const firstItem = state.optionMap.first
+ if (!firstItem) {
return state
}
// When wrapping to first, reset viewport to start
- if (!item.next && next === state.optionMap.first) {
- return {
- ...state,
- focusedValue: next.value,
- visibleFromIndex: 0,
- visibleToIndex: state.visibleOptionCount,
- }
- }
-
- const needsToScroll = next.index >= state.visibleToIndex
-
- if (!needsToScroll) {
- return {
- ...state,
- focusedValue: next.value,
- }
- }
-
- const nextVisibleToIndex = Math.min(
- state.optionMap.size,
- state.visibleToIndex + 1,
- )
-
- const nextVisibleFromIndex = nextVisibleToIndex - state.visibleOptionCount
-
return {
...state,
- focusedValue: next.value,
- visibleFromIndex: nextVisibleFromIndex,
- visibleToIndex: nextVisibleToIndex,
+ focusedValue: firstItem.value,
+ visibleFromIndex: 0,
+ visibleToIndex: state.visibleOptionCount,
}
}
@@ -136,44 +136,43 @@ const reducer = (state: State, action: Action): State => {
return state
}
- // Wrap to last item if at the beginning
- const previous = item.previous || state.optionMap.last
+ // If there's a previous item in the list, go to it
+ if (item.previous) {
+ const needsToScroll = item.previous.index < state.visibleFromIndex
- if (!previous) {
- return state
- }
+ if (!needsToScroll) {
+ return {
+ ...state,
+ focusedValue: item.previous.value,
+ }
+ }
+
+ const nextVisibleFromIndex = Math.max(0, state.visibleFromIndex - 1)
+ const nextVisibleToIndex = nextVisibleFromIndex + state.visibleOptionCount
- // When wrapping to last, reset viewport to end
- if (!item.previous && previous === state.optionMap.last) {
- const nextVisibleToIndex = state.optionMap.size
- const nextVisibleFromIndex = Math.max(
- 0,
- nextVisibleToIndex - state.visibleOptionCount,
- )
return {
...state,
- focusedValue: previous.value,
+ focusedValue: item.previous.value,
visibleFromIndex: nextVisibleFromIndex,
visibleToIndex: nextVisibleToIndex,
}
}
- const needsToScroll = previous.index <= state.visibleFromIndex
-
- if (!needsToScroll) {
- return {
- ...state,
- focusedValue: previous.value,
- }
+ // No previous item - wrap to last item
+ const lastItem = state.optionMap.last
+ if (!lastItem) {
+ return state
}
- const nextVisibleFromIndex = Math.max(0, state.visibleFromIndex - 1)
-
- const nextVisibleToIndex = nextVisibleFromIndex + state.visibleOptionCount
-
+ // When wrapping to last, reset viewport to end
+ const nextVisibleToIndex = state.optionMap.size
+ const nextVisibleFromIndex = Math.max(
+ 0,
+ nextVisibleToIndex - state.visibleOptionCount,
+ )
return {
...state,
- focusedValue: previous.value,
+ focusedValue: lastItem.value,
visibleFromIndex: nextVisibleFromIndex,
visibleToIndex: nextVisibleToIndex,
}
diff --git a/src/components/EffortPicker.tsx b/src/components/EffortPicker.tsx
new file mode 100644
index 00000000..2e86509e
--- /dev/null
+++ b/src/components/EffortPicker.tsx
@@ -0,0 +1,152 @@
+import React, { useState } from 'react'
+import { Box, Text } from '../ink.js'
+import { useMainLoopModel } from '../hooks/useMainLoopModel.js'
+import { useAppState, useSetAppState } from '../state/AppState.js'
+import type { EffortLevel, OpenAIEffortLevel } from '../utils/effort.js'
+import {
+ getAvailableEffortLevels,
+ getDisplayedEffortLevel,
+ getEffortLevelDescription,
+ getEffortLevelLabel,
+ getEffortValueDescription,
+ modelSupportsEffort,
+ modelUsesOpenAIEffort,
+ standardEffortToOpenAI,
+ isOpenAIEffortLevel,
+} from '../utils/effort.js'
+import { getAPIProvider } from '../utils/model/providers.js'
+import { getReasoningEffortForModel } from '../services/api/providerConfig.js'
+import { Select } from './CustomSelect/select.js'
+import { effortLevelToSymbol } from './EffortIndicator.js'
+import { KeyboardShortcutHint } from './design-system/KeyboardShortcutHint.js'
+import { Byline } from './design-system/Byline.js'
+
+type EffortOption = {
+ label: React.ReactNode
+ value: string
+ description: string
+ isAvailable: boolean
+}
+
+type Props = {
+ onSelect: (effort: EffortLevel | undefined) => void
+ onCancel?: () => void
+}
+
+export function EffortPicker({ onSelect, onCancel }: Props) {
+ const model = useMainLoopModel()
+ const appStateEffort = useAppState((s: any) => s.effortValue)
+ const setAppState = useSetAppState()
+ const provider = getAPIProvider()
+ const usesOpenAIEffort = modelUsesOpenAIEffort(model)
+ const availableLevels = getAvailableEffortLevels(model)
+ const currentDisplayedLevel = getDisplayedEffortLevel(model, appStateEffort)
+
+ // For OpenAI/Codex, get the model's default reasoning effort
+ const modelReasoningEffort = usesOpenAIEffort ? getReasoningEffortForModel(model) : undefined
+ const defaultEffortForModel = modelReasoningEffort || currentDisplayedLevel
+
+ const options: EffortOption[] = [
+ {
+ label: ,
+ value: 'auto',
+ description: 'Use the default effort level for your model',
+ isAvailable: true,
+ },
+ ...availableLevels.map(level => {
+ const displayLevel = usesOpenAIEffort
+ ? (level === 'xhigh' ? 'max' : level)
+ : level
+ const isCurrent = currentDisplayedLevel === displayLevel
+ return {
+ label: (
+
+ ),
+ value: level,
+ description: getEffortLevelDescription(level as EffortLevel),
+ isAvailable: true,
+ }
+ }),
+ ]
+
+ function handleSelect(value: string) {
+ if (value === 'auto') {
+ setAppState(prev => ({
+ ...prev,
+ effortValue: undefined,
+ }))
+ onSelect(undefined)
+ } else {
+ const effortLevel = value as EffortLevel
+ setAppState(prev => ({
+ ...prev,
+ effortValue: effortLevel,
+ }))
+ onSelect(effortLevel)
+ }
+ }
+
+ function handleCancel() {
+ onCancel?.()
+ }
+
+ const supportsEffort = modelSupportsEffort(model)
+ // For OpenAI/Codex, use the model's default reasoning effort as initial focus
+ // For Claude, use the displayed effort level or 'auto'
+ const initialFocus = usesOpenAIEffort
+ ? (modelReasoningEffort || 'auto')
+ : (appStateEffort ? String(appStateEffort) : 'auto')
+
+ return (
+
+
+ Set effort level
+
+ {usesOpenAIEffort
+ ? `OpenAI/Codex provider (${provider})`
+ : supportsEffort
+ ? `Claude model ยท ${provider} provider`
+ : `Effort not supported for this model`
+ }
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ )
+}
+
+function EffortOptionLabel({ level, text, isCurrent }: { level: EffortLevel | 'auto', text: string, isCurrent: boolean }) {
+ const symbol = level === 'auto' ? 'โ' : effortLevelToSymbol(level as EffortLevel)
+ const color = isCurrent ? 'remember' : level === 'auto' ? 'subtle' : 'suggestion'
+
+ return (
+ <>
+ {symbol}
+ {text}
+ {isCurrent && (current)}
+ >
+ )
+}
diff --git a/src/components/StartupScreen.ts b/src/components/StartupScreen.ts
index b20d26c1..e38a4111 100644
--- a/src/components/StartupScreen.ts
+++ b/src/components/StartupScreen.ts
@@ -97,21 +97,45 @@ function detectProvider(): { name: string; model: string; baseUrl: string; isLoc
}
if (useOpenAI) {
- const model = process.env.OPENAI_MODEL || 'gpt-4o'
+ const rawModel = process.env.OPENAI_MODEL || 'gpt-4o'
const baseUrl = process.env.OPENAI_BASE_URL || 'https://api.openai.com/v1'
const isLocal = /localhost|127\.0\.0\.1|0\.0\.0\.0/.test(baseUrl)
let name = 'OpenAI'
- if (/deepseek/i.test(baseUrl) || /deepseek/i.test(model)) name = 'DeepSeek'
+ if (/deepseek/i.test(baseUrl) || /deepseek/i.test(rawModel)) name = 'DeepSeek'
else if (/openrouter/i.test(baseUrl)) name = 'OpenRouter'
else if (/together/i.test(baseUrl)) name = 'Together AI'
else if (/groq/i.test(baseUrl)) name = 'Groq'
- else if (/mistral/i.test(baseUrl) || /mistral/i.test(model)) name = 'Mistral'
+ else if (/mistral/i.test(baseUrl) || /mistral/i.test(rawModel)) name = 'Mistral'
else if (/azure/i.test(baseUrl)) name = 'Azure OpenAI'
else if (/localhost:11434/i.test(baseUrl)) name = 'Ollama'
else if (/localhost:1234/i.test(baseUrl)) name = 'LM Studio'
- else if (/llama/i.test(model)) name = 'Meta Llama'
+ else if (/llama/i.test(rawModel)) name = 'Meta Llama'
else if (isLocal) name = 'Local'
- return { name, model, baseUrl, isLocal }
+
+ // Resolve model alias to actual model name + reasoning effort
+ let displayModel = rawModel
+ const codexAliases: Record = {
+ codexplan: { model: 'gpt-5.4', reasoningEffort: 'high' },
+ 'gpt-5.4': { model: 'gpt-5.4', reasoningEffort: 'high' },
+ 'gpt-5.3-codex': { model: 'gpt-5.3-codex', reasoningEffort: 'high' },
+ 'gpt-5.3-codex-spark': { model: 'gpt-5.3-codex-spark' },
+ codexspark: { model: 'gpt-5.3-codex-spark' },
+ 'gpt-5.2-codex': { model: 'gpt-5.2-codex', reasoningEffort: 'high' },
+ 'gpt-5.1-codex-max': { model: 'gpt-5.1-codex-max', reasoningEffort: 'high' },
+ 'gpt-5.1-codex-mini': { model: 'gpt-5.1-codex-mini' },
+ 'gpt-5.4-mini': { model: 'gpt-5.4-mini', reasoningEffort: 'medium' },
+ 'gpt-5.2': { model: 'gpt-5.2', reasoningEffort: 'medium' },
+ }
+ const alias = rawModel.toLowerCase()
+ if (alias in codexAliases) {
+ const resolved = codexAliases[alias]
+ displayModel = resolved.model
+ if (resolved.reasoningEffort) {
+ displayModel = `${displayModel} (${resolved.reasoningEffort})`
+ }
+ }
+
+ return { name, model: displayModel, baseUrl, isLocal }
}
// Default: Anthropic
diff --git a/src/hooks/useTypeahead.tsx b/src/hooks/useTypeahead.tsx
index a269902b..8183a011 100644
--- a/src/hooks/useTypeahead.tsx
+++ b/src/hooks/useTypeahead.tsx
@@ -1242,17 +1242,25 @@ export function useTypeahead({
const handleAutocompletePrevious = useCallback(() => {
setSuggestionsState(prev => ({
...prev,
- selectedSuggestion: prev.selectedSuggestion <= 0 ? suggestions.length - 1 : prev.selectedSuggestion - 1
+ selectedSuggestion: prev.suggestions.length === 0
+ ? -1
+ : prev.selectedSuggestion <= 0
+ ? prev.suggestions.length - 1
+ : Math.min(prev.selectedSuggestion - 1, prev.suggestions.length - 1)
}));
- }, [suggestions.length, setSuggestionsState]);
+ }, [setSuggestionsState]);
// Handler for autocomplete:next - selects next suggestion
const handleAutocompleteNext = useCallback(() => {
setSuggestionsState(prev => ({
...prev,
- selectedSuggestion: prev.selectedSuggestion >= suggestions.length - 1 ? 0 : prev.selectedSuggestion + 1
+ selectedSuggestion: prev.suggestions.length === 0
+ ? -1
+ : prev.selectedSuggestion >= prev.suggestions.length - 1
+ ? 0
+ : Math.max(0, prev.selectedSuggestion + 1)
}));
- }, [suggestions.length, setSuggestionsState]);
+ }, [setSuggestionsState]);
// Autocomplete context keybindings - only active when suggestions are visible
const autocompleteHandlers = useMemo(() => ({
diff --git a/src/services/api/openaiShim.ts b/src/services/api/openaiShim.ts
index 1f99b7c4..c456e9e4 100644
--- a/src/services/api/openaiShim.ts
+++ b/src/services/api/openaiShim.ts
@@ -655,9 +655,11 @@ class OpenAIShimStream {
class OpenAIShimMessages {
private defaultHeaders: Record
+ private reasoningEffort?: 'low' | 'medium' | 'high' | 'xhigh'
- constructor(defaultHeaders: Record) {
+ constructor(defaultHeaders: Record, reasoningEffort?: 'low' | 'medium' | 'high' | 'xhigh') {
this.defaultHeaders = defaultHeaders
+ this.reasoningEffort = reasoningEffort
}
create(
@@ -667,7 +669,7 @@ class OpenAIShimMessages {
const self = this
const promise = (async () => {
- const request = resolveProviderRequest({ model: params.model })
+ const request = resolveProviderRequest({ model: params.model, reasoningEffortOverride: self.reasoningEffort })
const response = await self._doRequest(request, params, options)
if (params.stream) {
@@ -993,9 +995,11 @@ class OpenAIShimMessages {
class OpenAIShimBeta {
messages: OpenAIShimMessages
+ reasoningEffort?: 'low' | 'medium' | 'high' | 'xhigh'
- constructor(defaultHeaders: Record) {
- this.messages = new OpenAIShimMessages(defaultHeaders)
+ constructor(defaultHeaders: Record, reasoningEffort?: 'low' | 'medium' | 'high' | 'xhigh') {
+ this.messages = new OpenAIShimMessages(defaultHeaders, reasoningEffort)
+ this.reasoningEffort = reasoningEffort
}
}
@@ -1003,6 +1007,7 @@ export function createOpenAIShimClient(options: {
defaultHeaders?: Record
maxRetries?: number
timeout?: number
+ reasoningEffort?: 'low' | 'medium' | 'high' | 'xhigh'
}): unknown {
hydrateGithubModelsTokenFromSecureStorage()
@@ -1025,7 +1030,7 @@ export function createOpenAIShimClient(options: {
const beta = new OpenAIShimBeta({
...(options.defaultHeaders ?? {}),
- })
+ }, options.reasoningEffort)
return {
beta,
diff --git a/src/services/api/providerConfig.ts b/src/services/api/providerConfig.ts
index 90643aa1..1c3097db 100644
--- a/src/services/api/providerConfig.ts
+++ b/src/services/api/providerConfig.ts
@@ -20,13 +20,43 @@ const CODEX_ALIAS_MODELS: Record<
model: 'gpt-5.4',
reasoningEffort: 'high',
},
+ 'gpt-5.4': {
+ model: 'gpt-5.4',
+ reasoningEffort: 'high',
+ },
+ 'gpt-5.3-codex': {
+ model: 'gpt-5.3-codex',
+ reasoningEffort: 'high',
+ },
+ 'gpt-5.3-codex-spark': {
+ model: 'gpt-5.3-codex-spark',
+ },
codexspark: {
model: 'gpt-5.3-codex-spark',
},
+ 'gpt-5.2-codex': {
+ model: 'gpt-5.2-codex',
+ reasoningEffort: 'high',
+ },
+ 'gpt-5.1-codex-max': {
+ model: 'gpt-5.1-codex-max',
+ reasoningEffort: 'high',
+ },
+ 'gpt-5.1-codex-mini': {
+ model: 'gpt-5.1-codex-mini',
+ },
+ 'gpt-5.4-mini': {
+ model: 'gpt-5.4-mini',
+ reasoningEffort: 'medium',
+ },
+ 'gpt-5.2': {
+ model: 'gpt-5.2',
+ reasoningEffort: 'medium',
+ },
} as const
type CodexAlias = keyof typeof CODEX_ALIAS_MODELS
-type ReasoningEffort = 'low' | 'medium' | 'high'
+type ReasoningEffort = 'low' | 'medium' | 'high' | 'xhigh'
export type ProviderTransport = 'chat_completions' | 'codex_responses'
@@ -102,7 +132,7 @@ function decodeJwtPayload(token: string): Record | undefined {
function parseReasoningEffort(value: string | undefined): ReasoningEffort | undefined {
if (!value) return undefined
const normalized = value.trim().toLowerCase()
- if (normalized === 'low' || normalized === 'medium' || normalized === 'high') {
+ if (normalized === 'low' || normalized === 'medium' || normalized === 'high' || normalized === 'xhigh') {
return normalized
}
return undefined
@@ -193,6 +223,7 @@ export function resolveProviderRequest(options?: {
model?: string
baseUrl?: string
fallbackModel?: string
+ reasoningEffortOverride?: ReasoningEffort
}): ResolvedProviderRequest {
const isGithubMode = isEnvTruthy(process.env.CLAUDE_CODE_USE_GITHUB)
const requestedModel =
@@ -217,6 +248,11 @@ export function resolveProviderRequest(options?: {
? normalizeGithubModelsApiModel(requestedModel)
: descriptor.baseModel
+ const reasoning = options?.reasoningEffortOverride
+ ? { effort: options.reasoningEffortOverride }
+ : descriptor.reasoning
+
+
return {
transport,
requestedModel,
@@ -227,7 +263,7 @@ export function resolveProviderRequest(options?: {
? DEFAULT_CODEX_BASE_URL
: DEFAULT_OPENAI_BASE_URL)
).replace(/\/+$/, ''),
- reasoning: descriptor.reasoning,
+ reasoning,
}
}
@@ -336,3 +372,11 @@ export function resolveCodexApiCredentials(
source: 'auth.json',
}
}
+
+export function getReasoningEffortForModel(model: string): ReasoningEffort | undefined {
+ const normalized = model.trim().toLowerCase()
+ const base = normalized.split('?', 1)[0] ?? normalized
+ const alias = base as CodexAlias
+ const aliasConfig = CODEX_ALIAS_MODELS[alias]
+ return aliasConfig?.reasoningEffort
+}
diff --git a/src/utils/effort.ts b/src/utils/effort.ts
index cafcf3de..2a391ee6 100644
--- a/src/utils/effort.ts
+++ b/src/utils/effort.ts
@@ -17,6 +17,14 @@ export const EFFORT_LEVELS = [
'max',
] as const satisfies readonly EffortLevel[]
+export const OPENAI_EFFORT_LEVELS = [
+ 'low',
+ 'medium',
+ 'high',
+ 'xhigh',
+] as const
+
+export type OpenAIEffortLevel = typeof OPENAI_EFFORT_LEVELS[number]
export type EffortValue = EffortLevel | number
// @[MODEL LAUNCH]: Add the new model to the allowlist if it supports the effort parameter.
@@ -68,6 +76,46 @@ export function isEffortLevel(value: string): value is EffortLevel {
return (EFFORT_LEVELS as readonly string[]).includes(value)
}
+export function isOpenAIEffortLevel(value: string): value is OpenAIEffortLevel {
+ return (OPENAI_EFFORT_LEVELS as readonly string[]).includes(value)
+}
+
+export function modelUsesOpenAIEffort(model: string): boolean {
+ const provider = getAPIProvider()
+ return provider === 'openai' || provider === 'codex'
+}
+
+export function getAvailableEffortLevels(model: string): EffortLevel[] | OpenAIEffortLevel[] {
+ if (modelUsesOpenAIEffort(model)) {
+ return [...OPENAI_EFFORT_LEVELS] as OpenAIEffortLevel[]
+ }
+ const levels: EffortLevel[] = ['low', 'medium', 'high']
+ if (modelSupportsMaxEffort(model)) {
+ levels.push('max')
+ }
+ return levels
+}
+
+export function getEffortLevelLabel(level: EffortLevel | OpenAIEffortLevel): string {
+ if (level === 'xhigh') return 'Extra High'
+ if (level === 'max') return 'Max'
+ return capitalize(level)
+}
+
+export function openAIEffortToStandard(level: OpenAIEffortLevel): EffortLevel {
+ if (level === 'xhigh') return 'max'
+ return level
+}
+
+export function standardEffortToOpenAI(level: EffortLevel): OpenAIEffortLevel {
+ if (level === 'max') return 'xhigh'
+ return level as OpenAIEffortLevel
+}
+
+function capitalize(s: string): string {
+ return s.charAt(0).toUpperCase() + s.slice(1)
+}
+
export function parseEffortValue(value: unknown): EffortValue | undefined {
if (value === undefined || value === null || value === '') {
return undefined
@@ -221,7 +269,7 @@ export function convertEffortValueToLevel(value: EffortValue): EffortLevel {
* @param level The effort level to describe
* @returns Human-readable description
*/
-export function getEffortLevelDescription(level: EffortLevel): string {
+export function getEffortLevelDescription(level: EffortLevel | OpenAIEffortLevel): string {
switch (level) {
case 'low':
return 'Quick, straightforward implementation with minimal overhead'
@@ -231,6 +279,8 @@ export function getEffortLevelDescription(level: EffortLevel): string {
return 'Comprehensive implementation with extensive testing and documentation'
case 'max':
return 'Maximum capability with deepest reasoning (Opus 4.6 only)'
+ case 'xhigh':
+ return 'Extra high reasoning effort for complex tasks (OpenAI/Codex)'
}
}
diff --git a/src/utils/model/aliases.ts b/src/utils/model/aliases.ts
index 91514da1..75ae388c 100644
--- a/src/utils/model/aliases.ts
+++ b/src/utils/model/aliases.ts
@@ -6,8 +6,6 @@ export const MODEL_ALIASES = [
'sonnet[1m]',
'opus[1m]',
'opusplan',
- 'codexplan',
- 'codexspark',
] as const
export type ModelAlias = (typeof MODEL_ALIASES)[number]
diff --git a/src/utils/model/model.ts b/src/utils/model/model.ts
index 6c81a8ef..97a74d95 100644
--- a/src/utils/model/model.ts
+++ b/src/utils/model/model.ts
@@ -123,6 +123,10 @@ export function getDefaultOpusModel(): ModelName {
if (getAPIProvider() === 'openai') {
return process.env.OPENAI_MODEL || 'gpt-4o'
}
+ // Codex provider: use user-specified model or default to gpt-5.4
+ if (getAPIProvider() === 'codex') {
+ return process.env.OPENAI_MODEL || 'gpt-5.4'
+ }
// 3P providers (Bedrock, Vertex, Foundry) โ kept as a separate branch
// even when values match, since 3P availability lags firstParty and
// these will diverge again at the next model launch.
@@ -145,6 +149,10 @@ export function getDefaultSonnetModel(): ModelName {
if (getAPIProvider() === 'openai') {
return process.env.OPENAI_MODEL || 'gpt-4o'
}
+ // Codex provider
+ if (getAPIProvider() === 'codex') {
+ return process.env.OPENAI_MODEL || 'gpt-5.4'
+ }
// Default to Sonnet 4.5 for 3P since they may not have 4.6 yet
if (getAPIProvider() !== 'firstParty') {
return getModelStrings().sonnet45
@@ -165,6 +173,10 @@ export function getDefaultHaikuModel(): ModelName {
if (getAPIProvider() === 'openai') {
return process.env.OPENAI_MODEL || 'gpt-4o-mini'
}
+ // Codex provider
+ if (getAPIProvider() === 'codex') {
+ return process.env.OPENAI_MODEL || 'gpt-5.4'
+ }
// Haiku 4.5 is available on all platforms (first-party, Foundry, Bedrock, Vertex)
return getModelStrings().haiku45
@@ -217,6 +229,10 @@ export function getDefaultMainLoopModelSetting(): ModelName | ModelAlias {
if (getAPIProvider() === 'openai') {
return process.env.OPENAI_MODEL || 'gpt-4o'
}
+ // Codex provider: always use the configured Codex model (default gpt-5.4)
+ if (getAPIProvider() === 'codex') {
+ return process.env.OPENAI_MODEL || 'gpt-5.4'
+ }
// Ants default to defaultModel from flag config, or Opus 1M if not configured
if (process.env.USER_TYPE === 'ant') {
@@ -343,12 +359,6 @@ export function renderDefaultModelSetting(
if (setting === 'opusplan') {
return 'Opus 4.6 in plan mode, else Sonnet 4.6'
}
- if (setting === 'codexplan') {
- return 'Codex Plan (GPT-5.4 high reasoning)'
- }
- if (setting === 'codexspark') {
- return 'Codex Spark (GPT-5.3 Codex Spark)'
- }
return renderModelName(parseUserSpecifiedModel(setting))
}
@@ -383,11 +393,12 @@ export function renderModelSetting(setting: ModelName | ModelAlias): string {
if (setting === 'opusplan') {
return 'Opus Plan'
}
+ // Handle Codex models - show actual model name + resolved model
if (setting === 'codexplan') {
- return 'Codex Plan'
+ return 'codexplan (gpt-5.4)'
}
if (setting === 'codexspark') {
- return 'Codex Spark'
+ return 'codexspark (gpt-5.3-codex-spark)'
}
if (isModelAlias(setting)) {
return capitalize(setting)
@@ -401,8 +412,8 @@ export function renderModelSetting(setting: ModelName | ModelAlias): string {
* if the model is not recognized as a public model.
*/
export function getPublicModelDisplayName(model: ModelName): string | null {
- // For OpenAI/Gemini providers, show the actual model name not a Claude alias
- if (getAPIProvider() === 'openai' || getAPIProvider() === 'gemini') {
+ // For OpenAI/Gemini/Codex providers, show the actual model name not a Claude alias
+ if (getAPIProvider() === 'openai' || getAPIProvider() === 'gemini' || getAPIProvider() === 'codex') {
return null
}
switch (model) {
@@ -517,10 +528,6 @@ export function parseUserSpecifiedModel(
if (isModelAlias(modelString)) {
switch (modelString) {
- case 'codexplan':
- return modelInputTrimmed
- case 'codexspark':
- return modelInputTrimmed
case 'opusplan':
return getDefaultSonnetModel() + (has1mTag ? '[1m]' : '') // Sonnet is default, Opus in plan mode
case 'sonnet':
@@ -535,6 +542,14 @@ export function parseUserSpecifiedModel(
}
}
+ // Handle Codex aliases - map to actual model names
+ if (modelString === 'codexplan') {
+ return 'gpt-5.4'
+ }
+ if (modelString === 'codexspark') {
+ return 'gpt-5.3-codex-spark'
+ }
+
// Opus 4/4.1 are no longer available on the first-party API (same as
// Claude.ai) โ silently remap to the current Opus default. The 'opus'
// alias already resolves to 4.6, so the only users on these explicit
diff --git a/src/utils/model/modelOptions.ts b/src/utils/model/modelOptions.ts
index 0c464d6a..84371c84 100644
--- a/src/utils/model/modelOptions.ts
+++ b/src/utils/model/modelOptions.ts
@@ -268,20 +268,65 @@ function getOpusPlanOption(): ModelOption {
function getCodexPlanOption(): ModelOption {
return {
- value: 'codexplan',
- label: 'Codex Plan',
+ value: 'gpt-5.4',
+ label: 'gpt-5.4',
description: 'GPT-5.4 on the Codex backend with high reasoning',
}
}
function getCodexSparkOption(): ModelOption {
return {
- value: 'codexspark',
- label: 'Codex Spark',
+ value: 'gpt-5.3-codex-spark',
+ label: 'gpt-5.3-codex-spark',
description: 'GPT-5.3 Codex Spark on the Codex backend for fast tool loops',
}
}
+function getCodexModelOptions(): ModelOption[] {
+ return [
+ {
+ value: 'gpt-5.4',
+ label: 'gpt-5.4',
+ description: 'GPT-5.4 with high reasoning',
+ },
+ {
+ value: 'gpt-5.3-codex',
+ label: 'gpt-5.3-codex',
+ description: 'GPT-5.3 Codex with high reasoning',
+ },
+ {
+ value: 'gpt-5.3-codex-spark',
+ label: 'gpt-5.3-codex-spark',
+ description: 'GPT-5.3 Codex Spark for fast tool loops',
+ },
+ {
+ value: 'codexspark',
+ label: 'codexspark',
+ description: 'GPT-5.3 Codex Spark alias for fast tool loops',
+ },
+ {
+ value: 'gpt-5.2-codex',
+ label: 'gpt-5.2-codex',
+ description: 'GPT-5.2 Codex with high reasoning',
+ },
+ {
+ value: 'gpt-5.1-codex-max',
+ label: 'gpt-5.1-codex-max',
+ description: 'GPT-5.1 Codex Max for deep reasoning',
+ },
+ {
+ value: 'gpt-5.1-codex-mini',
+ label: 'gpt-5.1-codex-mini',
+ description: 'GPT-5.1 Codex Mini - faster, cheaper',
+ },
+ {
+ value: 'gpt-5.4-mini',
+ label: 'gpt-5.4-mini',
+ description: 'GPT-5.4 Mini - faster, cheaper',
+ },
+ ]
+}
+
// @[MODEL LAUNCH]: Update the model picker lists below to include/reorder options for the new model.
// Each user tier (ant, Max/Team Premium, Pro/Team Standard/Enterprise, PAYG 1P, PAYG 3P) has its own list.
function getModelOptionsBase(fastMode = false): ModelOption[] {
@@ -360,8 +405,9 @@ function getModelOptionsBase(fastMode = false): ModelOption[] {
// PAYG 3P: Default (Sonnet 4.5) + Sonnet (3P custom) or Sonnet 4.6/1M + Opus (3P custom) or Opus 4.1/Opus 4.6/Opus1M + Haiku + Opus 4.1
const payg3pOptions = [getDefaultOptionForUser(fastMode)]
- if (getAPIProvider() === 'openai') {
- payg3pOptions.push(getCodexPlanOption(), getCodexSparkOption())
+ // Add Codex models for openai and codex providers
+ if (getAPIProvider() === 'openai' || getAPIProvider() === 'codex') {
+ payg3pOptions.push(...getCodexModelOptions())
}
const customSonnet = getCustomSonnetOption()
@@ -517,9 +563,9 @@ export function getModelOptions(fastMode = false): ModelOption[] {
return filterModelOptionsByAllowlist(options)
} else if (customModel === 'opusplan') {
return filterModelOptionsByAllowlist([...options, getOpusPlanOption()])
- } else if (customModel === 'codexplan') {
+ } else if (customModel === 'gpt-5.4') {
return filterModelOptionsByAllowlist([...options, getCodexPlanOption()])
- } else if (customModel === 'codexspark') {
+ } else if (customModel === 'gpt-5.3-codex-spark') {
return filterModelOptionsByAllowlist([...options, getCodexSparkOption()])
} else if (customModel === 'opus' && getAPIProvider() === 'firstParty') {
return filterModelOptionsByAllowlist([
@@ -554,11 +600,23 @@ export function getModelOptions(fastMode = false): ModelOption[] {
*/
function filterModelOptionsByAllowlist(options: ModelOption[]): ModelOption[] {
const settings = getSettings_DEPRECATED() || {}
- if (!settings.availableModels) {
- return options // No restrictions
- }
- return options.filter(
+ const filtered = !settings.availableModels
+ ? options // No restrictions
+ : options.filter(
opt =>
opt.value === null || (opt.value !== null && isModelAllowed(opt.value)),
)
+
+ // Select state uses option values as identity keys. If two entries share the
+ // same value (e.g. provider-specific aliases collapsing to one model ID),
+ // navigation/focus can become inconsistent and appear as duplicate rendering.
+ const seen = new Set()
+ return filtered.filter(opt => {
+ const key = String(opt.value)
+ if (seen.has(key)) {
+ return false
+ }
+ seen.add(key)
+ return true
+ })
}
diff --git a/src/utils/model/modelStrings.ts b/src/utils/model/modelStrings.ts
index 5b7be104..4d8399d1 100644
--- a/src/utils/model/modelStrings.ts
+++ b/src/utils/model/modelStrings.ts
@@ -23,9 +23,12 @@ export type ModelStrings = Record
const MODEL_KEYS = Object.keys(ALL_MODEL_CONFIGS) as ModelKey[]
function getBuiltinModelStrings(provider: APIProvider): ModelStrings {
+ // Codex piggybacks on the OpenAI provider transport for Anthropic tier aliases.
+ // Reuse OpenAI mappings so model string lookups never return undefined.
+ const providerKey = provider === 'codex' ? 'openai' : provider
const out = {} as ModelStrings
for (const key of MODEL_KEYS) {
- out[key] = ALL_MODEL_CONFIGS[key][provider]
+ out[key] = ALL_MODEL_CONFIGS[key][providerKey]
}
return out
}
diff --git a/src/utils/model/providers.ts b/src/utils/model/providers.ts
index 30a1f1c9..6b6d627e 100644
--- a/src/utils/model/providers.ts
+++ b/src/utils/model/providers.ts
@@ -9,6 +9,7 @@ export type APIProvider =
| 'openai'
| 'gemini'
| 'github'
+ | 'codex'
export function getAPIProvider(): APIProvider {
return isEnvTruthy(process.env.CLAUDE_CODE_USE_GEMINI)
@@ -16,7 +17,9 @@ export function getAPIProvider(): APIProvider {
: isEnvTruthy(process.env.CLAUDE_CODE_USE_GITHUB)
? 'github'
: isEnvTruthy(process.env.CLAUDE_CODE_USE_OPENAI)
- ? 'openai'
+ ? isCodexModel()
+ ? 'codex'
+ : 'openai'
: isEnvTruthy(process.env.CLAUDE_CODE_USE_BEDROCK)
? 'bedrock'
: isEnvTruthy(process.env.CLAUDE_CODE_USE_VERTEX)
@@ -29,6 +32,19 @@ export function getAPIProvider(): APIProvider {
export function usesAnthropicAccountFlow(): boolean {
return getAPIProvider() === 'firstParty'
}
+function isCodexModel(): boolean {
+ const model = (process.env.OPENAI_MODEL || '').toLowerCase()
+ return (
+ model === 'codexplan' ||
+ model === 'codexspark' ||
+ model === 'gpt-5.4' ||
+ model === 'gpt-5.3-codex' ||
+ model === 'gpt-5.3-codex-spark' ||
+ model === 'gpt-5.2-codex' ||
+ model === 'gpt-5.1-codex-max' ||
+ model === 'gpt-5.1-codex-mini'
+ )
+}
export function getAPIProviderForStatsig(): AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS {
return getAPIProvider() as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS
diff --git a/src/utils/status.tsx b/src/utils/status.tsx
index bc159cdb..97e8c742 100644
--- a/src/utils/status.tsx
+++ b/src/utils/status.tsx
@@ -12,6 +12,7 @@ import { formatNumber } from './format.js';
import { getIdeClientName, type IDEExtensionInstallationStatus, isJetBrainsIde, toIDEDisplayName } from './ide.js';
import { getClaudeAiUserDefaultModelDescription, modelDisplayString } from './model/model.js';
import { getAPIProvider } from './model/providers.js';
+import { resolveProviderRequest } from '../services/api/providerConfig.js';
import { getMTLSConfig } from './mtls.js';
import { checkInstall } from './nativeInstaller/index.js';
import { getProxyUrl } from './proxy.js';
@@ -246,6 +247,7 @@ export function buildAPIProviderProperties(): Property[] {
vertex: 'Google Vertex AI',
foundry: 'Microsoft Foundry',
openai: 'OpenAI-compatible',
+ codex: 'Codex',
gemini: 'Google Gemini',
}[apiProvider];
properties.push({
@@ -332,9 +334,46 @@ export function buildAPIProviderProperties(): Property[] {
}
const openaiModel = process.env.OPENAI_MODEL;
if (openaiModel) {
+ // Build display model string with resolved model + reasoning effort
+ let modelDisplay = openaiModel;
+ const resolvedModel = resolveProviderRequest({ model: openaiModel }).resolvedModel;
+ const reasoningEffort = resolveProviderRequest({ model: openaiModel }).reasoning?.effort;
+ if (resolvedModel && resolvedModel !== openaiModel.toLowerCase()) {
+ // Show resolved model name
+ modelDisplay = resolvedModel;
+ }
+ if (reasoningEffort) {
+ modelDisplay = `${modelDisplay} (${reasoningEffort})`;
+ }
properties.push({
label: 'Model',
- value: openaiModel
+ value: modelDisplay
+ });
+ }
+ } else if (apiProvider === 'codex') {
+ const codexBaseUrl = process.env.OPENAI_BASE_URL;
+ if (codexBaseUrl) {
+ properties.push({
+ label: 'Codex base URL',
+ value: codexBaseUrl
+ });
+ }
+ const openaiModel = process.env.OPENAI_MODEL;
+ if (openaiModel) {
+ // Build display model string with resolved model + reasoning effort
+ let modelDisplay = openaiModel;
+ const resolvedModel = resolveProviderRequest({ model: openaiModel }).resolvedModel;
+ const reasoningEffort = resolveProviderRequest({ model: openaiModel }).reasoning?.effort;
+ if (resolvedModel && resolvedModel !== openaiModel.toLowerCase()) {
+ // Show resolved model name
+ modelDisplay = resolvedModel;
+ }
+ if (reasoningEffort) {
+ modelDisplay = `${modelDisplay} (${reasoningEffort})`;
+ }
+ properties.push({
+ label: 'Model',
+ value: modelDisplay
});
}
} else if (apiProvider === 'gemini') {
diff --git a/src/utils/suggestions/commandSuggestions.ts b/src/utils/suggestions/commandSuggestions.ts
index 4a90db55..2f83ae6f 100644
--- a/src/utils/suggestions/commandSuggestions.ts
+++ b/src/utils/suggestions/commandSuggestions.ts
@@ -286,6 +286,25 @@ function createCommandSuggestionItem(
}
}
+/**
+ * Ensure suggestion IDs are unique for React keys and selection logic.
+ * If duplicates exist, append a stable numeric suffix to subsequent entries.
+ */
+function ensureUniqueSuggestionIds(items: SuggestionItem[]): SuggestionItem[] {
+ const counts = new Map()
+ return items.map(item => {
+ const seen = counts.get(item.id) ?? 0
+ counts.set(item.id, seen + 1)
+ if (seen === 0) {
+ return item
+ }
+ return {
+ ...item,
+ id: `${item.id}#${seen + 1}`,
+ }
+ })
+}
+
/**
* Generate command suggestions based on input
*/
@@ -369,14 +388,14 @@ export function generateCommandSuggestions(
// Combine with built-in commands prioritized after recently used,
// so they remain visible even when many skills are installed
- return [
+ return ensureUniqueSuggestionIds([
...recentlyUsed,
...builtinCommands,
...userCommands,
...projectCommands,
...policyCommands,
...otherCommands,
- ].map(cmd => createCommandSuggestionItem(cmd))
+ ].map(cmd => createCommandSuggestionItem(cmd)))
}
// The Fuse index filters isHidden at build time and is keyed on the
@@ -491,10 +510,13 @@ export function generateCommandSuggestions(
if (hiddenExact) {
const hiddenId = getCommandId(hiddenExact)
if (!fuseSuggestions.some(s => s.id === hiddenId)) {
- return [createCommandSuggestionItem(hiddenExact), ...fuseSuggestions]
+ return ensureUniqueSuggestionIds([
+ createCommandSuggestionItem(hiddenExact),
+ ...fuseSuggestions,
+ ])
}
}
- return fuseSuggestions
+ return ensureUniqueSuggestionIds(fuseSuggestions)
}
/**