From 8b6aaf521ec9c4b722f28d5fb9f15e430b4d1ba1 Mon Sep 17 00:00:00 2001 From: boggedbrush <90526147+boggedbrush@users.noreply.github.com> Date: Wed, 29 Apr 2026 12:21:22 -0400 Subject: [PATCH 1/3] feat(codex): add gpt-5.5 support --- apps/desktop/scripts/electron-launcher.mjs | 80 ++++++++++++-- apps/desktop/src/main.ts | 35 +----- apps/server/src/codexAppServerManager.test.ts | 10 +- apps/server/src/codexAppServerManager.ts | 3 +- apps/server/src/os-jank.ts | 6 +- .../persistence/Layers/ProjectionThreads.ts | 2 +- .../src/provider/Layers/CodexProvider.ts | 101 ++++-------------- .../provider/Layers/ProviderRegistry.test.ts | 10 ++ apps/server/src/provider/codexAccount.ts | 4 +- apps/server/src/serverSettings.test.ts | 66 +++++++++--- apps/server/src/serverSettings.ts | 48 +++++---- .../components/chat/ModelPickerContent.tsx | 13 ++- .../chat/ProviderModelPicker.browser.tsx | 23 ++++ .../components/chat/ProviderModelPicker.tsx | 2 + .../settings/SettingsModelsSection.test.tsx | 6 ++ .../settings/SettingsModelsSection.tsx | 46 ++++---- .../settings/SettingsPanelPrimitives.tsx | 1 + packages/contracts/src/model.ts | 3 +- packages/shared/src/model.test.ts | 3 +- scripts/build-desktop-artifact.ts | 2 +- 20 files changed, 273 insertions(+), 191 deletions(-) diff --git a/apps/desktop/scripts/electron-launcher.mjs b/apps/desktop/scripts/electron-launcher.mjs index 1bd358fc..ae85448a 100644 --- a/apps/desktop/scripts/electron-launcher.mjs +++ b/apps/desktop/scripts/electron-launcher.mjs @@ -10,6 +10,7 @@ import { readdirSync, rmSync, statSync, + symlinkSync, writeFileSync, } from "node:fs"; import { createRequire } from "node:module"; @@ -18,8 +19,8 @@ import { fileURLToPath } from "node:url"; const isDevelopment = Boolean(process.env.VITE_DEV_SERVER_URL); const APP_DISPLAY_NAME = isDevelopment ? "Kodo Code (Dev)" : "Kodo Code (Alpha)"; -const APP_BUNDLE_ID = isDevelopment ? "com.kodo.code.dev" : "com.kodo.code"; -const LAUNCHER_VERSION = 2; +const APP_BUNDLE_ID = isDevelopment ? "app.kodocode.dev" : "app.kodocode"; +const LAUNCHER_VERSION = 4; const ELECTRON_INSTALL_CANDIDATES = ["node", "bun"]; const __dirname = dirname(fileURLToPath(import.meta.url)); @@ -46,8 +47,33 @@ function setPlistString(plistPath, key, value) { throw new Error(`Failed to update plist key "${key}" at ${plistPath}: ${details}`.trim()); } +function ensureMainBundleInfoPlist(appBundlePath) { + const contentsDir = join(appBundlePath, "Contents"); + const infoPlistPath = join(contentsDir, "Info.plist"); + if (existsSync(infoPlistPath)) { + return infoPlistPath; + } + + mkdirSync(contentsDir, { recursive: true }); + writeFileSync( + infoPlistPath, + ` + + + + CFBundleExecutable + Electron + CFBundlePackageType + APPL + + +`, + ); + return infoPlistPath; +} + function patchMainBundleInfoPlist(appBundlePath, iconPath) { - const infoPlistPath = join(appBundlePath, "Contents", "Info.plist"); + const infoPlistPath = ensureMainBundleInfoPlist(appBundlePath); setPlistString(infoPlistPath, "CFBundleDisplayName", APP_DISPLAY_NAME); setPlistString(infoPlistPath, "CFBundleName", APP_DISPLAY_NAME); setPlistString(infoPlistPath, "CFBundleIdentifier", APP_BUNDLE_ID); @@ -92,6 +118,45 @@ function patchHelperBundleInfoPlists(appBundlePath) { } } +function repairFrameworkVersionSymlinks(appBundlePath) { + const frameworksDir = join(appBundlePath, "Contents", "Frameworks"); + if (!existsSync(frameworksDir)) { + return; + } + + for (const entry of readdirSync(frameworksDir, { withFileTypes: true })) { + if (!entry.isDirectory() || !entry.name.endsWith(".framework")) { + continue; + } + + const frameworkPath = join(frameworksDir, entry.name); + const frameworkExecutable = entry.name.replace(/\.framework$/, ""); + const versionsDir = join(frameworkPath, "Versions"); + const versionAPath = join(versionsDir, "A"); + const currentPath = join(versionsDir, "Current"); + if (!existsSync(versionAPath)) { + continue; + } + + if (!existsSync(currentPath)) { + rmSync(currentPath, { force: true }); + symlinkSync("A", currentPath); + } + + const executablePath = join(frameworkPath, frameworkExecutable); + if (existsSync(join(versionAPath, frameworkExecutable))) { + rmSync(executablePath, { force: true }); + symlinkSync(`Versions/Current/${frameworkExecutable}`, executablePath); + } + + const resourcesPath = join(frameworkPath, "Resources"); + if (existsSync(join(versionAPath, "Resources"))) { + rmSync(resourcesPath, { force: true }); + symlinkSync("Versions/Current/Resources", resourcesPath); + } + } +} + function readJson(path) { try { return JSON.parse(readFileSync(path, "utf8")); @@ -198,6 +263,7 @@ function buildMacLauncher(electronBinaryPath) { const runtimeDir = join(desktopDir, ".electron-runtime"); const targetAppBundlePath = join(runtimeDir, `${APP_DISPLAY_NAME}.app`); const targetBinaryPath = join(targetAppBundlePath, "Contents", "MacOS", "Electron"); + const targetInfoPlistPath = join(targetAppBundlePath, "Contents", "Info.plist"); const iconPath = resolveMacLauncherIconPath(); const metadataPath = join(runtimeDir, "metadata.json"); @@ -205,10 +271,10 @@ function buildMacLauncher(electronBinaryPath) { const expectedMetadata = { launcherVersion: LAUNCHER_VERSION, + appDisplayName: APP_DISPLAY_NAME, + appBundleId: APP_BUNDLE_ID, sourceAppBundlePath, sourceAppMtimeMs: statSync(sourceAppBundlePath).mtimeMs, - appBundleId: APP_BUNDLE_ID, - appDisplayName: APP_DISPLAY_NAME, iconPath, iconMtimeMs: statSync(iconPath).mtimeMs, }; @@ -216,6 +282,7 @@ function buildMacLauncher(electronBinaryPath) { const currentMetadata = readJson(metadataPath); if ( existsSync(targetBinaryPath) && + existsSync(targetInfoPlistPath) && currentMetadata && JSON.stringify(currentMetadata) === JSON.stringify(expectedMetadata) ) { @@ -223,7 +290,8 @@ function buildMacLauncher(electronBinaryPath) { } rmSync(targetAppBundlePath, { recursive: true, force: true }); - cpSync(sourceAppBundlePath, targetAppBundlePath, { recursive: true }); + cpSync(sourceAppBundlePath, targetAppBundlePath, { recursive: true, verbatimSymlinks: true }); + repairFrameworkVersionSymlinks(targetAppBundlePath); patchMainBundleInfoPlist(targetAppBundlePath, iconPath); patchHelperBundleInfoPlists(targetAppBundlePath); writeFileSync(metadataPath, `${JSON.stringify(expectedMetadata, null, 2)}\n`); diff --git a/apps/desktop/src/main.ts b/apps/desktop/src/main.ts index 5ab01ff0..f4dbba69 100644 --- a/apps/desktop/src/main.ts +++ b/apps/desktop/src/main.ts @@ -76,7 +76,7 @@ const LEGACY_DESKTOP_SCHEME = "t3"; const ROOT_DIR = Path.resolve(__dirname, "../../.."); const isDevelopment = Boolean(process.env.VITE_DEV_SERVER_URL); const APP_DISPLAY_NAME = isDevelopment ? "Kodo Code (Dev)" : "Kodo Code (Alpha)"; -const APP_USER_MODEL_ID = "com.kodo.code"; +const APP_USER_MODEL_ID = "app.kodocode"; const LINUX_DESKTOP_ENTRY_NAME = isDevelopment ? "kodo-code-dev.desktop" : "kodo-code.desktop"; const LINUX_WM_CLASS = isDevelopment ? "kodo-code-dev" : "kodo-code"; const USER_DATA_DIR_NAME = isDevelopment ? "kodo-code-dev" : "kodo-code"; @@ -803,32 +803,7 @@ function resolveIconPath(ext: "ico" | "icns" | "png"): string | null { return resolveResourcePath(`icon.${ext}`); } -function resolveMacDockIcon(): Electron.NativeImage | null { - const iconPath = resolveIconPath("icns"); - if (!iconPath) { - return null; - } - - const icon = nativeImage.createFromPath(iconPath); - return icon.isEmpty() ? null : icon; -} - -function resolveVectorIconPath(): string | null { - const candidate = isDevelopment - ? Path.join(ROOT_DIR, "assets/dev/blueprint.svg") - : Path.join(ROOT_DIR, "assets/prod/logo.svg"); - return FS.existsSync(candidate) ? candidate : null; -} - function resolveNativeAppIcon(ext: "ico" | "icns" | "png"): Electron.NativeImage | null { - const vectorIconPath = resolveVectorIconPath(); - if (vectorIconPath) { - const vectorIcon = nativeImage.createFromPath(vectorIconPath); - if (!vectorIcon.isEmpty()) { - return vectorIcon; - } - } - const rasterIconPath = resolveIconPath(ext); if (!rasterIconPath) { return null; @@ -885,12 +860,8 @@ function configureAppIdentity(): void { (app as LinuxDesktopNamedApp).setDesktopName?.(LINUX_DESKTOP_ENTRY_NAME); } - if (process.platform === "darwin" && app.dock) { - const icon = resolveMacDockIcon(); - if (icon) { - app.dock.setIcon(icon); - } - } + // macOS uses CFBundleIconFile from the app bundle. Calling app.dock.setIcon() + // renders the raw image payload and bypasses the bundle icon presentation. } function clearUpdatePollTimer(): void { diff --git a/apps/server/src/codexAppServerManager.test.ts b/apps/server/src/codexAppServerManager.test.ts index 708ebf0a..1a2e6db1 100644 --- a/apps/server/src/codexAppServerManager.test.ts +++ b/apps/server/src/codexAppServerManager.test.ts @@ -3,7 +3,7 @@ import { randomUUID } from "node:crypto"; import { mkdtempSync, rmSync, writeFileSync } from "node:fs"; import os from "node:os"; import path from "node:path"; -import { ApprovalRequestId, ThreadId } from "@t3tools/contracts"; +import { ApprovalRequestId, DEFAULT_MODEL_BY_PROVIDER, ThreadId } from "@t3tools/contracts"; import { buildCodexInitializeParams, @@ -249,6 +249,10 @@ describe("normalizeCodexModelSlug", () => { expect(normalizeCodexModelSlug("gpt-5.3")).toBe("gpt-5.3-codex"); }); + it("maps 5.5 aliases to gpt-5.5", () => { + expect(normalizeCodexModelSlug("5.5")).toBe("gpt-5.5"); + }); + it("prefers codex id when model differs", () => { expect(normalizeCodexModelSlug("gpt-5.3", "gpt-5.3-codex")).toBe("gpt-5.3-codex"); }); @@ -381,7 +385,7 @@ describe("resolveCodexModelForAccount", () => { planType: "plus", sparkEnabled: false, }), - ).toBe("gpt-5.3-codex"); + ).toBe(DEFAULT_MODEL_BY_PROVIDER.codex); }); it("keeps spark for supported plans", () => { @@ -401,7 +405,7 @@ describe("resolveCodexModelForAccount", () => { planType: null, sparkEnabled: false, }), - ).toBe("gpt-5.3-codex"); + ).toBe(DEFAULT_MODEL_BY_PROVIDER.codex); }); }); diff --git a/apps/server/src/codexAppServerManager.ts b/apps/server/src/codexAppServerManager.ts index b4cc8e5c..c4e3649d 100644 --- a/apps/server/src/codexAppServerManager.ts +++ b/apps/server/src/codexAppServerManager.ts @@ -27,6 +27,7 @@ import { parseCodexCliVersion, } from "./provider/codexCliVersion"; import { + CODEX_DEFAULT_MODEL, readCodexAccountSnapshot, resolveCodexModelForAccount, type CodexAccountSnapshot, @@ -384,7 +385,7 @@ function buildCodexCollaborationMode(input: { } const codexMode = input.interactionMode === "plan" || input.interactionMode === "review" ? "plan" : "default"; - const model = normalizeCodexModelSlug(input.model) ?? "gpt-5.3-codex"; + const model = normalizeCodexModelSlug(input.model) ?? CODEX_DEFAULT_MODEL; return { mode: codexMode, settings: { diff --git a/apps/server/src/os-jank.ts b/apps/server/src/os-jank.ts index 5ea849cc..011ef196 100644 --- a/apps/server/src/os-jank.ts +++ b/apps/server/src/os-jank.ts @@ -56,9 +56,9 @@ export function fixPath( } } -export const expandHomePath = Effect.fn(function* (input: string) { - return expandHomePathValue(input); -}); +export const expandHomePath = Effect.fn((input: string) => + Effect.succeed(expandHomePathValue(input)), +); export const resolveBaseDir = Effect.fn(function* (raw: string | undefined) { const { join, resolve } = yield* Path.Path; diff --git a/apps/server/src/persistence/Layers/ProjectionThreads.ts b/apps/server/src/persistence/Layers/ProjectionThreads.ts index c5e17343..05134458 100644 --- a/apps/server/src/persistence/Layers/ProjectionThreads.ts +++ b/apps/server/src/persistence/Layers/ProjectionThreads.ts @@ -1,6 +1,6 @@ import * as SqlClient from "effect/unstable/sql/SqlClient"; import * as SqlSchema from "effect/unstable/sql/SqlSchema"; -import { Effect, Layer, Option, Schema, Struct } from "effect"; +import { Effect, Layer, Option, Schema } from "effect"; import { toPersistenceSqlError } from "../Errors.ts"; import { diff --git a/apps/server/src/provider/Layers/CodexProvider.ts b/apps/server/src/provider/Layers/CodexProvider.ts index 834c6ef8..e6c647ce 100644 --- a/apps/server/src/provider/Layers/CodexProvider.ts +++ b/apps/server/src/provider/Layers/CodexProvider.ts @@ -50,123 +50,68 @@ import { ServerSettingsService } from "../../serverSettings"; import { expandHomePath } from "../../pathExpansion"; import { ServerSettingsError } from "@t3tools/contracts"; -const DEFAULT_CODEX_MODEL_CAPABILITIES: ModelCapabilities = { +const codexModelCapabilities = ( + defaultReasoningEffort: "xhigh" | "high" | "medium" | "low" = "high", +): ModelCapabilities => ({ reasoningEffortLevels: [ - { value: "xhigh", label: "Extra High" }, - { value: "high", label: "High", isDefault: true }, - { value: "medium", label: "Medium" }, - { value: "low", label: "Low" }, + { value: "xhigh", label: "Extra High", isDefault: defaultReasoningEffort === "xhigh" }, + { value: "high", label: "High", isDefault: defaultReasoningEffort === "high" }, + { value: "medium", label: "Medium", isDefault: defaultReasoningEffort === "medium" }, + { value: "low", label: "Low", isDefault: defaultReasoningEffort === "low" }, ], supportsFastMode: true, supportsThinkingToggle: false, contextWindowOptions: [], promptInjectedEffortLevels: [], -}; +}); + +const DEFAULT_CODEX_MODEL_CAPABILITIES: ModelCapabilities = codexModelCapabilities(); const PROVIDER = "codex" as const; const OPENAI_AUTH_PROVIDERS = new Set(["openai"]); + const BUILT_IN_MODELS: ReadonlyArray = [ + { + slug: "gpt-5.5", + name: "GPT-5.5", + isCustom: false, + capabilities: codexModelCapabilities("low"), + }, { slug: "gpt-5.4", name: "GPT-5.4", isCustom: false, - capabilities: { - reasoningEffortLevels: [ - { value: "xhigh", label: "Extra High" }, - { value: "high", label: "High", isDefault: true }, - { value: "medium", label: "Medium" }, - { value: "low", label: "Low" }, - ], - supportsFastMode: true, - supportsThinkingToggle: false, - contextWindowOptions: [], - promptInjectedEffortLevels: [], - }, + capabilities: codexModelCapabilities(), }, { slug: "gpt-5.4-mini", name: "GPT-5.4 Mini", isCustom: false, - capabilities: { - reasoningEffortLevels: [ - { value: "xhigh", label: "Extra High" }, - { value: "high", label: "High", isDefault: true }, - { value: "medium", label: "Medium" }, - { value: "low", label: "Low" }, - ], - supportsFastMode: true, - supportsThinkingToggle: false, - contextWindowOptions: [], - promptInjectedEffortLevels: [], - }, + capabilities: codexModelCapabilities(), }, { slug: "gpt-5.3-codex", name: "GPT-5.3 Codex", isCustom: false, - capabilities: { - reasoningEffortLevels: [ - { value: "xhigh", label: "Extra High" }, - { value: "high", label: "High", isDefault: true }, - { value: "medium", label: "Medium" }, - { value: "low", label: "Low" }, - ], - supportsFastMode: true, - supportsThinkingToggle: false, - contextWindowOptions: [], - promptInjectedEffortLevels: [], - }, + capabilities: codexModelCapabilities(), }, { slug: "gpt-5.3-codex-spark", name: "GPT-5.3 Codex Spark", isCustom: false, - capabilities: { - reasoningEffortLevels: [ - { value: "xhigh", label: "Extra High" }, - { value: "high", label: "High", isDefault: true }, - { value: "medium", label: "Medium" }, - { value: "low", label: "Low" }, - ], - supportsFastMode: true, - supportsThinkingToggle: false, - contextWindowOptions: [], - promptInjectedEffortLevels: [], - }, + capabilities: codexModelCapabilities(), }, { slug: "gpt-5.2-codex", name: "GPT-5.2 Codex", isCustom: false, - capabilities: { - reasoningEffortLevels: [ - { value: "xhigh", label: "Extra High" }, - { value: "high", label: "High", isDefault: true }, - { value: "medium", label: "Medium" }, - { value: "low", label: "Low" }, - ], - supportsFastMode: true, - supportsThinkingToggle: false, - contextWindowOptions: [], - promptInjectedEffortLevels: [], - }, + capabilities: codexModelCapabilities(), }, { slug: "gpt-5.2", name: "GPT-5.2", isCustom: false, - capabilities: { - reasoningEffortLevels: [ - { value: "xhigh", label: "Extra High" }, - { value: "high", label: "High", isDefault: true }, - { value: "medium", label: "Medium" }, - { value: "low", label: "Low" }, - ], - supportsFastMode: true, - supportsThinkingToggle: false, - contextWindowOptions: [], - promptInjectedEffortLevels: [], - }, + capabilities: codexModelCapabilities(), }, ]; diff --git a/apps/server/src/provider/Layers/ProviderRegistry.test.ts b/apps/server/src/provider/Layers/ProviderRegistry.test.ts index 43af723d..cdee14da 100644 --- a/apps/server/src/provider/Layers/ProviderRegistry.test.ts +++ b/apps/server/src/provider/Layers/ProviderRegistry.test.ts @@ -176,6 +176,16 @@ it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsService.layerTest()))( assert.strictEqual(status.status, "ready"); assert.strictEqual(status.installed, true); assert.strictEqual(status.auth.status, "authenticated"); + assert.strictEqual( + status.models.some((model) => model.slug === "gpt-5.5" && model.name === "GPT-5.5"), + true, + ); + const gpt55 = status.models.find((model) => model.slug === "gpt-5.5"); + assert.ok(gpt55?.capabilities); + assert.strictEqual( + gpt55.capabilities.reasoningEffortLevels.find((level) => level.isDefault)?.value, + "low", + ); }).pipe( Effect.provide( mockSpawnerLayer((args) => { diff --git a/apps/server/src/provider/codexAccount.ts b/apps/server/src/provider/codexAccount.ts index 52e59a57..8812c035 100644 --- a/apps/server/src/provider/codexAccount.ts +++ b/apps/server/src/provider/codexAccount.ts @@ -1,4 +1,4 @@ -import type { ServerProviderModel } from "@t3tools/contracts"; +import { DEFAULT_MODEL_BY_PROVIDER, type ServerProviderModel } from "@t3tools/contracts"; export type CodexPlanType = | "free" @@ -18,7 +18,7 @@ export interface CodexAccountSnapshot { readonly sparkEnabled: boolean; } -export const CODEX_DEFAULT_MODEL = "gpt-5.3-codex"; +export const CODEX_DEFAULT_MODEL = DEFAULT_MODEL_BY_PROVIDER.codex; export const CODEX_SPARK_MODEL = "gpt-5.3-codex-spark"; const CODEX_SPARK_ENABLED_PLAN_TYPES = new Set(["pro"]); diff --git a/apps/server/src/serverSettings.test.ts b/apps/server/src/serverSettings.test.ts index d9ca3e39..c03c590a 100644 --- a/apps/server/src/serverSettings.test.ts +++ b/apps/server/src/serverSettings.test.ts @@ -455,11 +455,45 @@ it.layer(NodeServices.layer)("server settings", (it) => { assert.equal(next.modelSelectionPresets.codex[DEFAULT_MODEL_SELECTION_PRESET_ID], undefined); assert.equal(next.modelSelectionPresets.codex["starter-codex-free"]?.name, "Free"); + assert.deepEqual( + next.modelSelectionPresets.codex["starter-codex-free"]?.reviewModelSelection, + { + provider: "codex", + model: "gpt-5.4-mini", + options: { + reasoningEffort: "high", + }, + }, + ); + assert.deepEqual(next.modelSelectionPresets.codex["starter-codex-go"]?.planModelSelection, { + provider: "codex", + model: "gpt-5.4-mini", + options: { + reasoningEffort: "high", + }, + }); + assert.deepEqual(next.modelSelectionPresets.codex["starter-codex-plus"]?.planModelSelection, { + provider: "codex", + model: "gpt-5.5", + options: { + reasoningEffort: "medium", + }, + }); + assert.deepEqual( + next.modelSelectionPresets.codex["starter-codex-plus"]?.reviewModelSelection, + { + provider: "codex", + model: "gpt-5.5", + options: { + reasoningEffort: "low", + }, + }, + ); assert.deepEqual( next.modelSelectionPresets.codex["starter-codex-pro-5x"]?.askModelSelection, { provider: "codex", - model: "gpt-5.4", + model: "gpt-5.5", options: { reasoningEffort: "low", }, @@ -469,9 +503,9 @@ it.layer(NodeServices.layer)("server settings", (it) => { next.modelSelectionPresets.codex["starter-codex-pro-5x"]?.codeModelSelection, { provider: "codex", - model: "gpt-5.3-codex", + model: "gpt-5.5", options: { - reasoningEffort: "high", + reasoningEffort: "medium", }, }, ); @@ -479,9 +513,9 @@ it.layer(NodeServices.layer)("server settings", (it) => { next.modelSelectionPresets.codex["starter-codex-pro-20x"]?.askModelSelection, { provider: "codex", - model: "gpt-5.4", + model: "gpt-5.5", options: { - reasoningEffort: "medium", + reasoningEffort: "low", }, }, ); @@ -489,9 +523,9 @@ it.layer(NodeServices.layer)("server settings", (it) => { next.modelSelectionPresets.codex["starter-codex-pro-20x"]?.reviewModelSelection, { provider: "codex", - model: "gpt-5.3-codex", + model: "gpt-5.5", options: { - reasoningEffort: "xhigh", + reasoningEffort: "high", }, }, ); @@ -515,21 +549,21 @@ it.layer(NodeServices.layer)("server settings", (it) => { }).pipe(Effect.provide(makeServerSettingsLayer())), ); - it.effect("preserves legacy starter preset entries while still seeding missing built-ins", () => + it.effect("updates legacy starter preset entries while still seeding missing built-ins", () => Effect.gen(function* () { const serverSettings = yield* ServerSettingsService; const next = yield* serverSettings.getSettings; assert.equal(next.modelSelectionPresets.codex["starter-codex-pro-100"]?.name, "pro 100"); assert.equal(next.modelSelectionPresets.codex["starter-codex-pro-200"]?.name, "pro 200"); - assert.equal(next.modelSelectionPresets.codex["starter-codex-free"]?.name, "free"); - assert.equal(next.modelSelectionPresets.codex["starter-codex-go"]?.name, "go"); - assert.equal(next.modelSelectionPresets.codex["starter-codex-plus"]?.name, "plus"); + assert.equal(next.modelSelectionPresets.codex["starter-codex-free"]?.name, "Free"); + assert.equal(next.modelSelectionPresets.codex["starter-codex-go"]?.name, "Go"); + assert.equal(next.modelSelectionPresets.codex["starter-codex-plus"]?.name, "Plus"); assert.deepEqual( next.modelSelectionPresets.codex["starter-codex-pro-5x"]?.askModelSelection, { provider: "codex", - model: "gpt-5.4", + model: "gpt-5.5", options: { reasoningEffort: "low", }, @@ -539,9 +573,9 @@ it.layer(NodeServices.layer)("server settings", (it) => { next.modelSelectionPresets.codex["starter-codex-pro-5x"]?.codeModelSelection, { provider: "codex", - model: "gpt-5.3-codex", + model: "gpt-5.5", options: { - reasoningEffort: "high", + reasoningEffort: "medium", }, }, ); @@ -549,9 +583,9 @@ it.layer(NodeServices.layer)("server settings", (it) => { next.modelSelectionPresets.codex["starter-codex-pro-20x"]?.askModelSelection, { provider: "codex", - model: "gpt-5.4", + model: "gpt-5.5", options: { - reasoningEffort: "medium", + reasoningEffort: "low", }, }, ); diff --git a/apps/server/src/serverSettings.ts b/apps/server/src/serverSettings.ts index 342ee8dc..e95bc0dc 100644 --- a/apps/server/src/serverSettings.ts +++ b/apps/server/src/serverSettings.ts @@ -116,6 +116,8 @@ const PRESET_SELECTION_SETTINGS_KEYS = [ "reviewModelSelection", ] as const; type PresetSelectionSettingsKey = (typeof PRESET_SELECTION_SETTINGS_KEYS)[number]; +const CODEX_EFFICIENT_MODEL = "gpt-5.4-mini"; +const CODEX_RECOMMENDED_MODEL = "gpt-5.5"; function makeCodexPreset( id: string, @@ -216,34 +218,34 @@ const BUILT_IN_MODEL_SELECTION_PRESETS: { } = { codex: [ makeCodexPreset("starter-codex-free", "Free", { - ask: { model: "gpt-5.4-mini", effort: "low" }, - plan: { model: "gpt-5.4-mini", effort: "medium" }, - code: { model: "gpt-5.4-mini", effort: "low" }, - review: { model: "gpt-5.4", effort: "low" }, + ask: { model: CODEX_EFFICIENT_MODEL, effort: "low" }, + plan: { model: CODEX_EFFICIENT_MODEL, effort: "medium" }, + code: { model: CODEX_EFFICIENT_MODEL, effort: "medium" }, + review: { model: CODEX_EFFICIENT_MODEL, effort: "high" }, }), makeCodexPreset("starter-codex-go", "Go", { - ask: { model: "gpt-5.4-mini", effort: "low" }, - plan: { model: "gpt-5.4-mini", effort: "medium" }, - code: { model: "gpt-5.4-mini", effort: "medium" }, - review: { model: "gpt-5.3-codex", effort: "medium" }, + ask: { model: CODEX_EFFICIENT_MODEL, effort: "low" }, + plan: { model: CODEX_EFFICIENT_MODEL, effort: "high" }, + code: { model: CODEX_EFFICIENT_MODEL, effort: "medium" }, + review: { model: CODEX_EFFICIENT_MODEL, effort: "high" }, }), makeCodexPreset("starter-codex-plus", "Plus", { - ask: { model: "gpt-5.4-mini", effort: "low" }, - plan: { model: "gpt-5.4", effort: "medium" }, - code: { model: "gpt-5.4-mini", effort: "medium" }, - review: { model: "gpt-5.3-codex", effort: "medium" }, + ask: { model: CODEX_EFFICIENT_MODEL, effort: "low" }, + plan: { model: CODEX_RECOMMENDED_MODEL, effort: "medium" }, + code: { model: CODEX_EFFICIENT_MODEL, effort: "medium" }, + review: { model: CODEX_RECOMMENDED_MODEL, effort: "low" }, }), makeCodexPreset("starter-codex-pro-5x", "Pro (5X)", { - ask: { model: "gpt-5.4", effort: "low" }, - plan: { model: "gpt-5.4", effort: "high" }, - code: { model: "gpt-5.3-codex", effort: "high" }, - review: { model: "gpt-5.3-codex", effort: "high" }, + ask: { model: CODEX_RECOMMENDED_MODEL, effort: "low" }, + plan: { model: CODEX_RECOMMENDED_MODEL, effort: "medium" }, + code: { model: CODEX_RECOMMENDED_MODEL, effort: "medium" }, + review: { model: CODEX_RECOMMENDED_MODEL, effort: "high" }, }), makeCodexPreset("starter-codex-pro-20x", "Pro (20X)", { - ask: { model: "gpt-5.4", effort: "medium" }, - plan: { model: "gpt-5.4", effort: "high" }, - code: { model: "gpt-5.3-codex", effort: "high" }, - review: { model: "gpt-5.3-codex", effort: "xhigh" }, + ask: { model: CODEX_RECOMMENDED_MODEL, effort: "low" }, + plan: { model: CODEX_RECOMMENDED_MODEL, effort: "medium" }, + code: { model: CODEX_RECOMMENDED_MODEL, effort: "medium" }, + review: { model: CODEX_RECOMMENDED_MODEL, effort: "high" }, }), ], claudeAgent: [ @@ -360,7 +362,11 @@ function seedBuiltInPresets(settings: ServerSettings): ServerSettings { for (const provider of PROVIDER_ORDER) { for (const preset of BUILT_IN_MODEL_SELECTION_PRESETS[provider]) { - if (nextPresets[provider][preset.id]) { + if (provider !== "codex" && nextPresets[provider][preset.id]) { + continue; + } + + if (Equal.equals(nextPresets[provider][preset.id], preset)) { continue; } diff --git a/apps/web/src/components/chat/ModelPickerContent.tsx b/apps/web/src/components/chat/ModelPickerContent.tsx index 991f1c95..0cf715d3 100644 --- a/apps/web/src/components/chat/ModelPickerContent.tsx +++ b/apps/web/src/components/chat/ModelPickerContent.tsx @@ -39,6 +39,7 @@ export const ModelPickerContent = memo(function ModelPickerContent(props: { providers?: ReadonlyArray; keybindings?: ResolvedKeybindingsConfig; modelOptionsByProvider: Record>; + allowAutoModel?: boolean; showAsAuto?: boolean; terminalOpen?: boolean; onRequestClose?: () => void; @@ -127,13 +128,18 @@ export const ModelPickerContent = memo(function ModelPickerContent(props: { return []; } - return [autoModelByProvider[providerKind as ProviderKind], ...models].map((model) => + const providerModels = + props.allowAutoModel === false + ? models + : [autoModelByProvider[providerKind as ProviderKind], ...models]; + + return providerModels.map((model) => Object.assign({}, model, { provider: providerKind as ProviderKind, }), ) satisfies Array; }); - }, [autoModelByProvider, props.modelOptionsByProvider, readyProviderSet]); + }, [autoModelByProvider, props.allowAutoModel, props.modelOptionsByProvider, readyProviderSet]); const filteredModels = useMemo(() => { let result = flatModels; @@ -242,6 +248,9 @@ export const ModelPickerContent = memo(function ModelPickerContent(props: { const handleModelSelect = useCallback( (modelSlug: string, provider: ProviderKind) => { if (modelSlug === COMPOSER_AUTO_MODEL_VALUE) { + if (props.allowAutoModel === false) { + return; + } props.onProviderModelChange(provider, COMPOSER_AUTO_MODEL_VALUE); return; } diff --git a/apps/web/src/components/chat/ProviderModelPicker.browser.tsx b/apps/web/src/components/chat/ProviderModelPicker.browser.tsx index 280e5322..0b0c531f 100644 --- a/apps/web/src/components/chat/ProviderModelPicker.browser.tsx +++ b/apps/web/src/components/chat/ProviderModelPicker.browser.tsx @@ -121,6 +121,7 @@ async function mountPicker(props: { providers?: ReadonlyArray; keybindings?: ResolvedKeybindingsConfig; triggerVariant?: "ghost" | "outline"; + allowAutoModel?: boolean; showAsAuto?: boolean; }) { const host = document.createElement("div"); @@ -141,6 +142,7 @@ async function mountPicker(props: { providers={providers} modelOptionsByProvider={modelOptionsByProvider} {...(props.keybindings ? { keybindings: props.keybindings } : {})} + {...(props.allowAutoModel !== undefined ? { allowAutoModel: props.allowAutoModel } : {})} {...(props.showAsAuto !== undefined ? { showAsAuto: props.showAsAuto } : {})} triggerVariant={props.triggerVariant} onProviderModelChange={onProviderModelChange} @@ -287,6 +289,27 @@ describe("ProviderModelPicker", () => { } }); + it("hides Auto when disabled for persisted settings pickers", async () => { + const mounted = await mountPicker({ + provider: "claudeAgent", + model: "claude-opus-4-6", + lockedProvider: "claudeAgent", + allowAutoModel: false, + }); + + try { + await page.getByRole("button").click(); + await vi.waitFor(() => { + expect(document.querySelector(".model-picker-list")).not.toBeNull(); + }); + + expect(getVisibleModelNames().some((name) => name.includes("Auto"))).toBe(false); + expect(getVisibleModelNames().some((name) => name.includes("Claude Opus 4.6"))).toBe(true); + } finally { + await mounted.cleanup(); + } + }); + it("shows jump shortcut labels when keybindings are provided", async () => { const mounted = await mountPicker({ provider: "codex", diff --git a/apps/web/src/components/chat/ProviderModelPicker.tsx b/apps/web/src/components/chat/ProviderModelPicker.tsx index 74db8970..22586d74 100644 --- a/apps/web/src/components/chat/ProviderModelPicker.tsx +++ b/apps/web/src/components/chat/ProviderModelPicker.tsx @@ -31,6 +31,7 @@ export const ProviderModelPicker = memo(function ProviderModelPicker(props: { keybindings?: ResolvedKeybindingsConfig; modelOptionsByProvider: Record>; activeProviderIconClassName?: string; + allowAutoModel?: boolean; compact?: boolean; disabled?: boolean; showAsAuto?: boolean; @@ -159,6 +160,7 @@ export const ProviderModelPicker = memo(function ProviderModelPicker(props: { {...(props.providers ? { providers: props.providers } : {})} {...(props.keybindings ? { keybindings: props.keybindings } : {})} modelOptionsByProvider={props.modelOptionsByProvider} + {...(props.allowAutoModel !== undefined ? { allowAutoModel: props.allowAutoModel } : {})} {...(props.showAsAuto !== undefined ? { showAsAuto: props.showAsAuto } : {})} terminalOpen={props.terminalOpen ?? false} onRequestClose={() => setIsMenuOpen(false)} diff --git a/apps/web/src/components/settings/SettingsModelsSection.test.tsx b/apps/web/src/components/settings/SettingsModelsSection.test.tsx index 231d6abb..1ae1946d 100644 --- a/apps/web/src/components/settings/SettingsModelsSection.test.tsx +++ b/apps/web/src/components/settings/SettingsModelsSection.test.tsx @@ -25,6 +25,12 @@ vi.mock("../../rpc/serverState", () => ({ auth: { status: "authenticated" }, checkedAt: "2026-01-01T00:00:00.000Z", models: [ + { + slug: "gpt-5.5", + name: "GPT-5.5", + isCustom: false, + capabilities: null, + }, { slug: "gpt-5.4", name: "GPT-5.4", diff --git a/apps/web/src/components/settings/SettingsModelsSection.tsx b/apps/web/src/components/settings/SettingsModelsSection.tsx index 5716dbce..f0349b90 100644 --- a/apps/web/src/components/settings/SettingsModelsSection.tsx +++ b/apps/web/src/components/settings/SettingsModelsSection.tsx @@ -109,12 +109,12 @@ const BUILT_IN_PRESET_MODE_SELECTIONS: Readonly< codeModelSelection: { provider: "codex", model: "gpt-5.4-mini", - options: { reasoningEffort: "low" }, + options: { reasoningEffort: "medium" }, }, reviewModelSelection: { provider: "codex", - model: "gpt-5.4", - options: { reasoningEffort: "low" }, + model: "gpt-5.4-mini", + options: { reasoningEffort: "high" }, }, }, "starter-codex-go": { @@ -126,7 +126,7 @@ const BUILT_IN_PRESET_MODE_SELECTIONS: Readonly< planModelSelection: { provider: "codex", model: "gpt-5.4-mini", - options: { reasoningEffort: "medium" }, + options: { reasoningEffort: "high" }, }, codeModelSelection: { provider: "codex", @@ -135,8 +135,8 @@ const BUILT_IN_PRESET_MODE_SELECTIONS: Readonly< }, reviewModelSelection: { provider: "codex", - model: "gpt-5.3-codex", - options: { reasoningEffort: "medium" }, + model: "gpt-5.4-mini", + options: { reasoningEffort: "high" }, }, }, "starter-codex-plus": { @@ -147,7 +147,7 @@ const BUILT_IN_PRESET_MODE_SELECTIONS: Readonly< }, planModelSelection: { provider: "codex", - model: "gpt-5.4", + model: "gpt-5.5", options: { reasoningEffort: "medium" }, }, codeModelSelection: { @@ -157,52 +157,52 @@ const BUILT_IN_PRESET_MODE_SELECTIONS: Readonly< }, reviewModelSelection: { provider: "codex", - model: "gpt-5.3-codex", - options: { reasoningEffort: "medium" }, + model: "gpt-5.5", + options: { reasoningEffort: "low" }, }, }, "starter-codex-pro-5x": { askModelSelection: { provider: "codex", - model: "gpt-5.4", + model: "gpt-5.5", options: { reasoningEffort: "low" }, }, planModelSelection: { provider: "codex", - model: "gpt-5.4", - options: { reasoningEffort: "high" }, + model: "gpt-5.5", + options: { reasoningEffort: "medium" }, }, codeModelSelection: { provider: "codex", - model: "gpt-5.3-codex", - options: { reasoningEffort: "high" }, + model: "gpt-5.5", + options: { reasoningEffort: "medium" }, }, reviewModelSelection: { provider: "codex", - model: "gpt-5.3-codex", + model: "gpt-5.5", options: { reasoningEffort: "high" }, }, }, "starter-codex-pro-20x": { askModelSelection: { provider: "codex", - model: "gpt-5.4", - options: { reasoningEffort: "medium" }, + model: "gpt-5.5", + options: { reasoningEffort: "low" }, }, planModelSelection: { provider: "codex", - model: "gpt-5.4", - options: { reasoningEffort: "high" }, + model: "gpt-5.5", + options: { reasoningEffort: "medium" }, }, codeModelSelection: { provider: "codex", - model: "gpt-5.3-codex", - options: { reasoningEffort: "high" }, + model: "gpt-5.5", + options: { reasoningEffort: "medium" }, }, reviewModelSelection: { provider: "codex", - model: "gpt-5.3-codex", - options: { reasoningEffort: "xhigh" }, + model: "gpt-5.5", + options: { reasoningEffort: "high" }, }, }, }, diff --git a/apps/web/src/components/settings/SettingsPanelPrimitives.tsx b/apps/web/src/components/settings/SettingsPanelPrimitives.tsx index 8f448695..51132a35 100644 --- a/apps/web/src/components/settings/SettingsPanelPrimitives.tsx +++ b/apps/web/src/components/settings/SettingsPanelPrimitives.tsx @@ -111,6 +111,7 @@ export function ModelSelectionControl({ lockedProvider={lockedProvider ?? null} providers={providers} modelOptionsByProvider={modelOptionsByProvider} + allowAutoModel={false} triggerVariant="outline" triggerClassName="min-w-0 max-w-none shrink-0 text-foreground/90 hover:text-foreground" onProviderModelChange={onProviderModelChange} diff --git a/packages/contracts/src/model.ts b/packages/contracts/src/model.ts index e62a957e..43a9377e 100644 --- a/packages/contracts/src/model.ts +++ b/packages/contracts/src/model.ts @@ -52,7 +52,7 @@ export const ModelCapabilities = Schema.Struct({ export type ModelCapabilities = typeof ModelCapabilities.Type; export const DEFAULT_MODEL_BY_PROVIDER: Record = { - codex: "gpt-5.4", + codex: "gpt-5.5", claudeAgent: "claude-sonnet-4-6", }; @@ -66,6 +66,7 @@ export const DEFAULT_GIT_TEXT_GENERATION_MODEL_BY_PROVIDER: Record> = { codex: { + "5.5": "gpt-5.5", "5.4": "gpt-5.4", "5.3": "gpt-5.3-codex", "gpt-5.3": "gpt-5.3-codex", diff --git a/packages/shared/src/model.test.ts b/packages/shared/src/model.test.ts index acf1cc3f..170e4d51 100644 --- a/packages/shared/src/model.test.ts +++ b/packages/shared/src/model.test.ts @@ -92,6 +92,7 @@ const providersWithoutSpark: ReadonlyArray = [ describe("normalizeModelSlug", () => { it("maps known aliases to canonical slugs", () => { + expect(normalizeModelSlug("5.5")).toBe("gpt-5.5"); expect(normalizeModelSlug("5.3")).toBe("gpt-5.3-codex"); expect(normalizeModelSlug("sonnet", "claudeAgent")).toBe("claude-sonnet-4-6"); }); @@ -345,7 +346,7 @@ describe("resolveApiModelId", () => { }); it("returns the model as-is for Codex selections", () => { - expect(resolveApiModelId({ provider: "codex", model: "gpt-5.4" })).toBe("gpt-5.4"); + expect(resolveApiModelId({ provider: "codex", model: "gpt-5.5" })).toBe("gpt-5.5"); }); it("resolves codex auto for API model ids", () => { diff --git a/scripts/build-desktop-artifact.ts b/scripts/build-desktop-artifact.ts index ddac9250..8c29f8dd 100644 --- a/scripts/build-desktop-artifact.ts +++ b/scripts/build-desktop-artifact.ts @@ -469,7 +469,7 @@ const createBuildConfig = Effect.fn("createBuildConfig")(function* ( mockUpdateServerPort: string | undefined, ) { const buildConfig: Record = { - appId: "com.kodo.code", + appId: "app.kodocode", productName, artifactName: "Kodo-Code-${version}-${arch}.${ext}", asarUnpack: ["apps/server/dist/**", "node_modules/**"], From 77ba0b6559ee0e5bdee3d4c9730ec6acb349a459 Mon Sep 17 00:00:00 2001 From: boggedbrush <90526147+boggedbrush@users.noreply.github.com> Date: Wed, 29 Apr 2026 12:30:27 -0400 Subject: [PATCH 2/3] fix(codex): use utility default for text generation auto --- .../src/git/Layers/CodexTextGeneration.test.ts | 2 +- .../src/git/Layers/CodexTextGeneration.ts | 17 +++++++++++++---- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/apps/server/src/git/Layers/CodexTextGeneration.test.ts b/apps/server/src/git/Layers/CodexTextGeneration.test.ts index 2c1f05e4..11fc1211 100644 --- a/apps/server/src/git/Layers/CodexTextGeneration.test.ts +++ b/apps/server/src/git/Layers/CodexTextGeneration.test.ts @@ -284,7 +284,7 @@ it.layer(CodexTextGenerationTestLayer)("CodexTextGenerationLive", (it) => { subject: "Add important change", body: "", }), - requireModel: "gpt-5.4", + requireModel: "gpt-5.4-mini", }, Effect.gen(function* () { const textGeneration = yield* TextGeneration; diff --git a/apps/server/src/git/Layers/CodexTextGeneration.ts b/apps/server/src/git/Layers/CodexTextGeneration.ts index 64099ade..0d6ad25b 100644 --- a/apps/server/src/git/Layers/CodexTextGeneration.ts +++ b/apps/server/src/git/Layers/CodexTextGeneration.ts @@ -3,7 +3,10 @@ import { randomUUID } from "node:crypto"; import { Effect, FileSystem, Layer, Option, Path, Schema, Scope, Stream } from "effect"; import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"; -import { CodexModelSelection } from "@t3tools/contracts"; +import { + CodexModelSelection, + DEFAULT_GIT_TEXT_GENERATION_MODEL_BY_PROVIDER, +} from "@t3tools/contracts"; import { sanitizeBranchFragment, sanitizeFeatureBranchName } from "@t3tools/shared/git"; import { resolveAttachmentPath } from "../../attachmentStore.ts"; @@ -163,9 +166,15 @@ const makeCodexTextGeneration = Effect.gen(function* () { const runCodexCommand = Effect.fn("runCodexJson.runCodexCommand")(function* () { // Resolve frontend sentinel models like `auto` before invoking Codex CLI. - const resolvedModelSelection = resolveModelSelectionDefault( - modelSelection, - ) as CodexModelSelection; + // Utility text generation should use the cheaper utility default, not the + // interactive composer default. + const resolvedModelSelection = + modelSelection.model.trim().toLowerCase() === "auto" + ? ({ + ...modelSelection, + model: DEFAULT_GIT_TEXT_GENERATION_MODEL_BY_PROVIDER.codex, + } satisfies CodexModelSelection) + : (resolveModelSelectionDefault(modelSelection) as CodexModelSelection); const normalizedOptions = normalizeCodexModelOptionsWithCapabilities( getCodexModelCapabilities(resolvedModelSelection.model), resolvedModelSelection.options, From 4fa0a74447671228c87fa051c034a0d5078c810b Mon Sep 17 00:00:00 2001 From: boggedbrush <90526147+boggedbrush@users.noreply.github.com> Date: Wed, 29 Apr 2026 12:40:41 -0400 Subject: [PATCH 3/3] fix(codex): update traits browser fixture for gpt-5.5 --- .../src/components/chat/TraitsPicker.browser.tsx | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/apps/web/src/components/chat/TraitsPicker.browser.tsx b/apps/web/src/components/chat/TraitsPicker.browser.tsx index 67bda031..ac5970e6 100644 --- a/apps/web/src/components/chat/TraitsPicker.browser.tsx +++ b/apps/web/src/components/chat/TraitsPicker.browser.tsx @@ -38,6 +38,21 @@ const TEST_PROVIDERS: ReadonlyArray = [ auth: { status: "authenticated" }, checkedAt: "2026-01-01T00:00:00.000Z", models: [ + { + slug: "gpt-5.5", + name: "GPT-5.5", + isCustom: false, + capabilities: { + reasoningEffortLevels: [ + { value: "xhigh", label: "Extra High" }, + { value: "high", label: "High", isDefault: true }, + ], + supportsFastMode: true, + supportsThinkingToggle: false, + contextWindowOptions: [], + promptInjectedEffortLevels: [], + }, + }, { slug: "gpt-5.4", name: "GPT-5.4",