| author | |
| committer | |
| log | 116c8c5e8e0db2fe7fad070c2f67738a62f93546 |
| tree | ec6b3428610593479a94c4d56c0cd4d354e63e62 |
| parent | f8886e874d43fe591fa50d28e9a8983632ba7e45 |
6 files changed, 40 insertions(+), 70 deletions(-)
apps/web/src/components/ChatView.tsx+12-17| ... | ... | @@ -1,6 +1,6 @@ |
| 1 | 1 | import { |
| 2 | 2 | type ApprovalRequestId, |
| 3 | DEFAULT_MODEL, | |
| 3 | DEFAULT_MODEL_BY_PROVIDER, | |
| 4 | 4 | EDITORS, |
| 5 | 5 | type EditorId, |
| 6 | 6 | type KeybindingCommand, |
| ... | ... | @@ -575,7 +575,9 @@ export default function ChatView({ threadId }: ChatViewProps) { |
| 575 | 575 | const setComposerDraftProvider = useComposerDraftStore((store) => store.setProvider); |
| 576 | 576 | const setComposerDraftModel = useComposerDraftStore((store) => store.setModel); |
| 577 | 577 | const setComposerDraftRuntimeMode = useComposerDraftStore((store) => store.setRuntimeMode); |
| 578 | const setComposerDraftInteractionMode = useComposerDraftStore((store) => store.setInteractionMode); | |
| 578 | const setComposerDraftInteractionMode = useComposerDraftStore( | |
| 579 | (store) => store.setInteractionMode, | |
| 580 | ); | |
| 579 | 581 | const setComposerDraftEffort = useComposerDraftStore((store) => store.setEffort); |
| 580 | 582 | const setComposerDraftCodexFastMode = useComposerDraftStore((store) => store.setCodexFastMode); |
| 581 | 583 | const addComposerDraftImage = useComposerDraftStore((store) => store.addImage); |
| ... | ... | @@ -703,14 +705,15 @@ export default function ChatView({ threadId }: ChatViewProps) { |
| 703 | 705 | ? buildLocalDraftThread( |
| 704 | 706 | threadId, |
| 705 | 707 | draftThread, |
| 706 | fallbackDraftProject?.model ?? DEFAULT_MODEL, | |
| 708 | fallbackDraftProject?.model ?? DEFAULT_MODEL_BY_PROVIDER.codex, | |
| 707 | 709 | localDraftError, |
| 708 | 710 | ) |
| 709 | 711 | : undefined, |
| 710 | 712 | [draftThread, fallbackDraftProject?.model, localDraftError, threadId], |
| 711 | 713 | ); |
| 712 | 714 | const activeThread = serverThread ?? localDraftThread; |
| 713 | const runtimeMode = composerDraft.runtimeMode ?? activeThread?.runtimeMode ?? DEFAULT_RUNTIME_MODE; | |
| 715 | const runtimeMode = | |
| 716 | composerDraft.runtimeMode ?? activeThread?.runtimeMode ?? DEFAULT_RUNTIME_MODE; | |
| 714 | 717 | const interactionMode = |
| 715 | 718 | composerDraft.interactionMode ?? activeThread?.interactionMode ?? DEFAULT_INTERACTION_MODE; |
| 716 | 719 | const isServerThread = serverThread !== undefined; |
| ... | ... | @@ -757,10 +760,7 @@ export default function ChatView({ threadId }: ChatViewProps) { |
| 757 | 760 | const selectedProvider: ProviderKind = lockedProvider ?? selectedProviderByThreadId ?? "codex"; |
| 758 | 761 | const baseThreadModel = resolveModelSlugForProvider( |
| 759 | 762 | selectedProvider, |
| 760 | activeThread?.model ?? | |
| 761 | activeProject?.model ?? | |
| 762 | getDefaultModel(selectedProvider) ?? | |
| 763 | DEFAULT_MODEL, | |
| 763 | activeThread?.model ?? activeProject?.model ?? getDefaultModel(selectedProvider), | |
| 764 | 764 | ); |
| 765 | 765 | const selectedModel = useMemo(() => { |
| 766 | 766 | const draftModel = composerDraft.model; |
| ... | ... | @@ -2470,7 +2470,7 @@ export default function ChatView({ threadId }: ChatViewProps) { |
| 2470 | 2470 | } |
| 2471 | 2471 | const title = truncateTitle(titleSeed); |
| 2472 | 2472 | let threadCreateModel: ModelSlug = |
| 2473 | selectedModel || (activeProject.model as ModelSlug) || DEFAULT_MODEL; | |
| 2473 | selectedModel || (activeProject.model as ModelSlug) || DEFAULT_MODEL_BY_PROVIDER.codex; | |
| 2474 | 2474 | |
| 2475 | 2475 | if (isLocalDraftThread) { |
| 2476 | 2476 | await api.orchestration.dispatchCommand({ |
| ... | ... | @@ -2881,7 +2881,7 @@ export default function ChatView({ threadId }: ChatViewProps) { |
| 2881 | 2881 | selectedModel || |
| 2882 | 2882 | (activeThread.model as ModelSlug) || |
| 2883 | 2883 | (activeProject.model as ModelSlug) || |
| 2884 | DEFAULT_MODEL; | |
| 2884 | DEFAULT_MODEL_BY_PROVIDER.codex; | |
| 2885 | 2885 | |
| 2886 | 2886 | sendInFlightRef.current = true; |
| 2887 | 2887 | setSendPhase("sending-turn"); |
| ... | ... | @@ -5127,9 +5127,7 @@ const MessagesTimeline = memo(function MessagesTimeline({ |
| 5127 | 5127 | ); |
| 5128 | 5128 | }); |
| 5129 | 5129 | |
| 5130 | function isAvailableProviderOption( | |
| 5131 | option: (typeof PROVIDER_OPTIONS)[number], | |
| 5132 | ): option is { | |
| 5130 | function isAvailableProviderOption(option: (typeof PROVIDER_OPTIONS)[number]): option is { | |
| 5133 | 5131 | value: ProviderKind; |
| 5134 | 5132 | label: string; |
| 5135 | 5133 | available: true; |
| ... | ... | @@ -5227,10 +5225,7 @@ const ProviderModelPicker = memo(function ProviderModelPicker(props: { |
| 5227 | 5225 | } |
| 5228 | 5226 | > |
| 5229 | 5227 | <span className="flex min-w-0 items-center gap-2"> |
| 5230 | <ProviderIcon | |
| 5231 | aria-hidden="true" | |
| 5232 | className="size-4 shrink-0 text-muted-foreground/70" | |
| 5233 | /> | |
| 5228 | <ProviderIcon aria-hidden="true" className="size-4 shrink-0 text-muted-foreground/70" /> | |
| 5234 | 5229 | <span className="truncate">{selectedModelLabel}</span> |
| 5235 | 5230 | <ChevronDownIcon aria-hidden="true" className="size-3 opacity-60" /> |
| 5236 | 5231 | </span> |
apps/web/src/components/Sidebar.tsx+3-3| ... | ... | @@ -9,7 +9,7 @@ import { |
| 9 | 9 | import { useCallback, useEffect, useMemo, useRef, useState } from "react"; |
| 10 | 10 | import { |
| 11 | 11 | DEFAULT_RUNTIME_MODE, |
| 12 | DEFAULT_MODEL, | |
| 12 | DEFAULT_MODEL_BY_PROVIDER, | |
| 13 | 13 | type DesktopUpdateState, |
| 14 | 14 | ProjectId, |
| 15 | 15 | ThreadId, |
| ... | ... | @@ -506,7 +506,7 @@ export default function Sidebar() { |
| 506 | 506 | projectId, |
| 507 | 507 | title, |
| 508 | 508 | workspaceRoot: cwd, |
| 509 | defaultModel: DEFAULT_MODEL, | |
| 509 | defaultModel: DEFAULT_MODEL_BY_PROVIDER.codex, | |
| 510 | 510 | createdAt, |
| 511 | 511 | }) |
| 512 | 512 | .then(() => true) |
| ... | ... | @@ -885,7 +885,7 @@ export default function Sidebar() { |
| 885 | 885 | ? "text-sky-400" |
| 886 | 886 | : shouldHighlightDesktopUpdateError(desktopUpdateState) |
| 887 | 887 | ? "text-rose-500 animate-pulse" |
| 888 | : "text-amber-500 animate-pulse"; | |
| 888 | : "text-amber-500 animate-pulse"; | |
| 889 | 889 | const newThreadShortcutLabel = useMemo( |
| 890 | 890 | () => |
| 891 | 891 | shortcutLabelForCommand(keybindings, "chat.newLocal") ?? |
apps/web/src/store.ts+14-35| ... | ... | @@ -1,6 +1,6 @@ |
| 1 | 1 | import { Fragment, type ReactNode, createElement, useEffect } from "react"; |
| 2 | 2 | import { |
| 3 | DEFAULT_MODEL, | |
| 3 | DEFAULT_MODEL_BY_PROVIDER, | |
| 4 | 4 | type ProviderKind, |
| 5 | 5 | ThreadId, |
| 6 | 6 | type OrchestrationReadModel, |
| ... | ... | @@ -13,11 +13,7 @@ import { |
| 13 | 13 | resolveModelSlugForProvider, |
| 14 | 14 | } from "@t3tools/shared/model"; |
| 15 | 15 | import { create } from "zustand"; |
| 16 | import { | |
| 17 | type ChatMessage, | |
| 18 | type Project, | |
| 19 | type Thread, | |
| 20 | } from "./types"; | |
| 16 | import { type ChatMessage, type Project, type Thread } from "./types"; | |
| 21 | 17 | |
| 22 | 18 | // ── State ──────────────────────────────────────────────────────────── |
| 23 | 19 | |
| ... | ... | @@ -114,7 +110,9 @@ function mapProjectsFromReadModel( |
| 114 | 110 | id: project.id, |
| 115 | 111 | name: project.title, |
| 116 | 112 | cwd: project.workspaceRoot, |
| 117 | model: existing?.model ?? resolveModelSlug(project.defaultModel ?? DEFAULT_MODEL), | |
| 113 | model: | |
| 114 | existing?.model ?? | |
| 115 | resolveModelSlug(project.defaultModel ?? DEFAULT_MODEL_BY_PROVIDER.codex), | |
| 118 | 116 | expanded: |
| 119 | 117 | existing?.expanded ?? |
| 120 | 118 | (persistedExpandedProjectCwds.size > 0 |
| ... | ... | @@ -181,11 +179,7 @@ function resolveWsHttpOrigin(): string { |
| 181 | 179 | try { |
| 182 | 180 | const wsUrl = new URL(wsCandidate); |
| 183 | 181 | const protocol = |
| 184 | wsUrl.protocol === "wss:" | |
| 185 | ? "https:" | |
| 186 | : wsUrl.protocol === "ws:" | |
| 187 | ? "http:" | |
| 188 | : wsUrl.protocol; | |
| 182 | wsUrl.protocol === "wss:" ? "https:" : wsUrl.protocol === "ws:" ? "http:" : wsUrl.protocol; | |
| 189 | 183 | return `${protocol}//${wsUrl.host}`; |
| 190 | 184 | } catch { |
| 191 | 185 | return window.location.origin; |
| ... | ... | @@ -205,17 +199,12 @@ function attachmentPreviewRoutePath(attachmentId: string): string { |
| 205 | 199 | |
| 206 | 200 | // ── Pure state transition functions ──────────────────────────────────── |
| 207 | 201 | |
| 208 | export function syncServerReadModel( | |
| 209 | state: AppState, | |
| 210 | readModel: OrchestrationReadModel, | |
| 211 | ): AppState { | |
| 202 | export function syncServerReadModel(state: AppState, readModel: OrchestrationReadModel): AppState { | |
| 212 | 203 | const projects = mapProjectsFromReadModel( |
| 213 | 204 | readModel.projects.filter((project) => project.deletedAt === null), |
| 214 | 205 | state.projects, |
| 215 | 206 | ); |
| 216 | const existingThreadById = new Map( | |
| 217 | state.threads.map((thread) => [thread.id, thread] as const), | |
| 218 | ); | |
| 207 | const existingThreadById = new Map(state.threads.map((thread) => [thread.id, thread] as const)); | |
| 219 | 208 | const threads = readModel.threads |
| 220 | 209 | .filter((thread) => thread.deletedAt === null) |
| 221 | 210 | .map((thread) => { |
| ... | ... | @@ -334,9 +323,7 @@ export function markThreadUnread(state: AppState, threadId: ThreadId): AppState |
| 334 | 323 | export function toggleProject(state: AppState, projectId: Project["id"]): AppState { |
| 335 | 324 | return { |
| 336 | 325 | ...state, |
| 337 | projects: state.projects.map((p) => | |
| 338 | p.id === projectId ? { ...p, expanded: !p.expanded } : p, | |
| 339 | ), | |
| 326 | projects: state.projects.map((p) => (p.id === projectId ? { ...p, expanded: !p.expanded } : p)), | |
| 340 | 327 | }; |
| 341 | 328 | } |
| 342 | 329 | |
| ... | ... | @@ -390,27 +377,19 @@ interface AppStore extends AppState { |
| 390 | 377 | toggleProject: (projectId: Project["id"]) => void; |
| 391 | 378 | setProjectExpanded: (projectId: Project["id"], expanded: boolean) => void; |
| 392 | 379 | setError: (threadId: ThreadId, error: string | null) => void; |
| 393 | setThreadBranch: ( | |
| 394 | threadId: ThreadId, | |
| 395 | branch: string | null, | |
| 396 | worktreePath: string | null, | |
| 397 | ) => void; | |
| 380 | setThreadBranch: (threadId: ThreadId, branch: string | null, worktreePath: string | null) => void; | |
| 398 | 381 | } |
| 399 | 382 | |
| 400 | 383 | export const useStore = create<AppStore>((set) => ({ |
| 401 | 384 | ...readPersistedState(), |
| 402 | syncServerReadModel: (readModel) => | |
| 403 | set((state) => syncServerReadModel(state, readModel)), | |
| 385 | syncServerReadModel: (readModel) => set((state) => syncServerReadModel(state, readModel)), | |
| 404 | 386 | markThreadVisited: (threadId, visitedAt) => |
| 405 | 387 | set((state) => markThreadVisited(state, threadId, visitedAt)), |
| 406 | markThreadUnread: (threadId) => | |
| 407 | set((state) => markThreadUnread(state, threadId)), | |
| 408 | toggleProject: (projectId) => | |
| 409 | set((state) => toggleProject(state, projectId)), | |
| 388 | markThreadUnread: (threadId) => set((state) => markThreadUnread(state, threadId)), | |
| 389 | toggleProject: (projectId) => set((state) => toggleProject(state, projectId)), | |
| 410 | 390 | setProjectExpanded: (projectId, expanded) => |
| 411 | 391 | set((state) => setProjectExpanded(state, projectId, expanded)), |
| 412 | setError: (threadId, error) => | |
| 413 | set((state) => setError(state, threadId, error)), | |
| 392 | setError: (threadId, error) => set((state) => setError(state, threadId, error)), | |
| 414 | 393 | setThreadBranch: (threadId, branch, worktreePath) => |
| 415 | 394 | set((state) => setThreadBranch(state, threadId, branch, worktreePath)), |
| 416 | 395 | })); |
packages/contracts/src/model.ts+2-5| ... | ... | @@ -29,18 +29,15 @@ export const MODEL_OPTIONS_BY_PROVIDER = { |
| 29 | 29 | { slug: "gpt-5.2", name: "GPT-5.2" }, |
| 30 | 30 | ], |
| 31 | 31 | } as const satisfies Record<ProviderKind, readonly ModelOption[]>; |
| 32 | export type ModelOptionsByProvider = typeof MODEL_OPTIONS_BY_PROVIDER; | |
| 32 | 33 | |
| 33 | type BuiltInModelSlug = (typeof MODEL_OPTIONS_BY_PROVIDER)[ProviderKind][number]["slug"]; | |
| 34 | type BuiltInModelSlug = ModelOptionsByProvider[ProviderKind][number]["slug"]; | |
| 34 | 35 | export type ModelSlug = BuiltInModelSlug | (string & {}); |
| 35 | 36 | |
| 36 | 37 | export const DEFAULT_MODEL_BY_PROVIDER = { |
| 37 | 38 | codex: "gpt-5.4", |
| 38 | 39 | } as const satisfies Record<ProviderKind, ModelSlug>; |
| 39 | 40 | |
| 40 | // Backward compatibility for existing Codex-only call sites. | |
| 41 | export const MODEL_OPTIONS = MODEL_OPTIONS_BY_PROVIDER.codex; | |
| 42 | export const DEFAULT_MODEL = DEFAULT_MODEL_BY_PROVIDER.codex; | |
| 43 | ||
| 44 | 41 | export const MODEL_SLUG_ALIASES_BY_PROVIDER = { |
| 45 | 42 | codex: { |
| 46 | 43 | "5.4": "gpt-5.4", |
packages/shared/src/model.test.ts+8-8| ... | ... | @@ -1,5 +1,5 @@ |
| 1 | 1 | import { describe, expect, it } from "vitest"; |
| 2 | import { DEFAULT_MODEL, MODEL_OPTIONS } from "@t3tools/contracts"; | |
| 2 | import { DEFAULT_MODEL_BY_PROVIDER, MODEL_OPTIONS_BY_PROVIDER } from "@t3tools/contracts"; | |
| 3 | 3 | |
| 4 | 4 | import { |
| 5 | 5 | getDefaultModel, |
| ... | ... | @@ -31,23 +31,23 @@ describe("normalizeModelSlug", () => { |
| 31 | 31 | |
| 32 | 32 | describe("resolveModelSlug", () => { |
| 33 | 33 | it("returns default only when the model is missing", () => { |
| 34 | expect(resolveModelSlug(undefined)).toBe(DEFAULT_MODEL); | |
| 35 | expect(resolveModelSlug(null)).toBe(DEFAULT_MODEL); | |
| 34 | expect(resolveModelSlug(undefined)).toBe(DEFAULT_MODEL_BY_PROVIDER.codex); | |
| 35 | expect(resolveModelSlug(null)).toBe(DEFAULT_MODEL_BY_PROVIDER.codex); | |
| 36 | 36 | }); |
| 37 | 37 | |
| 38 | 38 | it("preserves unknown custom models", () => { |
| 39 | expect(resolveModelSlug("gpt-4.1")).toBe(DEFAULT_MODEL); | |
| 40 | expect(resolveModelSlug("custom/internal-model")).toBe(DEFAULT_MODEL); | |
| 39 | expect(resolveModelSlug("gpt-4.1")).toBe(DEFAULT_MODEL_BY_PROVIDER.codex); | |
| 40 | expect(resolveModelSlug("custom/internal-model")).toBe(DEFAULT_MODEL_BY_PROVIDER.codex); | |
| 41 | 41 | }); |
| 42 | 42 | |
| 43 | 43 | it("resolves only supported model options", () => { |
| 44 | for (const model of MODEL_OPTIONS) { | |
| 44 | for (const model of MODEL_OPTIONS_BY_PROVIDER.codex) { | |
| 45 | 45 | expect(resolveModelSlug(model.slug)).toBe(model.slug); |
| 46 | 46 | } |
| 47 | 47 | }); |
| 48 | 48 | it("keeps codex defaults for backward compatibility", () => { |
| 49 | expect(getDefaultModel()).toBe(DEFAULT_MODEL); | |
| 50 | expect(getModelOptions()).toEqual(MODEL_OPTIONS); | |
| 49 | expect(getDefaultModel()).toBe(DEFAULT_MODEL_BY_PROVIDER.codex); | |
| 50 | expect(getModelOptions()).toEqual(MODEL_OPTIONS_BY_PROVIDER.codex); | |
| 51 | 51 | }); |
| 52 | 52 | }); |
| 53 | 53 |
packages/shared/src/model.ts+1-2| ... | ... | @@ -1,6 +1,5 @@ |
| 1 | 1 | import { |
| 2 | 2 | CODEX_REASONING_EFFORT_OPTIONS, |
| 3 | DEFAULT_MODEL, | |
| 4 | 3 | DEFAULT_MODEL_BY_PROVIDER, |
| 5 | 4 | MODEL_OPTIONS_BY_PROVIDER, |
| 6 | 5 | MODEL_SLUG_ALIASES_BY_PROVIDER, |
| ... | ... | @@ -20,7 +19,7 @@ export function getModelOptions(provider: ProviderKind = "codex") { |
| 20 | 19 | } |
| 21 | 20 | |
| 22 | 21 | export function getDefaultModel(provider: ProviderKind = "codex"): ModelSlug { |
| 23 | return DEFAULT_MODEL_BY_PROVIDER[provider] ?? DEFAULT_MODEL; | |
| 22 | return DEFAULT_MODEL_BY_PROVIDER[provider]; | |
| 24 | 23 | } |
| 25 | 24 | |
| 26 | 25 | export function normalizeModelSlug( |