diff --git a/apps/ade-cli/src/tuiClient/__tests__/appInput.test.ts b/apps/ade-cli/src/tuiClient/__tests__/appInput.test.ts index de0185f4e..c6a25c5b5 100644 --- a/apps/ade-cli/src/tuiClient/__tests__/appInput.test.ts +++ b/apps/ade-cli/src/tuiClient/__tests__/appInput.test.ts @@ -1203,7 +1203,7 @@ describe("interface draft setup", () => { ]); }); - it("shows the product-facing Sol effort ladder without internal max", () => { + it("shows the complete GPT-5.6 effort ladders from runtime and registry metadata", () => { const modelState = initialModelState("chat"); const models = [{ id: "gpt-5.6-sol", @@ -1214,7 +1214,19 @@ describe("interface draft setup", () => { .map((effort) => ({ effort, description: effort })), }]; - expect(modelReasoningEfforts(modelState, models)).toEqual(["low", "medium", "high", "xhigh", "ultra"]); + expect(modelReasoningEfforts(modelState, models)).toEqual(["low", "medium", "high", "xhigh", "max", "ultra"]); + expect(modelReasoningEfforts({ + ...modelState, + model: "gpt-5.6-terra", + modelId: "openai/gpt-5.6-terra", + displayName: "GPT-5.6 Terra", + }, [])).toEqual(["low", "medium", "high", "xhigh", "max", "ultra"]); + expect(modelReasoningEfforts({ + ...modelState, + model: "gpt-5.6-luna", + modelId: "openai/gpt-5.6-luna", + displayName: "GPT-5.6 Luna", + }, [])).toEqual(["low", "medium", "high", "xhigh", "max"]); expect(buildSetupRows({ modelState, models, @@ -1224,7 +1236,7 @@ describe("interface draft setup", () => { interfaceEditable: true, }).find((row) => row.kind === "reasoning")).toMatchObject({ value: "Light", - detail: "Light, Medium, High, Extra High, Ultra", + detail: "Light, Medium, High, Extra High, Max, Ultra", }); }); diff --git a/apps/ade-cli/src/tuiClient/modelState.ts b/apps/ade-cli/src/tuiClient/modelState.ts index 7a44ad796..1413f69eb 100644 --- a/apps/ade-cli/src/tuiClient/modelState.ts +++ b/apps/ade-cli/src/tuiClient/modelState.ts @@ -20,7 +20,7 @@ import { theme } from "./theme"; import type { AdeCodeInterfaceMode, AdeCodeModelState, AdeCodeProvider, SetupPaneRow, SetupPaneRowKind } from "./types"; import { normalizeProvider, providerLabel } from "./providerMetadata"; -export const EFFORTS = ["low", "medium", "high", "xhigh", "ultra"]; +export const EFFORTS = ["low", "medium", "high", "xhigh", "max", "ultra"]; export const CODEX_PRESETS = ["default", "edit", "plan", "full-auto", "config-toml"] as const; export const CLAUDE_PERMISSION_OPTIONS = ["default", "auto", "plan", "acceptEdits", "bypassPermissions"] as const; export const OPENCODE_PERMISSION_OPTIONS = ["plan", "edit", "full-auto", "config-toml"] as const; @@ -69,10 +69,9 @@ export function cliProviderForModelStateProvider(provider: AdeCodeProvider): Cli function firstReasoningEffortForModel(model: AgentChatModelInfo | null | undefined, provider: AdeCodeProvider): string | null { const modelId = `${model?.modelId ?? ""} ${model?.id ?? ""} ${model?.displayName ?? ""}`.toLowerCase(); - const isGpt56CodexModel = provider === "codex" && /gpt-5\.6-(?:sol|terra|luna)/.test(modelId); const efforts = model?.reasoningEfforts ?.map((entry) => entry.effort) - .filter((effort) => Boolean(effort) && (!isGpt56CodexModel || effort !== "max")) ?? []; + .filter(Boolean) ?? []; const advertisedDefault = model?.defaultReasoningEffort?.trim().toLowerCase() ?? null; if (modelId.includes("fable") && efforts.includes("high")) return "high"; if (advertisedDefault && efforts.includes(advertisedDefault)) return advertisedDefault; @@ -285,13 +284,8 @@ export function modelReasoningEfforts(modelState: AdeCodeModelState, models: Age const model = models.find((entry) => entry.id === modelState.modelId || entry.modelId === modelState.modelId); const fromModel = model?.reasoningEfforts?.map((entry) => entry.effort).filter(Boolean) ?? []; const descriptor = modelState.modelId ? getModelById(modelState.modelId) : undefined; - const isGpt56CodexModel = modelState.provider === "codex" - && /gpt-5\.6-(?:sol|terra|luna)/i.test(`${descriptor?.providerModelId ?? ""} ${modelState.model}`); - const visibleEfforts = (efforts: string[]) => isGpt56CodexModel - ? efforts.filter((effort) => effort !== "max") - : efforts; - if (fromModel.length) return visibleEfforts(fromModel); - if (descriptor?.reasoningTiers?.length) return visibleEfforts(descriptor.reasoningTiers); + if (fromModel.length) return fromModel; + if (descriptor?.reasoningTiers?.length) return descriptor.reasoningTiers; return modelState.provider === "codex" ? EFFORTS : []; } @@ -307,6 +301,7 @@ export function reasoningEffortDisplayLabel( if (effort === "medium") return "Medium"; if (effort === "high") return "High"; if (effort === "xhigh") return "Extra High"; + if (effort === "max") return "Max"; if (effort === "ultra") return "Ultra"; return effort; } diff --git a/apps/desktop/src/main/services/chat/agentChatService.test.ts b/apps/desktop/src/main/services/chat/agentChatService.test.ts index 446a8fc41..0e3157c01 100644 --- a/apps/desktop/src/main/services/chat/agentChatService.test.ts +++ b/apps/desktop/src/main/services/chat/agentChatService.test.ts @@ -15565,16 +15565,15 @@ describe("createAgentChatService", () => { expect.objectContaining({ effort: "medium" }), expect.objectContaining({ effort: "high" }), expect.objectContaining({ effort: "xhigh" }), + expect.objectContaining({ effort: "max" }), expect.objectContaining({ effort: "ultra" }), ], serviceTiers: ["fast"], }); expect(models[1]).toMatchObject({ isDefault: false, defaultReasoningEffort: "medium" }); expect(models[2]?.reasoningEfforts?.map((entry) => entry.effort)).toEqual([ - "low", "medium", "high", "xhigh", + "low", "medium", "high", "xhigh", "max", ]); - expect(models.slice(0, 3).flatMap((model) => model.reasoningEfforts ?? [])) - .not.toContainEqual(expect.objectContaining({ effort: "max" })); expect(models[3]?.isDefault).toBe(false); const aggregate = await service.getAvailableModels({}); diff --git a/apps/desktop/src/main/services/chat/agentChatService.ts b/apps/desktop/src/main/services/chat/agentChatService.ts index e4da118d6..2a3d29f4c 100644 --- a/apps/desktop/src/main/services/chat/agentChatService.ts +++ b/apps/desktop/src/main/services/chat/agentChatService.ts @@ -2143,21 +2143,6 @@ const CLAUDE_REASONING_EFFORTS: Array<{ effort: string; description: string }> = const KNOWN_CLAUDE_EFFORTS = new Set(CLAUDE_REASONING_EFFORTS.map((e) => e.effort)); -function isGpt56CodexDescriptor(descriptor: ModelDescriptor): boolean { - return /^gpt-5\.6-(?:sol|terra|luna)$/i.test(descriptor.providerModelId); -} - -function visibleCodexReasoningEfforts( - descriptor: ModelDescriptor, - efforts: Array<{ effort: string; description: string }>, -): Array<{ effort: string; description: string }> { - if (!isGpt56CodexDescriptor(descriptor)) return efforts; - // Codex Desktop keeps `max` behind an opt-in model-feature flag. ADE does - // not expose that flag, so omit only that internal/optional tier while still - // retaining unknown app-server values for forward compatibility. - return efforts.filter((entry) => entry.effort !== "max"); -} - function codexModelInfoFromDescriptor( descriptor: ModelDescriptor, overrides?: Partial>, @@ -2170,7 +2155,7 @@ function codexModelInfoFromDescriptor( displayName: descriptor.displayName, description: overrides?.description ?? describeCodexModel(descriptor.displayName), isDefault: overrides?.isDefault ?? descriptor.id === DEFAULT_CODEX_DESCRIPTOR?.id, - reasoningEfforts: visibleCodexReasoningEfforts(descriptor, advertisedReasoningEfforts), + reasoningEfforts: advertisedReasoningEfforts, defaultReasoningEffort: overrides?.defaultReasoningEffort ?? descriptor.defaultReasoningEffort ?? null, diff --git a/apps/desktop/src/renderer/components/shared/ModelPicker/ReasoningEffortPicker.test.tsx b/apps/desktop/src/renderer/components/shared/ModelPicker/ReasoningEffortPicker.test.tsx index 6306dcf62..3f22d0f8f 100644 --- a/apps/desktop/src/renderer/components/shared/ModelPicker/ReasoningEffortPicker.test.tsx +++ b/apps/desktop/src/renderer/components/shared/ModelPicker/ReasoningEffortPicker.test.tsx @@ -206,8 +206,8 @@ describe("ReasoningEffortPicker", () => { expect(trigger.textContent).toContain("ULTRA"); await user.click(trigger); - expect(screen.getAllByRole("radio")).toHaveLength(5); - expect(screen.queryByRole("radio", { name: "Max" })).toBeNull(); + expect(screen.getAllByRole("radio")).toHaveLength(6); + expect(screen.getByRole("radio", { name: "Max" })).toBeTruthy(); expect(screen.getByRole("radio", { name: "Light" })).toBeTruthy(); expect(screen.getByRole("radio", { name: "Ultra" })).toBeTruthy(); expect(screen.getByText(/automatically delegates work to multiple agents/i)).toBeTruthy(); @@ -317,7 +317,7 @@ describe("ReasoningEffortPicker", () => { firePointer("pointerup", 236); expect(track!.hasAttribute("data-dragging")).toBe(false); - expect(track!.style.getPropertyValue("--reasoning-slider-thumb-position")).toContain("75%"); + expect(track!.style.getPropertyValue("--reasoning-slider-thumb-position")).toContain("60%"); expect(releasePointerCapture).toHaveBeenCalledWith(7); expect(onChange).toHaveBeenCalledTimes(1); expect(onChange).toHaveBeenCalledWith("xhigh"); @@ -375,7 +375,7 @@ describe("ReasoningEffortPicker", () => { expect(onChange).toHaveBeenCalledTimes(1); expect(onChange).toHaveBeenCalledWith("xhigh"); - expect(track!.style.getPropertyValue("--reasoning-slider-thumb-position")).toContain("75%"); + expect(track!.style.getPropertyValue("--reasoning-slider-thumb-position")).toContain("60%"); expect(trigger.getAttribute("aria-expanded")).toBe("true"); }); diff --git a/apps/desktop/src/renderer/components/shared/ModelPicker/modelCatalog.test.ts b/apps/desktop/src/renderer/components/shared/ModelPicker/modelCatalog.test.ts index e11de8689..1b9544c72 100644 --- a/apps/desktop/src/renderer/components/shared/ModelPicker/modelCatalog.test.ts +++ b/apps/desktop/src/renderer/components/shared/ModelPicker/modelCatalog.test.ts @@ -5,6 +5,13 @@ import { resetRuntimeCatalogDescriptorCacheForTests, resolveModelDescriptorWithRuntimeCatalog, } from "./modelCatalog"; +import { sortModelItems } from "./modelOrdering"; +import { buildModelPickerSearchText, scoreModelPickerSearch } from "./modelPickerSearch"; +import { + rememberRuntimeCatalog, + resetModelPickerRuntimeCatalogForTests, + runtimeCatalogProviderIsFresh, +} from "./runtimeCatalogCache"; import type { AgentChatModelCatalog } from "../../../../shared/types"; describe("mergeSelectorModels", () => { @@ -119,6 +126,54 @@ describe("mergeSelectorModels", () => { expect(resolveModelDescriptorWithRuntimeCatalog("cursor/composer-2")?.reasoningTiers).toEqual(["high"]); }); + it("preserves the complete GPT-5.6 app-server effort ladders", () => { + const model = ( + id: "gpt-5.6-sol" | "gpt-5.6-terra" | "gpt-5.6-luna", + efforts: string[], + ) => ({ + id, + runtimeModelId: id, + provider: "codex" as const, + providerKey: "openai", + groupKey: "codex", + displayName: id, + isDefault: id === "gpt-5.6-sol", + isAvailable: true, + reasoningEfforts: efforts.map((effort) => ({ effort, description: effort })), + supportsReasoning: true, + supportsTools: true, + }); + const catalog: AgentChatModelCatalog = { + fetchedAt: new Date().toISOString(), + groups: [{ + key: "codex", + displayName: "Codex", + providers: [{ + key: "openai", + displayName: "OpenAI", + badgeColor: "#10A37F", + modelCount: 3, + subsections: [{ + key: "models", + label: "Models", + models: [ + model("gpt-5.6-sol", ["low", "medium", "high", "xhigh", "max", "ultra"]), + model("gpt-5.6-terra", ["low", "medium", "high", "xhigh", "max", "ultra"]), + model("gpt-5.6-luna", ["low", "medium", "high", "xhigh", "max"]), + ], + }], + }], + }], + }; + + const result = descriptorsFromAgentChatModelCatalog(catalog); + expect(result.models.map(({ id, reasoningTiers }) => ({ id, reasoningTiers }))).toEqual([ + { id: "gpt-5.6-sol", reasoningTiers: ["low", "medium", "high", "xhigh", "max", "ultra"] }, + { id: "gpt-5.6-terra", reasoningTiers: ["low", "medium", "high", "xhigh", "max", "ultra"] }, + { id: "gpt-5.6-luna", reasoningTiers: ["low", "medium", "high", "xhigh", "max"] }, + ]); + }); + it("uses Cursor catalog subsections as picker sub-provider groups", () => { const catalog: AgentChatModelCatalog = { fetchedAt: new Date().toISOString(), @@ -167,3 +222,176 @@ describe("mergeSelectorModels", () => { }); }); }); + +describe("model picker ordering", () => { + it("preserves source order unless favorites or explicit ordering apply", () => { + const items = [ + { modelId: "a" }, + { modelId: "b" }, + { modelId: "c" }, + { modelId: "d" }, + ]; + + expect(sortModelItems(items).map((item) => item.modelId)).toEqual(["a", "b", "c", "d"]); + expect(sortModelItems(items, { modelIdOrder: ["c", "a"] }).map((item) => item.modelId)) + .toEqual(["c", "a", "b", "d"]); + expect(sortModelItems(items, { + favoriteModelIds: new Set(["c"]), + groupFavorites: true, + modelIdOrder: ["b", "d"], + }).map((item) => item.modelId)).toEqual(["c", "b", "d", "a"]); + }); + + it("moves favorites only when grouping is enabled", () => { + const items = [{ modelId: "x" }, { modelId: "y" }]; + + expect(sortModelItems(items, { + favoriteModelIds: ["y"], + groupFavorites: false, + }).map((item) => item.modelId)).toEqual(["x", "y"]); + expect(sortModelItems(items, { + favoriteModelIds: new Set(["y"]), + groupFavorites: true, + }).map((item) => item.modelId)).toEqual(["y", "x"]); + }); +}); + +describe("model picker search", () => { + const opus = { + family: "opencode" as const, + providerDisplayName: "opencode", + name: "Claude Opus 4.8 1M", + subProvider: "GitHub Copilot", + aliases: ["opus-latest"], + }; + + it("builds provider-agnostic searchable text", () => { + expect(buildModelPickerSearchText(opus)) + .toBe("claude opus 4.8 1m github copilot opencode opencode opus-latest"); + }); + + it("requires every query token while tolerating typos", () => { + expect(scoreModelPickerSearch(opus, "coplt op")).not.toBeNull(); + expect(scoreModelPickerSearch({ + family: "openai", + providerDisplayName: "Codex", + name: "GPT-5 Codex", + }, "coplt op")).toBeNull(); + expect(scoreModelPickerSearch(opus, "")).toBe(0); + }); + + it("ranks exact text above fuzzy text and favorite boosts", () => { + const exactScore = scoreModelPickerSearch(opus, "copilot opus"); + const fuzzyScore = scoreModelPickerSearch(opus, "coplt op"); + const favoriteScore = scoreModelPickerSearch({ + family: "anthropic", + providerDisplayName: "Claude", + name: "Claude Opus 4.8 1M", + isFavorite: true, + }, "opus 4.8"); + const nonFavoriteExactScore = scoreModelPickerSearch({ + family: "cursor", + providerDisplayName: "Cursor", + name: "Opus 4.8 1M", + }, "opus 4.8"); + + expect(exactScore).not.toBeNull(); + expect(fuzzyScore).not.toBeNull(); + expect(exactScore!).toBeLessThan(fuzzyScore!); + expect(favoriteScore).not.toBeNull(); + expect(nonFavoriteExactScore).not.toBeNull(); + expect(nonFavoriteExactScore!).toBeLessThan(favoriteScore!); + }); + + it("matches provider names and discovered aliases", () => { + expect(scoreModelPickerSearch({ + family: "openai", + providerDisplayName: "Codex Personal", + name: "GPT-5 Codex", + }, "personal")).not.toBeNull(); + expect(scoreModelPickerSearch({ + family: "cursor", + providerDisplayName: "Cursor", + name: "Composer 2", + aliases: ["composer-latest"], + }, "composer-latest")).not.toBeNull(); + }); +}); + +function cursorCatalog(availability: { sdk: boolean; cli: boolean }): AgentChatModelCatalog { + return { + fetchedAt: new Date().toISOString(), + groups: [{ + key: "cursor", + displayName: "Cursor", + providers: [{ + key: "cursor", + displayName: "Cursor", + badgeColor: "#60A5FA", + modelCount: 1, + subsections: [{ + key: "cursor", + label: "Cursor", + models: [{ + id: "cursor/composer-2", + runtimeModelId: "cursor/composer-2", + provider: "cursor", + providerKey: "cursor", + groupKey: "cursor", + displayName: "Composer 2", + isDefault: true, + isAvailable: true, + supportsReasoning: true, + supportsTools: true, + cursorAvailability: availability, + }], + }], + }], + }], + }; +} + +describe("runtime catalog cache flavor-aware cursor freshness", () => { + beforeEach(() => { + resetModelPickerRuntimeCatalogForTests(); + }); + + it("keeps CLI stale after an SDK-only refresh", () => { + rememberRuntimeCatalog(cursorCatalog({ sdk: true, cli: false }), { + mode: "force", + refreshProvider: "cursor", + }); + + expect(runtimeCatalogProviderIsFresh("cursor", "sdk")).toBe(true); + expect(runtimeCatalogProviderIsFresh("cursor", "cli")).toBe(false); + expect(runtimeCatalogProviderIsFresh("cursor")).toBe(false); + }); + + it("tracks the probed source even when rows support both flavors", () => { + rememberRuntimeCatalog(cursorCatalog({ sdk: true, cli: true }), { + mode: "force", + refreshProvider: "cursor", + cursorSource: "sdk", + }); + + expect(runtimeCatalogProviderIsFresh("cursor", "sdk")).toBe(true); + expect(runtimeCatalogProviderIsFresh("cursor", "cli")).toBe(false); + }); + + it("marks both surfaces fresh after a full dual-capable refresh", () => { + rememberRuntimeCatalog(cursorCatalog({ sdk: true, cli: true }), { + mode: "force", + refreshProvider: "cursor", + }); + + expect(runtimeCatalogProviderIsFresh("cursor", "sdk")).toBe(true); + expect(runtimeCatalogProviderIsFresh("cursor", "cli")).toBe(true); + expect(runtimeCatalogProviderIsFresh("cursor")).toBe(true); + }); + + it("starts stale for every cursor flavor", () => { + expect(runtimeCatalogProviderIsFresh("cursor", "sdk")).toBe(false); + expect(runtimeCatalogProviderIsFresh("cursor", "cli")).toBe(false); + expect(runtimeCatalogProviderIsFresh("cursor")).toBe(false); + }); +}); diff --git a/apps/desktop/src/renderer/components/shared/ModelPicker/modelCatalog.ts b/apps/desktop/src/renderer/components/shared/ModelPicker/modelCatalog.ts index f0f952f1f..762a4325b 100644 --- a/apps/desktop/src/renderer/components/shared/ModelPicker/modelCatalog.ts +++ b/apps/desktop/src/renderer/components/shared/ModelPicker/modelCatalog.ts @@ -202,10 +202,9 @@ export function descriptorsFromAgentChatModelCatalog( for (const model of subsection.models ?? []) { const base = resolveModelDescriptor(model.id) ?? createUnknownModelPlaceholder(model.id); const family = pickerFamilyForCatalogGroup(String(model.groupKey || group.key), model.family); - const isGpt56CodexModel = /^gpt-5\.6-(?:sol|terra|luna)$/i.test(base.providerModelId); const runtimeReasoningTiers = model.reasoningEfforts ?.map((entry) => entry.effort.trim().toLowerCase()) - .filter((effort) => Boolean(effort) && (!isGpt56CodexModel || effort !== "max")); + .filter(Boolean); const serviceTiers = model.serviceTiers ?.map((entry) => entry.trim().toLowerCase()) .filter(Boolean); diff --git a/apps/desktop/src/renderer/components/shared/ModelPicker/modelOrdering.test.ts b/apps/desktop/src/renderer/components/shared/ModelPicker/modelOrdering.test.ts deleted file mode 100644 index e4e583dec..000000000 --- a/apps/desktop/src/renderer/components/shared/ModelPicker/modelOrdering.test.ts +++ /dev/null @@ -1,85 +0,0 @@ -import { describe, expect, it } from "vitest"; - -import { sortModelItems } from "./modelOrdering"; - -describe("sortModelItems", () => { - it("preserves the original order when no options are provided", () => { - const items = [ - { modelId: "anthropic/claude-opus-4-8", label: "opus" }, - { modelId: "openai/gpt-5", label: "gpt" }, - { modelId: "anthropic/claude-sonnet-5", label: "sonnet" }, - ]; - expect(sortModelItems(items).map((i) => i.modelId)).toEqual([ - "anthropic/claude-opus-4-8", - "openai/gpt-5", - "anthropic/claude-sonnet-5", - ]); - }); - - it("groups favorites first when groupFavorites is enabled", () => { - const items = [ - { modelId: "openai/gpt-5" }, - { modelId: "anthropic/claude-opus-4-8" }, - { modelId: "anthropic/claude-sonnet-5" }, - ]; - const sorted = sortModelItems(items, { - favoriteModelIds: ["anthropic/claude-opus-4-8"], - groupFavorites: true, - }); - expect(sorted.map((i) => i.modelId)).toEqual([ - "anthropic/claude-opus-4-8", - "openai/gpt-5", - "anthropic/claude-sonnet-5", - ]); - }); - - it("does not move favorites when groupFavorites is false", () => { - const items = [ - { modelId: "openai/gpt-5" }, - { modelId: "anthropic/claude-opus-4-8" }, - ]; - const sorted = sortModelItems(items, { - favoriteModelIds: ["anthropic/claude-opus-4-8"], - groupFavorites: false, - }); - expect(sorted.map((i) => i.modelId)).toEqual([ - "openai/gpt-5", - "anthropic/claude-opus-4-8", - ]); - }); - - it("honors an explicit modelIdOrder ahead of original order", () => { - const items = [ - { modelId: "a" }, - { modelId: "b" }, - { modelId: "c" }, - { modelId: "d" }, - ]; - const sorted = sortModelItems(items, { modelIdOrder: ["c", "a"] }); - expect(sorted.map((i) => i.modelId)).toEqual(["c", "a", "b", "d"]); - }); - - it("combines favorites grouping with id ordering", () => { - const items = [ - { modelId: "a" }, - { modelId: "b" }, - { modelId: "c" }, - { modelId: "d" }, - ]; - const sorted = sortModelItems(items, { - favoriteModelIds: new Set(["c"]), - groupFavorites: true, - modelIdOrder: ["b", "d"], - }); - expect(sorted.map((i) => i.modelId)).toEqual(["c", "b", "d", "a"]); - }); - - it("accepts a Set for favoriteModelIds", () => { - const items = [{ modelId: "x" }, { modelId: "y" }]; - const sorted = sortModelItems(items, { - favoriteModelIds: new Set(["y"]), - groupFavorites: true, - }); - expect(sorted.map((i) => i.modelId)).toEqual(["y", "x"]); - }); -}); diff --git a/apps/desktop/src/renderer/components/shared/ModelPicker/modelPickerSearch.test.ts b/apps/desktop/src/renderer/components/shared/ModelPicker/modelPickerSearch.test.ts deleted file mode 100644 index 576588ac2..000000000 --- a/apps/desktop/src/renderer/components/shared/ModelPicker/modelPickerSearch.test.ts +++ /dev/null @@ -1,159 +0,0 @@ -import { describe, expect, it } from "vitest"; - -import { buildModelPickerSearchText, scoreModelPickerSearch } from "./modelPickerSearch"; - -describe("buildModelPickerSearchText", () => { - it("builds provider-agnostic search text from generic fields", () => { - expect( - buildModelPickerSearchText({ - family: "opencode", - providerDisplayName: "opencode", - name: "Claude Opus 4.8 1M", - subProvider: "GitHub Copilot", - aliases: ["opus-latest"], - }), - ).toBe("claude opus 4.8 1m github copilot opencode opencode opus-latest"); - }); -}); - -describe("scoreModelPickerSearch", () => { - it("matches typo-tolerant multi-token queries", () => { - expect( - scoreModelPickerSearch( - { - family: "opencode", - providerDisplayName: "opencode", - name: "Claude Opus 4.8 1M", - subProvider: "GitHub Copilot", - }, - "coplt op", - ), - ).not.toBeNull(); - }); - - it("rejects results when any query token does not match", () => { - expect( - scoreModelPickerSearch( - { - family: "openai", - providerDisplayName: "Codex", - name: "GPT-5 Codex", - }, - "coplt op", - ), - ).toBeNull(); - }); - - it("ranks exact token matches ahead of fuzzier matches", () => { - const exactScore = scoreModelPickerSearch( - { - family: "opencode", - providerDisplayName: "opencode", - name: "Claude Opus 4.8 1M", - subProvider: "GitHub Copilot", - }, - "copilot opus", - ); - const fuzzyScore = scoreModelPickerSearch( - { - family: "opencode", - providerDisplayName: "opencode", - name: "Claude Opus 4.8 1M", - subProvider: "GitHub Copilot", - }, - "coplt op", - ); - - expect(exactScore).not.toBeNull(); - expect(fuzzyScore).not.toBeNull(); - expect(exactScore!).toBeLessThan(fuzzyScore!); - }); - - it("gives favorite models a strong enough ranking boost for partial queries", () => { - const favoriteScore = scoreModelPickerSearch( - { - family: "anthropic", - providerDisplayName: "Claude", - name: "Claude Opus 4.8 1M", - isFavorite: true, - }, - "opu", - ); - const nonFavoriteScore = scoreModelPickerSearch( - { - family: "cursor", - providerDisplayName: "Cursor", - name: "Claude Opus 4.5", - }, - "opu", - ); - - expect(favoriteScore).not.toBeNull(); - expect(nonFavoriteScore).not.toBeNull(); - expect(favoriteScore!).toBeLessThan(nonFavoriteScore!); - }); - - it("does not let the favorite boost outrank clearly better textual matches", () => { - const favoriteScore = scoreModelPickerSearch( - { - family: "anthropic", - providerDisplayName: "Claude", - name: "Claude Opus 4.8 1M", - isFavorite: true, - }, - "opus 4.8", - ); - const nonFavoriteExactScore = scoreModelPickerSearch( - { - family: "cursor", - providerDisplayName: "Cursor", - name: "Opus 4.8 1M", - }, - "opus 4.8", - ); - - expect(favoriteScore).not.toBeNull(); - expect(nonFavoriteExactScore).not.toBeNull(); - expect(nonFavoriteExactScore!).toBeLessThan(favoriteScore!); - }); - - it("matches a provider display name against its models", () => { - expect( - scoreModelPickerSearch( - { - family: "openai", - providerDisplayName: "Codex Personal", - name: "GPT-5 Codex", - }, - "personal", - ), - ).not.toBeNull(); - }); - - it("matches Cursor SDK aliases returned by model discovery", () => { - expect( - scoreModelPickerSearch( - { - family: "cursor", - providerDisplayName: "Cursor", - name: "Composer 2", - aliases: ["composer-latest"], - }, - "composer-latest", - ), - ).not.toBeNull(); - }); - - it("returns 0 for an empty query and a non-favorite item", () => { - expect( - scoreModelPickerSearch( - { - family: "anthropic", - providerDisplayName: "Claude", - name: "Claude Opus 4.8 1M", - }, - "", - ), - ).toBe(0); - }); -}); diff --git a/apps/desktop/src/renderer/components/shared/ModelPicker/runtimeCatalogCache.test.ts b/apps/desktop/src/renderer/components/shared/ModelPicker/runtimeCatalogCache.test.ts deleted file mode 100644 index 174788b57..000000000 --- a/apps/desktop/src/renderer/components/shared/ModelPicker/runtimeCatalogCache.test.ts +++ /dev/null @@ -1,104 +0,0 @@ -import { beforeEach, describe, expect, it } from "vitest"; -import { - rememberRuntimeCatalog, - resetModelPickerRuntimeCatalogForTests, - runtimeCatalogProviderIsFresh, -} from "./runtimeCatalogCache"; -import type { AgentChatModelCatalog } from "../../../../shared/types"; - -function cursorCatalog(availability: { sdk: boolean; cli: boolean }): AgentChatModelCatalog { - return { - fetchedAt: new Date().toISOString(), - groups: [ - { - key: "cursor", - displayName: "Cursor", - providers: [ - { - key: "cursor", - displayName: "Cursor", - badgeColor: "#60A5FA", - modelCount: 1, - subsections: [ - { - key: "cursor", - label: "Cursor", - models: [ - { - id: "cursor/composer-2", - runtimeModelId: "cursor/composer-2", - provider: "cursor", - providerKey: "cursor", - groupKey: "cursor", - displayName: "Composer 2", - isDefault: true, - isAvailable: true, - supportsReasoning: true, - supportsTools: true, - cursorAvailability: availability, - }, - ], - }, - ], - }, - ], - }, - ], - }; -} - -describe("runtimeCatalogCache flavor-aware cursor freshness", () => { - beforeEach(() => { - resetModelPickerRuntimeCatalogForTests(); - }); - - it("does not let an SDK-only refresh satisfy a CLI-surface freshness check", () => { - // A chat surface refreshed cursor through the SDK; only SDK rows are - // available in the cached catalog. - rememberRuntimeCatalog(cursorCatalog({ sdk: true, cli: false }), { - mode: "force", - refreshProvider: "cursor", - }); - - // The SDK surface sees its models as fresh... - expect(runtimeCatalogProviderIsFresh("cursor", "sdk")).toBe(true); - // ...but a CLI-flavored surface must still treat cursor as stale, because - // none of the cached rows are runnable through the cursor-agent CLI. - expect(runtimeCatalogProviderIsFresh("cursor", "cli")).toBe(false); - // The flavor-agnostic ("all") check needs BOTH sources fresh, so it is - // stale too — only the SDK source was refreshed. - expect(runtimeCatalogProviderIsFresh("cursor")).toBe(false); - }); - - it("an sdk-scoped refresh leaves the cli surface stale even with dual-capable rows", () => { - // The catalog carries dual-capable rows (cli AND sdk), but the refresh - // only probed the SDK source. The CLI surface must stay stale so a later - // Work-tab CLI picker still forces its own probe (per-source freshness), - // rather than trusting the SDK refresh because dual rows happen to be cli. - rememberRuntimeCatalog(cursorCatalog({ sdk: true, cli: true }), { - mode: "force", - refreshProvider: "cursor", - cursorSource: "sdk", - }); - - expect(runtimeCatalogProviderIsFresh("cursor", "sdk")).toBe(true); - expect(runtimeCatalogProviderIsFresh("cursor", "cli")).toBe(false); - }); - - it("treats both surfaces as fresh once the catalog carries CLI and SDK rows", () => { - rememberRuntimeCatalog(cursorCatalog({ sdk: true, cli: true }), { - mode: "force", - refreshProvider: "cursor", - }); - - expect(runtimeCatalogProviderIsFresh("cursor", "sdk")).toBe(true); - expect(runtimeCatalogProviderIsFresh("cursor", "cli")).toBe(true); - expect(runtimeCatalogProviderIsFresh("cursor")).toBe(true); - }); - - it("reports cursor stale for every flavor before any catalog is cached", () => { - expect(runtimeCatalogProviderIsFresh("cursor", "sdk")).toBe(false); - expect(runtimeCatalogProviderIsFresh("cursor", "cli")).toBe(false); - expect(runtimeCatalogProviderIsFresh("cursor")).toBe(false); - }); -}); diff --git a/apps/desktop/src/shared/modelRegistry.test.ts b/apps/desktop/src/shared/modelRegistry.test.ts index 94947f010..022214e92 100644 --- a/apps/desktop/src/shared/modelRegistry.test.ts +++ b/apps/desktop/src/shared/modelRegistry.test.ts @@ -142,7 +142,7 @@ describe("modelRegistry", () => { displayName: "GPT-5.6 Sol", providerModelId: "gpt-5.6-sol", contextWindow: 372_000, - reasoningTiers: ["low", "medium", "high", "xhigh", "ultra"], + reasoningTiers: ["low", "medium", "high", "xhigh", "max", "ultra"], defaultReasoningEffort: "low", serviceTiers: ["fast"], }); @@ -150,7 +150,7 @@ describe("modelRegistry", () => { displayName: "GPT-5.6 Terra", providerModelId: "gpt-5.6-terra", contextWindow: 372_000, - reasoningTiers: ["low", "medium", "high", "xhigh", "ultra"], + reasoningTiers: ["low", "medium", "high", "xhigh", "max", "ultra"], defaultReasoningEffort: "medium", serviceTiers: ["fast"], }); @@ -158,7 +158,7 @@ describe("modelRegistry", () => { displayName: "GPT-5.6 Luna", providerModelId: "gpt-5.6-luna", contextWindow: 372_000, - reasoningTiers: ["low", "medium", "high", "xhigh"], + reasoningTiers: ["low", "medium", "high", "xhigh", "max"], defaultReasoningEffort: "medium", serviceTiers: ["fast"], }); diff --git a/apps/desktop/src/shared/modelRegistry.ts b/apps/desktop/src/shared/modelRegistry.ts index b2753d60b..9a716cb7c 100644 --- a/apps/desktop/src/shared/modelRegistry.ts +++ b/apps/desktop/src/shared/modelRegistry.ts @@ -406,9 +406,7 @@ export const MODEL_REGISTRY: ModelDescriptor[] = [ contextWindow: 372_000, maxOutputTokens: 128_000, capabilities: ALL_CAPS, - // Codex keeps `max` behind an opt-in model-feature flag. ADE aligns with - // the default product-facing ladder while exposing Ultra orchestration. - reasoningTiers: ["low", "medium", "high", "xhigh", "ultra"], + reasoningTiers: ["low", "medium", "high", "xhigh", "max", "ultra"], defaultReasoningEffort: "low", serviceTiers: ["fast"], color: "#10A37F", @@ -430,7 +428,7 @@ export const MODEL_REGISTRY: ModelDescriptor[] = [ contextWindow: 372_000, maxOutputTokens: 128_000, capabilities: ALL_CAPS, - reasoningTiers: ["low", "medium", "high", "xhigh", "ultra"], + reasoningTiers: ["low", "medium", "high", "xhigh", "max", "ultra"], defaultReasoningEffort: "medium", serviceTiers: ["fast"], color: "#22B88A", @@ -452,7 +450,7 @@ export const MODEL_REGISTRY: ModelDescriptor[] = [ contextWindow: 372_000, maxOutputTokens: 128_000, capabilities: ALL_CAPS, - reasoningTiers: ["low", "medium", "high", "xhigh"], + reasoningTiers: ["low", "medium", "high", "xhigh", "max"], defaultReasoningEffort: "medium", serviceTiers: ["fast"], color: "#34D399", diff --git a/apps/ios/ADE/Views/Components/ADEDesignSystem.swift b/apps/ios/ADE/Views/Components/ADEDesignSystem.swift index 9d0cfb2a0..86bb93813 100644 --- a/apps/ios/ADE/Views/Components/ADEDesignSystem.swift +++ b/apps/ios/ADE/Views/Components/ADEDesignSystem.swift @@ -277,15 +277,15 @@ enum ADEColor { "sonnet": ["low", "medium", "high", "max"], // Claude Haiku intentionally absent — no reasoning tiers. // OpenAI / Codex - "openai/gpt-5.6-sol": ["low", "medium", "high", "xhigh", "ultra"], - "gpt-5.6-sol": ["low", "medium", "high", "xhigh", "ultra"], - "sol": ["low", "medium", "high", "xhigh", "ultra"], - "openai/gpt-5.6-terra": ["low", "medium", "high", "xhigh", "ultra"], - "gpt-5.6-terra": ["low", "medium", "high", "xhigh", "ultra"], - "terra": ["low", "medium", "high", "xhigh", "ultra"], - "openai/gpt-5.6-luna": ["low", "medium", "high", "xhigh"], - "gpt-5.6-luna": ["low", "medium", "high", "xhigh"], - "luna": ["low", "medium", "high", "xhigh"], + "openai/gpt-5.6-sol": ["low", "medium", "high", "xhigh", "max", "ultra"], + "gpt-5.6-sol": ["low", "medium", "high", "xhigh", "max", "ultra"], + "sol": ["low", "medium", "high", "xhigh", "max", "ultra"], + "openai/gpt-5.6-terra": ["low", "medium", "high", "xhigh", "max", "ultra"], + "gpt-5.6-terra": ["low", "medium", "high", "xhigh", "max", "ultra"], + "terra": ["low", "medium", "high", "xhigh", "max", "ultra"], + "openai/gpt-5.6-luna": ["low", "medium", "high", "xhigh", "max"], + "gpt-5.6-luna": ["low", "medium", "high", "xhigh", "max"], + "luna": ["low", "medium", "high", "xhigh", "max"], "openai/gpt-5.5": ["low", "medium", "high", "xhigh"], "gpt-5.5": ["low", "medium", "high", "xhigh"], "openai/gpt-5.4": ["low", "medium", "high", "xhigh"], diff --git a/apps/ios/ADE/Views/Work/WorkModelCatalog.swift b/apps/ios/ADE/Views/Work/WorkModelCatalog.swift index a8bcf1b74..524c88738 100644 --- a/apps/ios/ADE/Views/Work/WorkModelCatalog.swift +++ b/apps/ios/ADE/Views/Work/WorkModelCatalog.swift @@ -236,6 +236,7 @@ private func workCodex56ReasoningEfforts(includeUltra: Bool) -> [AgentChatModelR AgentChatModelReasoningEffort(effort: "medium", description: "Balanced speed and reasoning"), AgentChatModelReasoningEffort(effort: "high", description: "Deeper reasoning for complex work"), AgentChatModelReasoningEffort(effort: "xhigh", description: "Extended reasoning for difficult work"), + AgentChatModelReasoningEffort(effort: "max", description: "Maximum reasoning depth for the hardest problems"), ] if includeUltra { efforts.append(AgentChatModelReasoningEffort( @@ -257,17 +258,11 @@ private func workVisibleReasoningEfforts( || canonicalId == "openai/gpt-5.6-luna" else { return advertised ?? fallback } - let visibleAdvertised = advertised?.filter { - $0.effort.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() != "max" - } ?? [] - if !visibleAdvertised.isEmpty { - return visibleAdvertised + if let advertised, !advertised.isEmpty { + return advertised } - let visibleFallback = fallback.filter { - $0.effort.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() != "max" - } - if !visibleFallback.isEmpty { - return visibleFallback + if !fallback.isEmpty { + return fallback } switch canonicalId { case "openai/gpt-5.6-sol", "openai/gpt-5.6-terra": @@ -291,11 +286,11 @@ private func workVisibleDefaultReasoningEffort( return advertised ?? fallback } let normalizedAdvertised = advertised?.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() - if let normalizedAdvertised, !normalizedAdvertised.isEmpty, normalizedAdvertised != "max" { + if let normalizedAdvertised, !normalizedAdvertised.isEmpty { return normalizedAdvertised } let normalizedFallback = fallback?.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() - if let normalizedFallback, !normalizedFallback.isEmpty, normalizedFallback != "max" { + if let normalizedFallback, !normalizedFallback.isEmpty { return normalizedFallback } switch canonicalId { diff --git a/apps/ios/ADETests/ADETests.swift b/apps/ios/ADETests/ADETests.swift index 32f25c1ca..e36943c61 100644 --- a/apps/ios/ADETests/ADETests.swift +++ b/apps/ios/ADETests/ADETests.swift @@ -13129,18 +13129,18 @@ final class ADETests: XCTestCase { XCTAssertEqual(sol?.displayName, "GPT-5.6 Sol") XCTAssertEqual(sol?.tier, .flagship) XCTAssertEqual(sol?.tagline, "Flagship · 372k context") - XCTAssertEqual(sol?.reasoningEfforts.map(\.effort), ["low", "medium", "high", "xhigh", "ultra"]) + XCTAssertEqual(sol?.reasoningEfforts.map(\.effort), ["low", "medium", "high", "xhigh", "max", "ultra"]) XCTAssertEqual(sol?.defaultReasoningEffort, "low") XCTAssertTrue(sol?.supportsCodexFastMode == true) let terra = codexModels?.first(where: { $0.id == "gpt-5.6-terra" }) XCTAssertEqual(terra?.tier, .balanced) - XCTAssertEqual(terra?.reasoningEfforts.map(\.effort), ["low", "medium", "high", "xhigh", "ultra"]) + XCTAssertEqual(terra?.reasoningEfforts.map(\.effort), ["low", "medium", "high", "xhigh", "max", "ultra"]) XCTAssertEqual(terra?.defaultReasoningEffort, "medium") let luna = codexModels?.first(where: { $0.id == "gpt-5.6-luna" }) XCTAssertEqual(luna?.tier, .fast) - XCTAssertEqual(luna?.reasoningEfforts.map(\.effort), ["low", "medium", "high", "xhigh"]) + XCTAssertEqual(luna?.reasoningEfforts.map(\.effort), ["low", "medium", "high", "xhigh", "max"]) XCTAssertEqual(luna?.defaultReasoningEffort, "medium") XCTAssertTrue(workModelIdsEquivalent("sol", "openai/gpt-5.6-sol")) @@ -13164,9 +13164,9 @@ final class ADETests: XCTestCase { XCTAssertEqual(ADEColor.reasoningTiers(for: "opus[1m]"), ["low", "medium", "high", "xhigh", "max"]) XCTAssertEqual(ADEColor.reasoningTiers(for: "anthropic/claude-sonnet-5"), ["low", "medium", "high", "max"]) XCTAssertNil(ADEColor.reasoningTiers(for: "claude-haiku-4-5")) - XCTAssertEqual(ADEColor.reasoningTiers(for: "sol"), ["low", "medium", "high", "xhigh", "ultra"]) - XCTAssertEqual(ADEColor.reasoningTiers(for: "openai/gpt-5.6-terra"), ["low", "medium", "high", "xhigh", "ultra"]) - XCTAssertEqual(ADEColor.reasoningTiers(for: "gpt-5.6-luna"), ["low", "medium", "high", "xhigh"]) + XCTAssertEqual(ADEColor.reasoningTiers(for: "sol"), ["low", "medium", "high", "xhigh", "max", "ultra"]) + XCTAssertEqual(ADEColor.reasoningTiers(for: "openai/gpt-5.6-terra"), ["low", "medium", "high", "xhigh", "max", "ultra"]) + XCTAssertEqual(ADEColor.reasoningTiers(for: "gpt-5.6-luna"), ["low", "medium", "high", "xhigh", "max"]) XCTAssertEqual(ADEColor.reasoningTiers(for: "openai/gpt-5.3-codex-spark"), ["low", "medium", "high", "xhigh"]) XCTAssertEqual(ADEColor.reasoningTiers(for: "gpt-5.2"), ["low", "medium", "high", "xhigh"]) } @@ -13267,7 +13267,7 @@ final class ADETests: XCTestCase { ["id": "gpt-5.5", "runtimeModelId": "gpt-5.5", "provider": "codex", "providerKey": "openai", "groupKey": "codex", "displayName": "GPT-5.5", "isDefault": false, "isAvailable": true], ["id": "gpt-5.6-luna", "runtimeModelId": "gpt-5.6-luna", "provider": "codex", "providerKey": "openai", "groupKey": "codex", "displayName": "GPT-5.6 Luna", "isDefault": false, "defaultReasoningEffort": "medium", "isAvailable": true], ["id": "gpt-5.6-sol", "runtimeModelId": "gpt-5.6-sol", "provider": "codex", "providerKey": "openai", "groupKey": "codex", "displayName": "GPT-5.6 Sol", "isDefault": true, "defaultReasoningEffort": "low", "reasoningEfforts": [["effort": "low", "description": "fast"], ["effort": "medium", "description": "balanced"], ["effort": "high", "description": "deep"], ["effort": "xhigh", "description": "extended"], ["effort": "max", "description": "optional"], ["effort": "ultra", "description": "delegates"]], "isAvailable": true], - ["id": "gpt-5.6-terra", "runtimeModelId": "gpt-5.6-terra", "provider": "codex", "providerKey": "openai", "groupKey": "codex", "displayName": "GPT-5.6 Terra", "isDefault": false, "defaultReasoningEffort": "medium", "isAvailable": true], + ["id": "gpt-5.6-terra", "runtimeModelId": "gpt-5.6-terra", "provider": "codex", "providerKey": "openai", "groupKey": "codex", "displayName": "GPT-5.6 Terra", "isDefault": false, "defaultReasoningEffort": "max", "isAvailable": true], ], ]], ]], @@ -13284,7 +13284,8 @@ final class ADETests: XCTestCase { XCTAssertEqual(models?.map(\.id), ["gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna", "gpt-5.5"]) XCTAssertEqual(models?.first?.defaultReasoningEffort, "low") - XCTAssertEqual(models?.first?.reasoningEfforts.map(\.effort), ["low", "medium", "high", "xhigh", "ultra"]) + XCTAssertEqual(models?.first?.reasoningEfforts.map(\.effort), ["low", "medium", "high", "xhigh", "max", "ultra"]) + XCTAssertEqual(models?.first(where: { $0.id == "gpt-5.6-terra" })?.defaultReasoningEffort, "max") let legacyListData = try JSONSerialization.data(withJSONObject: [ "id": "gpt-5.6-luna", @@ -13299,7 +13300,7 @@ final class ADETests: XCTestCase { ], ]) let legacyListModel = try JSONDecoder().decode(AgentChatModelInfo.self, from: legacyListData) - XCTAssertEqual(workVisibleReasoningEfforts(for: legacyListModel).map(\.effort), ["low", "medium", "high", "xhigh"]) + XCTAssertEqual(workVisibleReasoningEfforts(for: legacyListModel).map(\.effort), ["low", "medium", "high", "xhigh", "max"]) let flatListData = try JSONSerialization.data(withJSONObject: [ ["id": "gpt-5.5", "displayName": "GPT-5.5", "isDefault": false], diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index a377cdb6c..64f784851 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -204,8 +204,8 @@ Terminal-native **Work** chat client (Ink 7 + React 19) for agents and power use Shared DTOs and cross-client policies are imported from `apps/desktop/src/shared/*` (never the renderer barrel) so `npm run typecheck` in `apps/ade-cli` covers both typed commands and the TUI. This includes `externalSessionAffordances.ts`, which keeps ADE Code's provider-native Continue/Copy choices aligned with desktop while `externalSessionBrowser.ts` owns TUI-only navigation and the Open-existing action. Entry: `apps/ade-cli/src/tuiClient/cli.tsx` → `apps/ade-cli/dist/tuiClient/cli.mjs`, loaded by `ade code`. The built TUI bundle is intended to run in isolation: tsup bundles its Ink/xterm/highlight dependencies and injects ESM shims for `__dirname` / `__filename`; both `apps/ade-cli/scripts/verify-built-cli.mjs` and the desktop artifact validators smoke-import it and run `runAdeCodeCli(["--help"])`. Provider/model/interface setup is kept in pure helpers (`modelState.ts`, `providerMetadata.ts`, `modelPickerController.ts`) so Chat-vs-CLI availability, Cursor SDK-vs-CLI model filtering, permission presets, Fast Mode, and setup rows stay testable outside the Ink root. Chat Info uses shared derivations for subagents, tasks, and scheduled work, so Claude wakeups/cron/background activity rendered in desktop also appears in ADE Code; `AgentChatSessionSummary.nextWakeAt` adds the runtime scheduler's earliest armed fire as an alarm countdown in the Schedule block. The TUI can hand off to a desktop window via the `app/navigate` JSON-RPC method when a desktop client is attached to the same runtime. Model setup shares the desktop registry: GPT-5.6 Sol/Terra/Luna lead the Codex -list, Sol is the default, and host-advertised reasoning defaults are honored -while the GPT-5.6 raw Max tier stays hidden. Transcript aggregation also +list, Sol is the default, and host-advertised reasoning defaults and effort +ladders are honored, including Max before Sol/Terra's Ultra. Transcript aggregation also preserves MCP app/action labels and collapses web/image lifecycle updates. In CLI interface mode, attached and embedded runtime actions use the same signed, explicit-opt-in `computer_use` MCP resolver as desktop when launching or @@ -421,7 +421,7 @@ Agent tools are split by domain: `apps/desktop/src/shared/modelRegistry.ts` + `apps/desktop/src/shared/modelProfiles.ts`: -- `MODEL_REGISTRY` — static CLI-wrapped entries + dynamically populated API-key/local entries. The OpenAI/Codex block is ordered GPT-5.6 Sol, Terra, Luna, then the retained GPT-5.5 and older rows; `pickDefaultCodexModel` chooses the newest Sol row, so new Codex sessions default to GPT-5.6 Sol. All three GPT-5.6 descriptors have a 372k context window and advertise Fast. Sol and Terra expose `low | medium | high | xhigh | ultra`; Luna stops at `xhigh`. The product labels those tiers Light, Medium, High, Extra High, and Ultra. Raw app-server catalogs can report `max`, but ADE hides that feature-gated tier for GPT-5.6. `defaultReasoningEffort` is `low` for Sol and `medium` for Terra/Luna and is honored by desktop, TUI, iOS, CTO, review, and handoff pickers. The Claude block is ordered for every picker as Fable 5, Opus 4.8 1M, Sonnet 5, Haiku 4.5, then Opus 4.7 1M. Sonnet 5 uses provider model `claude-sonnet-5` (1,000,000 context / 128,000 max output); removed Sonnet 4.6 and basic Opus 4.7 ids resolve forward as compatibility aliases but do not appear as selectable rows. Opus 4.7 1M remains available as `anthropic/claude-opus-4-7-1m` with aliases `opus[1m]` / `claude-opus-4-7[1m]`. `ModelDescriptor.serviceTiers?: string[]` advertises optional service tiers (today: `"fast"`, set on Fable/Opus, GPT-5.6 and older fast-capable Codex entries, and dynamic Cursor SDK/CLI rows) that the UI's Fast Mode toggle keys off. Codex maps it to the JSON-RPC `serviceTier` argument; Cursor SDK maps it through discovered model parameters, and Cursor CLI launches use the matching fast model alias when present. +- `MODEL_REGISTRY` — static CLI-wrapped entries + dynamically populated API-key/local entries. The OpenAI/Codex block is ordered GPT-5.6 Sol, Terra, Luna, then the retained GPT-5.5 and older rows; `pickDefaultCodexModel` chooses the newest Sol row, so new Codex sessions default to GPT-5.6 Sol. All three GPT-5.6 descriptors have a 372k context window and advertise Fast. Sol and Terra expose `low | medium | high | xhigh | max | ultra`; Luna exposes `low | medium | high | xhigh | max`. The product labels those tiers Light, Medium, High, Extra High, Max, and (for Sol/Terra) Ultra. Runtime app-server ladders pass through in their advertised order, including Max. `defaultReasoningEffort` is `low` for Sol and `medium` for Terra/Luna and is honored by desktop, TUI, iOS, CTO, review, and handoff pickers. The Claude block is ordered for every picker as Fable 5, Opus 4.8 1M, Sonnet 5, Haiku 4.5, then Opus 4.7 1M. Sonnet 5 uses provider model `claude-sonnet-5` (1,000,000 context / 128,000 max output); removed Sonnet 4.6 and basic Opus 4.7 ids resolve forward as compatibility aliases but do not appear as selectable rows. Opus 4.7 1M remains available as `anthropic/claude-opus-4-7-1m` with aliases `opus[1m]` / `claude-opus-4-7[1m]`. `ModelDescriptor.serviceTiers?: string[]` advertises optional service tiers (today: `"fast"`, set on Fable/Opus, GPT-5.6 and older fast-capable Codex entries, and dynamic Cursor SDK/CLI rows) that the UI's Fast Mode toggle keys off. Codex maps it to the JSON-RPC `serviceTier` argument; Cursor SDK maps it through discovered model parameters, and Cursor CLI launches use the matching fast model alias when present. - `ModelProviderGroup` = `"claude" | "codex" | "opencode" | "cursor" | "droid"`. Cursor and Droid each have their own top-level provider group used by the model picker, identity routing, and tracked CLI provider catalog. - Helpers: `getModelById`, `getModelPricing`, `updateModelPricingInRegistry`, `replaceDynamicOpenCodeModelDescriptors`, `resolveProviderGroupForModel`, `resolveModelDescriptorForProvider`, `getRuntimeModelRefForDescriptor`, `modelSupportsServiceTier(descriptor, tier)` / `modelSupportsFastMode(descriptor)`. - Reasoning tier passthrough (`providerOptions.ts`) maps tier strings directly to each provider's native config (`thinking.type`, `reasoningEffort`, `thinkingConfig.thinkingLevel`, etc.) — no arbitrary token budgets. Claude Fable and Opus 4.8 rows advertise `low | medium | high | xhigh | max | ultracode`; Sonnet 5 advertises `low | medium | high | max`. diff --git a/docs/features/ade-code/README.md b/docs/features/ade-code/README.md index b3428d84b..11a9bb92d 100644 --- a/docs/features/ade-code/README.md +++ b/docs/features/ade-code/README.md @@ -35,7 +35,7 @@ Point Cursor’s browser inspector at the served page for layout debugging. The | `apps/ade-cli/src/tuiClient/remoteBridge.ts` | Remote transport shim used by `remoteLauncher.ts`: starts `ade rpc --stdio` over SSH, performs process-backed JSON-RPC for selection/listing, then exposes a local one-connection bridge socket (Unix socket on POSIX, loopback TCP on Windows) to the regular TUI. Owns stderr tails, 16 MB RPC frame limits, child-process cleanup, and bridge-socket teardown. | | `apps/ade-cli/src/tuiClient/commands.ts` / `linearCommands.ts` | Slash command catalog and routing. `commands.ts` ships `/lane delete` (right-pane confirmation form that destroys the active lane), `/effort` (reasoning-effort-only picker, a narrower companion to `/model`), provider-agnostic `/skills` for Agent Skill discovery, and provider-agnostic `/secrets` for masked project-secret listing/copying. `linearCommands.ts` requires a sub-command — bare `/linear` returns the usage hint instead of silently picking `workflows`. It also routes the session/lane attachment verbs (`attach` / `detach` / `issues` → `lane` domain session-scoped or lane-scoped actions) and the issue write-bridge verbs (`comment` / `set-state` / `assign` / `label` → `linear_issue_tracker` domain), reusing `--issue-id` / `--linear-issue-json` / attachment flags (`source`, `includeInPr`, `closeOnMerge`, `role`) parsing shared with the typed `ade linear` CLI commands in `cli.ts`. | | `apps/ade-cli/src/tuiClient/providerMetadata.ts` | Provider labels, family labels, token normalization, and provider lookup helpers shared by setup rows and the model picker. Keeps Anthropic/OpenAI/Factory aliases mapped onto the TUI's provider ids and decides which providers support runtime catalog refresh. | -| `apps/ade-cli/src/tuiClient/modelState.ts` | Pure model/setup state for draft chats and `/model`: GPT-5.6 Sol default plus Sol/Terra/Luna ordering, Chat vs CLI interface mode, Cursor chat-vs-CLI availability reconciliation, Codex preset/approval/sandbox mapping, provider-specific permission summaries, host-aware reasoning defaults/visible tiers, Fast Mode support, and the `SetupPaneRow` list rendered in setup panes. GPT-5.6 labels `low` as Light, `xhigh` as Extra High, exposes Ultra on Sol/Terra, and filters the feature-gated raw Max tier. | +| `apps/ade-cli/src/tuiClient/modelState.ts` | Pure model/setup state for draft chats and `/model`: GPT-5.6 Sol default plus Sol/Terra/Luna ordering, Chat vs CLI interface mode, Cursor chat-vs-CLI availability reconciliation, Codex preset/approval/sandbox mapping, provider-specific permission summaries, host-aware reasoning defaults/visible tiers, Fast Mode support, and the `SetupPaneRow` list rendered in setup panes. GPT-5.6 labels `low` as Light and `xhigh` as Extra High, exposes Max on all three models, and adds Ultra after Max on Sol/Terra. | | `apps/ade-cli/src/tuiClient/modelPickerController.ts` | Small adapter between right-pane model-picker state and `modelPickerLayout.ts`: supplies active model/reasoning/interface, favorites/recents, AI status, footer focus, lane label, and provider refresh routing. | | `apps/ade-cli/src/tuiClient/rightPaneFormatters.ts` | Pure formatters for right-pane result panes (PR summary / review / checks / comments, Linear status, system details). Keeps `app.tsx` free of ad-hoc rendering helpers. | | `apps/ade-cli/src/tuiClient/format.ts` | Transcript rendering helpers for the TUI. | @@ -336,7 +336,7 @@ After local changes, run `npm run build` inside `apps/ade-cli` so both `dist/cli - `+ new chat` opens a draft setup view (`new-chat-setup`) in the right pane; it does not create a backend chat until the first prompt is sent from the middle composer. - The draft setup and `/model` panes carry an **Interface: Chat | CLI** row (immediately after Provider), matching the desktop/iOS Chat/CLI switcher. **Chat** creates an SDK chat via `chat.createSession` (all providers, including Claude); **CLI** starts a tracked provider CLI terminal via the `start_cli_session` action (Claude, Codex, Cursor, Droid, or OpenCode). New drafts default to the last explicitly selected Interface for the project, persisted in `~/.ade/ade-code-state.json` with legacy fallback; programmatic draft resets do not overwrite it. The row is editable while the chat is a draft and becomes read-only once a session exists (its value then reflects the session type). First-prompt submit branches on it: draft `CLI` starts a tracked terminal; otherwise an SDK chat is created. Ollama / LM Studio have no CLI and stay Chat-only. - `/model` opens the model setup view (`model-setup`) in the right pane. It can switch provider, model, reasoning, Fast Mode for fast-capable descriptors, and permission settings, refresh provider readiness through `ai.getStatus`, and open desktop Settings > AI Providers for full configuration. Cursor rows come from the same provider-grouped catalog as desktop, including `cursorAvailability`, so SDK chat models and Cursor CLI launch models stay separated consistently — and the picker gates Cursor availability on the selected Interface (Chat disables CLI-only Cursor models and vice versa). -- The Codex/OpenAI list always begins GPT-5.6 Sol, Terra, Luna; Sol is the new-chat default and GPT-5.5 remains below the family. Sol/Terra expose Light, Medium, High, Extra High, Ultra; Luna stops at Extra High. ADE Code uses each descriptor/host row's `defaultReasoningEffort` (`low` for Sol, `medium` for Terra/Luna) and filters raw `max` from these three models. +- The Codex/OpenAI list always begins GPT-5.6 Sol, Terra, Luna; Sol is the new-chat default and GPT-5.5 remains below the family. Sol/Terra expose Light, Medium, High, Extra High, Max, Ultra; Luna exposes Light, Medium, High, Extra High, Max. ADE Code uses each descriptor/host row's `defaultReasoningEffort` (`low` for Sol, `medium` for Terra/Luna) and preserves host-advertised effort ordering. - On macOS, both Chat and CLI Codex drafts inherit the same direct Computer Use integration as desktop. If the bundled plugin or canonical MCP server is explicitly enabled in Codex config and the standalone OpenAI client passes strict signature checks, native chats receive it on app-server thread start/resume and CLI launches receive `mcp_servers.computer_use` overrides. Disabled/unverified/missing clients add no flags. MCP app consent still appears as pending input; Full Auto does not bypass it. - `/login` delegates only to provider CLIs that can authenticate in the current terminal: Claude (`claude auth login`), Codex (`codex login`), and OpenCode (`opencode auth login`). After a successful login for the active provider, an auth-failed latest prompt is restored into the composer with a "logged in — press Enter to resend" notice. Cursor chat is `@cursor/sdk` and needs `CURSOR_API_KEY` or desktop Settings > AI Providers. Droid chat runs Factory Droid over ACP and needs `FACTORY_API_KEY` or Factory's interactive `droid` login. - The middle composer shows the selected provider, model, reasoning, and permission mode under the prompt so draft changes on the right are visible before the chat starts. diff --git a/docs/features/chat/README.md b/docs/features/chat/README.md index 37b391c4e..4fa78d3a5 100644 --- a/docs/features/chat/README.md +++ b/docs/features/chat/README.md @@ -24,6 +24,7 @@ for its separate RPC, sync, storage, and UI contracts. | `apps/desktop/src/main/services/chat/externalChatHistoryImport.ts` | Converts external Claude JSONL and Codex thread-turn history into ADE `AgentChatEventEnvelope` rows. It reads at most the last 32 MB of source transcript bytes, keeps the newest 2,000 imported content events, emits system notices for provenance/truncation, drops metadata-only/provider-wrapper user rows without stripping user-authored JSX/XML, preserves failed Claude tool-result status, maps user/assistant text plus tool calls/results/file changes/commands/search/image events where available, and derives a fallback imported-chat title from the first user or assistant text. | | `apps/desktop/src/main/services/chat/runtimeEvents.ts` | Canonical cross-runtime event vocabulary (`turn.*`, `content.delta`, `tool.*`, `subagent.*`, teammate/task events, compaction boundaries) plus shims between legacy `AgentChatEvent` rows and the canonical runtime envelope. Claude emits canonical subagent events alongside the legacy rows while the other adapters migrate. | | `apps/ade-cli/src/tuiClient/` | Terminal **Work** chat TUI (Ink + React): same action/RPC contracts as desktop, **attached** (socket) or **embedded** (headless runtime via `ade-cli`). See [ADE Code](../ade-code/README.md). | +| `apps/desktop/src/shared/modelRegistry.ts`, `apps/desktop/src/renderer/components/shared/ModelPicker/modelCatalog.ts` | Shared static model descriptors plus renderer merge of host-advertised catalogs. GPT-5.6 Sol/Terra/Luna stay first, Sol remains the Codex default, and runtime reasoning ladders pass through in provider order: Max precedes Ultra for Sol/Terra, while Luna ends at Max. | | `apps/desktop/src/main/services/builtInBrowser/builtInBrowserService.ts` | Main-process broker for the in-app web browser. Owns persistent project-profile partitions derived from the active project root (fallback `persist:ade-browser`) and one window/project browser service per ADE `BrowserWindow`, so each project keeps isolated cookies/storage while its tabs share that project's authenticated browser profile. Each window service manages multiple `WebContentsView` tabs (cap 10), active tab, per-tab lane/chat owner and lease metadata, lightweight browser agent sessions, bounds, visibility, inspect state, targeted status events, screenshot capture, scratch browser-agent observations, diagnostics, per-tab action traces, and emission of `BuiltInBrowserContextItem`s for selected page elements. Observe/click/type/key/scroll/fill/clear/wait/screenshot/select/reload/back/forward/stop can target a hidden or non-active tab by `tabId` or `sessionId`; inspect mode remains a visible-tab interaction. Sessions bind an agent workflow to one tab, remember owner plus last observation/trace ids, and have `ade browser session ` CLI aliases. Scratch observations live under `.ade/cache/browser-observations/`, include a bounded DOM element list plus console/network diagnostics by default, can render a numbered element-map screenshot with `includeElementMap`, and prune to the latest 3 observations per tab by default; click/fill/clear/press/wait can target viewport coordinates or resolve `selector`/`text`/`testId`/`elementIndex`/saved observation `handle` before dispatching CDP input. Waits wake from browser/network/page events with a timeout fallback, and `network-idle` requires complete ready state, no pending browser requests, and a configurable idle window. Handles preserve same-origin iframe/open-shadow-root context when available, and tab traces record action target metadata, duration, before/after URL, session id, observation id, and errors without storing typed fill/type text. `ade browser proof` promotes a fresh scratch observation to durable proof through the proof broker. Window-open requests from a page are handled via `setWindowOpenHandler` returning `action: "allow"` + a `createWindow` factory: a new internal tab is created and its `webContents` is returned to Chromium so the popup keeps its real `window.opener` relationship with the opener tab (important for OAuth flows that postMessage back to the parent). Download requests are saved through the browser session with sanitized, unique filenames in the user's Downloads folder instead of falling through to Chromium defaults. Navigation normalization/protocol policy lives in `builtInBrowserNavigation.ts`; Google sign-in permission policy lives in `builtInBrowserPermissions.ts`. Backs the `ade.builtInBrowser.*` IPC surface and is consumed by both `ChatBuiltInBrowserPanel` (sidebar Browser tab) and `openExternal.ts` (links inside the renderer route through the built-in browser when the protocol is `http`/`https`/`about:blank`). | | `apps/desktop/src/shared/types/builtInBrowser.ts` | Cross-process types for the built-in browser: `BuiltInBrowserStatus`, `BuiltInBrowserTab` (including per-tab owner/lease metadata), `BuiltInBrowserSession`, `BuiltInBrowserContextItem` (`kind: "built_in_browser_element" | "built_in_browser_capture"`), `BuiltInBrowserSelectResult`, `BuiltInBrowserScreenshot`, `BuiltInBrowserObservation` / `BuiltInBrowserDomSnapshot` / `BuiltInBrowserObservationElementMap`, browser diagnostics/action trace DTOs, agent action args for click/type/key/scroll/fill/clear/wait, `BuiltInBrowserOpenPanelArgs`, and the `BuiltInBrowserEventPayload` union (`status`, `open-request`, `selection`, `selection-cleared`, `error`). Navigate / create-tab / switch-tab args carry an optional `openPanel: boolean` so callers can ask for the Work sidebar Browser tab to flip open atomically with the navigation. | | `apps/desktop/src/shared/types/personalChats.ts` | Machine-scope personal-chat action, capability, result, queue-policy, and event-stream contract layered over the same `AgentChatSession` DTOs. | @@ -87,7 +88,7 @@ for its separate RPC, sync, storage, and UI contracts. | `apps/desktop/src/renderer/lib/claudeAuthPrompt.ts` | Renderer-side classifier for Claude logged-out / `/login`-required error text. Drives the header and sticky login CTAs; matches both Claude-first wording and ADE's own "Authentication failed for <model>" classified message. | | `apps/desktop/src/renderer/lib/openExternal.ts` | Renderer-side router for outbound URLs. Defines the `ADE_OPEN_BUILT_IN_BROWSER_EVENT` window event plus `openUrlInAdeBrowser(url)` and `openExternalUrl(url)`. `openUrlInAdeBrowser` dispatches the event (so any open `WorkSidebar` can flip to its Browser tab), then calls `window.ade.builtInBrowser.navigate({ url, newTab: true })`. Anything that is not a normal `http`/`https`/`about:blank` URL falls through to `window.ade.app.openExternal` (system browser). All in-renderer URL clicks (markdown links, lane-runtime open buttons, etc.) go through this helper so the user stays inside ADE. | | `apps/desktop/src/renderer/components/chat/AgentChatComposer.tsx` | Composer UI: single-session prompt entry, attachments, model/permission controls, slash commands, pending input answering, and parallel launch slot configuration. Permission popover rows keep only the mode title in the visible row (the explanation remains in the tooltip/title) for every provider-backed picker. Codex MCP elicitations show Allow once / Deny, conditionally show Always allow, and expose safe URL authorization through ADE's browser. Pasted/dropped image attachments show pending thumbnails while temp files save, and native Electron clipboard images read bytes through `ade.app.readClipboardImage` then write them through `ade.agentChat.saveTempAttachment` so remote-bound chats receive a runtime-readable attachment path. The launch-prompt clipboard helper is gated separately from prompt copying: `launchPromptClipboardEnabled` controls copying and `launchPromptClipboardNoticeEnabled` controls whether composer reminder text is shown. Orchestration model-selection pending inputs decode the full agent briefing metadata (`workDescription`, `filesHint`, `dependsOn`) so the picker can show what the lead is spawning without preselecting a recommended model. | -| `apps/desktop/src/renderer/components/shared/ModelPicker/ReasoningEffortPicker.tsx` | Shared reasoning slider. Supports pointer drag with nearest-tick snap, keyboard arrows/Home/End, a progressive filled gradient and active-tier pulse, GPT-5.6 labels (Light through Ultra), and an Ultra multi-agent usage note. Choosing or dragging to a tier leaves the popover open; outside click or Escape closes it. | +| `apps/desktop/src/renderer/components/shared/ModelPicker/ReasoningEffortPicker.tsx` | Shared reasoning slider. Supports pointer drag with nearest-tick snap, keyboard arrows/Home/End, a progressive filled gradient and active-tier pulse, GPT-5.6 labels (Light, Medium, High, Extra High, Max, and Ultra where supported), and an Ultra multi-agent usage note. Choosing or dragging to a tier leaves the popover open; outside click or Escape closes it. | | `apps/desktop/src/renderer/components/chat/ChatModelSelectionPendingCard.tsx` | Pending-input card used when ADE asks the user to choose a model for a new or rerouted agent. It renders the agent briefing, touched files, run-after dependencies, provider/model controls, cancel/confirm states, and leaves the model unset until the user chooses one. | | `apps/desktop/src/renderer/components/chat/ChatCursorCloudPanel.tsx` | Side panel for Cursor Cloud (background agents): lists existing cloud agents and runs for the lane, lets the user open an existing cloud chat in ADE, archive/unarchive/cancel, and stream run output. Backed by `ade.ai.cursorCloud.*` IPC. | | `apps/desktop/src/renderer/components/chat/CursorCloudInlineLaunch.tsx` | Inline composer affordance for "Send to Cursor Cloud": picks repo + branch + Cursor Cloud-eligible model, optionally targeting a detected PR, and dispatches the prompt to a fresh cloud agent. | diff --git a/docs/features/chat/agent-routing.md b/docs/features/chat/agent-routing.md index a43da7d37..4dd01bc71 100644 --- a/docs/features/chat/agent-routing.md +++ b/docs/features/chat/agent-routing.md @@ -114,12 +114,10 @@ The OpenAI section is pinned in this order on every ADE model surface: 3. `openai/gpt-5.6-luna` (`gpt-5.6-luna`) — 372k context; default effort `medium`. GPT-5.5 remains selectable below them. Sol and Terra expose `low | medium | -high | xhigh | ultra`; Luna exposes `low | medium | high | xhigh`. Desktop, -ADE Code, and iOS label those values Light, Medium, High, Extra High, and -Ultra. `ultra` is the multi-agent tier and carries a usage warning. Although a -raw 0.144.0 app-server catalog may advertise `max`, Codex Desktop puts that -tier behind a separate feature opt-in, so ADE filters it from the GPT-5.6 -picker ladder rather than presenting it as an ordinary level. +high | xhigh | max | ultra`; Luna exposes `low | medium | high | xhigh | max`. +Desktop, ADE Code, and iOS label those values Light, Medium, High, Extra High, +Max, and (for Sol/Terra) Ultra. Runtime app-server ladders retain their +advertised order. `ultra` is the multi-agent tier and carries a usage warning. `selectSupportedReasoningEffort()` centralizes fallback order: keep a valid explicit selection, then use the model's advertised default, then a valid diff --git a/docs/features/sync-and-multi-device/ios-companion.md b/docs/features/sync-and-multi-device/ios-companion.md index c5fa03e84..0d3e9cbdc 100644 --- a/docs/features/sync-and-multi-device/ios-companion.md +++ b/docs/features/sync-and-multi-device/ios-companion.md @@ -1513,9 +1513,9 @@ does not duplicate the full desktop Stats page. rows. The OpenAI picker always promotes GPT-5.6 Sol, Terra, Luna in that order even when a host returns another order; Sol is the fallback default and GPT-5.5 remains below them. The phone prefers host-advertised reasoning - tiers/defaults, filters raw `max` for GPT-5.6, and falls back to Light / - Medium / High / Extra High / Ultra on Sol/Terra and through Extra High on - Luna (`low` for Sol; `medium` for Terra/Luna). `shell` remains valid runtime-side but the phone no longer + tiers/defaults in their original order and falls back to Light / Medium / + High / Extra High / Max / Ultra on Sol/Terra and through Max on Luna + (`low` for Sol; `medium` for Terra/Luna). `shell` remains valid runtime-side but the phone no longer offers a plain-shell launch. `SyncStartCliSessionArgs` also carries an optional `reasoningEffort` field that the runtime forwards to `buildTrackedCliLaunchCommand`, so the phone can launch a Codex /