authorgravatar for mfazekas@szemafor.comMiklós Fazekas <mfazekas@szemafor.com> 2026-07-22 15:58:27+02:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-07-22 15:58:27+02:00
logb6e1b39335ffd34583c94f5b1e5e2c80520729fe
tree1ccbee9c94905fc17de762a7edb7427d38f22a92
parentc7b21ff172f8e64318ec681e8839ee60ca660fa4
signature Signed by PGP key B5690EEEBB952194

fix(web): default provider selection for users without Codex (#4117)

Co-authored-by: Julius Marminge <julius0216@outlook.com> Co-authored-by: codex <codex@users.noreply.github.com>

7 files changed, 458 insertions(+), 97 deletions(-)

apps/web/src/components/ChatView.tsx+6-8
...@@ -152,6 +152,7 @@ import {...@@ -152,6 +152,7 @@ import {
152} from "~/projectScripts";152} from "~/projectScripts";
153import { newDraftId, newMessageId, newThreadId } from "~/lib/utils";153import { newDraftId, newMessageId, newThreadId } from "~/lib/utils";
154import { getProviderModelCapabilities, resolveSelectableProvider } from "../providerModels";154import { getProviderModelCapabilities, resolveSelectableProvider } from "../providerModels";
155import { NO_PROVIDER_MODEL_SELECTION } from "../providerInstances";
155import { useEnvironmentSettings } from "../hooks/useSettings";156import { useEnvironmentSettings } from "../hooks/useSettings";
156import { resolveAppModelSelectionForInstance } from "../modelSelection";157import { resolveAppModelSelectionForInstance } from "../modelSelection";
157import { getTerminalFocusOwner } from "../lib/terminalFocus";158import { getTerminalFocusOwner } from "../lib/terminalFocus";
...@@ -1361,10 +1362,7 @@ function ChatViewContent(props: ChatViewProps) {...@@ -1361,10 +1362,7 @@ function ChatViewContent(props: ChatViewProps) {
1361 ? buildLocalDraftThread(1362 ? buildLocalDraftThread(
1362 threadId,1363 threadId,
1363 draftThread,1364 draftThread,
1364 fallbackDraftProject?.defaultModelSelection ?? {1365 fallbackDraftProject?.defaultModelSelection ?? NO_PROVIDER_MODEL_SELECTION,
1365 instanceId: ProviderInstanceId.make("codex"),
1366 model: DEFAULT_MODEL,
1367 },
1368 )1366 )
1369 : undefined,1367 : undefined,
1370 [draftThread, fallbackDraftProject?.defaultModelSelection, threadId],1368 [draftThread, fallbackDraftProject?.defaultModelSelection, threadId],
...@@ -1856,7 +1854,7 @@ function ChatViewContent(props: ChatViewProps) {...@@ -1856,7 +1854,7 @@ function ChatViewContent(props: ChatViewProps) {
1856 const providerStatuses = serverConfig?.providers ?? EMPTY_PROVIDERS;1854 const providerStatuses = serverConfig?.providers ?? EMPTY_PROVIDERS;
1857 const unlockedSelectedProvider = resolveSelectableProvider(1855 const unlockedSelectedProvider = resolveSelectableProvider(
1858 providerStatuses,1856 providerStatuses,
1859 selectedProviderByThreadId ?? threadProvider ?? ProviderDriverKind.make("codex"),1857 selectedProviderByThreadId ?? threadProvider,
1860 );1858 );
1861 const selectedProvider: ProviderDriverKind = lockedProvider ?? unlockedSelectedProvider;1859 const selectedProvider: ProviderDriverKind = lockedProvider ?? unlockedSelectedProvider;
1862 const phase = derivePhase(activeThread?.session ?? null);1860 const phase = derivePhase(activeThread?.session ?? null);
...@@ -4038,7 +4036,7 @@ function ChatViewContent(props: ChatViewProps) {...@@ -4038,7 +4036,7 @@ function ChatViewContent(props: ChatViewProps) {
4038 return;4036 return;
4039 }4037 }
4040 const sendCtx = composerRef.current?.getSendContext();4038 const sendCtx = composerRef.current?.getSendContext();
4041 if (!sendCtx) return;4039 if (!sendCtx?.providerAvailable) return;
4042 const {4040 const {
4043 images: composerImages,4041 images: composerImages,
4044 terminalContexts: composerTerminalContexts,4042 terminalContexts: composerTerminalContexts,
...@@ -4614,7 +4612,7 @@ function ChatViewContent(props: ChatViewProps) {...@@ -4614,7 +4612,7 @@ function ChatViewContent(props: ChatViewProps) {
4614 }4612 }
46154613
4616 const sendCtx = composerRef.current?.getSendContext();4614 const sendCtx = composerRef.current?.getSendContext();
4617 if (!sendCtx) {4615 if (!sendCtx?.providerAvailable) {
4618 return;4616 return;
4619 }4617 }
4620 const {4618 const {
...@@ -4773,7 +4771,7 @@ function ChatViewContent(props: ChatViewProps) {...@@ -4773,7 +4771,7 @@ function ChatViewContent(props: ChatViewProps) {
4773 }4771 }
47744772
4775 const sendCtx = composerRef.current?.getSendContext();4773 const sendCtx = composerRef.current?.getSendContext();
4776 if (!sendCtx) {4774 if (!sendCtx?.providerAvailable) {
4777 return;4775 return;
4778 }4776 }
4779 const {4777 const {
apps/web/src/components/CommandPalette.tsx+14-7
...@@ -7,12 +7,10 @@ import {...@@ -7,12 +7,10 @@ import {
7 squashAtomCommandFailure,7 squashAtomCommandFailure,
8} from "@t3tools/client-runtime/state/runtime";8} from "@t3tools/client-runtime/state/runtime";
9import {9import {
10 DEFAULT_MODEL,
11 type DesktopWslState,10 type DesktopWslState,
12 type EnvironmentId,11 type EnvironmentId,
13 type FilesystemBrowseResult,12 type FilesystemBrowseResult,
14 type ProjectId,13 type ProjectId,
15 ProviderInstanceId,
16 type SourceControlDiscoveryResult,14 type SourceControlDiscoveryResult,
17 type SourceControlProviderKind,15 type SourceControlProviderKind,
18 type SourceControlRepositoryInfo,16 type SourceControlRepositoryInfo,
...@@ -111,7 +109,8 @@ import { CommandPaletteResults } from "./CommandPaletteResults";...@@ -111,7 +109,8 @@ import { CommandPaletteResults } from "./CommandPaletteResults";
111import { AzureDevOpsIcon, BitbucketIcon, GitHubIcon, GitLabIcon } from "./Icons";109import { AzureDevOpsIcon, BitbucketIcon, GitHubIcon, GitLabIcon } from "./Icons";
112import { ProjectFavicon } from "./ProjectFavicon";110import { ProjectFavicon } from "./ProjectFavicon";
113import { ThreadRowLeadingStatus, ThreadRowTrailingStatus } from "./ThreadStatusIndicators";111import { ThreadRowLeadingStatus, ThreadRowTrailingStatus } from "./ThreadStatusIndicators";
114import { primaryServerKeybindingsAtom } from "../state/server";112import { primaryServerKeybindingsAtom, primaryServerProvidersAtom } from "../state/server";
113import { resolveDefaultProviderModelSelection } from "../providerInstances";
115import { resolveShortcutCommand } from "../keybindings";114import { resolveShortcutCommand } from "../keybindings";
116import {115import {
117 Command,116 Command,
...@@ -476,6 +475,7 @@ function OpenCommandPaletteDialog(props: {...@@ -476,6 +475,7 @@ function OpenCommandPaletteDialog(props: {
476 const projects = useProjects();475 const projects = useProjects();
477 const threads = useThreadShells();476 const threads = useThreadShells();
478 const keybindings = useAtomValue(primaryServerKeybindingsAtom);477 const keybindings = useAtomValue(primaryServerKeybindingsAtom);
478 const providers = useAtomValue(primaryServerProvidersAtom);
479 const [viewStack, setViewStack] = useState<CommandPaletteView[]>([]);479 const [viewStack, setViewStack] = useState<CommandPaletteView[]>([]);
480 const currentView = viewStack.at(-1) ?? null;480 const currentView = viewStack.at(-1) ?? null;
481 const [browseGeneration, setBrowseGeneration] = useState(0);481 const [browseGeneration, setBrowseGeneration] = useState(0);
...@@ -1149,6 +1149,10 @@ function OpenCommandPaletteDialog(props: {...@@ -1149,6 +1149,10 @@ function OpenCommandPaletteDialog(props: {
1149 }1149 }
11501150
1151 const projectId = newProjectId();1151 const projectId = newProjectId();
1152 const targetEnvironmentProviders =
1153 environments.find((environment) => environment.environmentId === input.environmentId)
1154 ?.serverConfig?.providers ??
1155 (input.environmentId === primaryEnvironmentId ? providers : []);
1152 const createResult = await createProject({1156 const createResult = await createProject({
1153 environmentId: input.environmentId,1157 environmentId: input.environmentId,
1154 input: {1158 input: {
...@@ -1156,10 +1160,10 @@ function OpenCommandPaletteDialog(props: {...@@ -1156,10 +1160,10 @@ function OpenCommandPaletteDialog(props: {
1156 title: inferProjectTitleFromPath(cwd),1160 title: inferProjectTitleFromPath(cwd),
1157 workspaceRoot: cwd,1161 workspaceRoot: cwd,
1158 createWorkspaceRootIfMissing: true,1162 createWorkspaceRootIfMissing: true,
1159 defaultModelSelection: {1163 defaultModelSelection: resolveDefaultProviderModelSelection(
1160 instanceId: ProviderInstanceId.make("codex"),1164 targetEnvironmentProviders,
1161 model: DEFAULT_MODEL,1165 null,
1162 },1166 ),
1163 },1167 },
1164 });1168 });
1165 if (createResult._tag === "Failure") {1169 if (createResult._tag === "Failure") {
...@@ -1195,8 +1199,11 @@ function OpenCommandPaletteDialog(props: {...@@ -1195,8 +1199,11 @@ function OpenCommandPaletteDialog(props: {
1195 [1199 [
1196 handleNewThread,1200 handleNewThread,
1197 createProject,1201 createProject,
1202 environments,
1198 navigate,1203 navigate,
1204 primaryEnvironmentId,
1199 projects,1205 projects,
1206 providers,
1200 setOpen,1207 setOpen,
1201 clientSettings.sidebarThreadSortOrder,1208 clientSettings.sidebarThreadSortOrder,
1202 threads,1209 threads,
apps/web/src/components/chat/ChatComposer.tsx+100-57
...@@ -114,7 +114,9 @@ import { getProviderDisplayName, getProviderInteractionModeToggle } from "../../...@@ -114,7 +114,9 @@ import { getProviderDisplayName, getProviderInteractionModeToggle } from "../../
114import {114import {
115 applyProviderInstanceSettings,115 applyProviderInstanceSettings,
116 deriveProviderInstanceEntries,116 deriveProviderInstanceEntries,
117 NO_PROVIDER_MODEL_SELECTION,
117 resolveProviderDriverKindForInstanceSelection,118 resolveProviderDriverKindForInstanceSelection,
119 resolveSelectableProviderInstanceEntry,
118 sortProviderInstanceEntries,120 sortProviderInstanceEntries,
119 type ProviderInstanceEntry,121 type ProviderInstanceEntry,
120} from "../../providerInstances";122} from "../../providerInstances";
...@@ -433,6 +435,7 @@ export interface ChatComposerHandle {...@@ -433,6 +435,7 @@ export interface ChatComposerHandle {
433 selectedPromptEffort: string | null;435 selectedPromptEffort: string | null;
434 selectedModelOptionsForDispatch: unknown;436 selectedModelOptionsForDispatch: unknown;
435 selectedModelSelection: ModelSelection;437 selectedModelSelection: ModelSelection;
438 providerAvailable: boolean;
436 selectedProvider: ProviderDriverKind;439 selectedProvider: ProviderDriverKind;
437 selectedModel: string;440 selectedModel: string;
438 selectedProviderModels: ReadonlyArray<ServerProvider["models"][number]>;441 selectedProviderModels: ReadonlyArray<ServerProvider["models"][number]>;
...@@ -696,8 +699,10 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps)...@@ -696,8 +699,10 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps)
696 providerInstanceEntries,699 providerInstanceEntries,
697 providerStatuses,700 providerStatuses,
698 explicitSelectedInstanceId,701 explicitSelectedInstanceId,
699 ) ?? ProviderDriverKind.make("codex");702 ) ??
700 const selectedProvider: ProviderDriverKind = lockedProvider ?? unlockedSelectedProvider;703 providerInstanceEntries[0]?.driverKind ??
704 ProviderDriverKind.make("unconfigured");
705 const requestedDriverKind: ProviderDriverKind = lockedProvider ?? unlockedSelectedProvider;
701 const lockedContinuationGroupKey = useMemo((): string | null => {706 const lockedContinuationGroupKey = useMemo((): string | null => {
702 if (!lockedProvider || !activeThread) return null;707 if (!lockedProvider || !activeThread) return null;
703 const lockedInstanceId =708 const lockedInstanceId =
...@@ -734,7 +739,7 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps)...@@ -734,7 +739,7 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps)
734 for (const candidate of candidates) {739 for (const candidate of candidates) {
735 if (!candidate) continue;740 if (!candidate) continue;
736 const match = providerInstanceEntries.find(741 const match = providerInstanceEntries.find(
737 (entry) => entry.instanceId === candidate && entry.enabled,742 (entry) => entry.instanceId === candidate && entry.enabled && entry.isAvailable,
738 );743 );
739 if (match) {744 if (match) {
740 // When locked to a specific driver kind, ignore persisted instance745 // When locked to a specific driver kind, ignore persisted instance
...@@ -749,36 +754,44 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps)...@@ -749,36 +754,44 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps)
749 return match.instanceId;754 return match.instanceId;
750 }755 }
751 }756 }
752 if (explicitSelectedInstanceId) {757 const compatibleEntries = providerInstanceEntries.filter(
753 return ProviderInstanceId.make(explicitSelectedInstanceId);
754 }
755 const byKind = providerInstanceEntries.find(
756 (entry) =>758 (entry) =>
757 entry.enabled &&759 (!lockedProvider || entry.driverKind === lockedProvider) &&
758 entry.driverKind === selectedProvider &&
759 (!lockedContinuationGroupKey || entry.continuationGroupKey === lockedContinuationGroupKey),760 (!lockedContinuationGroupKey || entry.continuationGroupKey === lockedContinuationGroupKey),
760 );761 );
761 if (byKind) return byKind.instanceId;762 const requestedDriverEntries = compatibleEntries.filter(
762 const anyEnabled = providerInstanceEntries.find((entry) => entry.enabled);763 (entry) => entry.driverKind === requestedDriverKind,
764 );
763 return (765 return (
764 anyEnabled?.instanceId ??766 resolveSelectableProviderInstanceEntry(requestedDriverEntries, undefined)?.instanceId ??
765 providerInstanceEntries[0]?.instanceId ??767 resolveSelectableProviderInstanceEntry(compatibleEntries, undefined)?.instanceId ??
766 activeThreadModelSelection?.instanceId ??768 NO_PROVIDER_MODEL_SELECTION.instanceId
767 activeProjectDefaultModelSelection?.instanceId ??
768 ProviderInstanceId.make("codex")
769 );769 );
770 }, [770 }, [
771 activeProjectDefaultModelSelection?.instanceId,771 activeProjectDefaultModelSelection?.instanceId,
772 activeThread?.session?.providerInstanceId,772 activeThread?.session?.providerInstanceId,
773 activeThreadModelSelection?.instanceId,773 activeThreadModelSelection?.instanceId,
774 composerDraft.activeProvider,774 composerDraft.activeProvider,
775 explicitSelectedInstanceId,
776 lockedContinuationGroupKey,775 lockedContinuationGroupKey,
777 lockedProvider,776 lockedProvider,
778 providerInstanceEntries,777 providerInstanceEntries,
779 selectedProvider,778 requestedDriverKind,
780 ]);779 ]);
781780
781 // Resolve the active instance's snapshot by `instanceId` so a custom
782 // instance gets its own slash commands, skills, and model list — not
783 // the first snapshot for the same driver kind.
784 const selectedProviderEntry = useMemo(
785 () => providerInstanceEntries.find((entry) => entry.instanceId === selectedInstanceId),
786 [providerInstanceEntries, selectedInstanceId],
787 );
788 const noProviderAvailable = selectedProviderEntry === undefined;
789 // The driver kind follows the instance that will actually run the turn,
790 // which can differ from the persisted selection when that selection is
791 // disabled.
792 const selectedProvider: ProviderDriverKind =
793 selectedProviderEntry?.driverKind ?? requestedDriverKind;
794
782 const { modelOptions: composerModelOptions, selectedModel } = useEffectiveComposerModelState({795 const { modelOptions: composerModelOptions, selectedModel } = useEffectiveComposerModelState({
783 threadRef: composerDraftTarget,796 threadRef: composerDraftTarget,
784 providers: providerStatuses,797 providers: providerStatuses,
...@@ -788,14 +801,6 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps)...@@ -788,14 +801,6 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps)
788 projectModelSelection: activeProjectDefaultModelSelection,801 projectModelSelection: activeProjectDefaultModelSelection,
789 settings,802 settings,
790 });803 });
791
792 // Resolve the active instance's snapshot by `instanceId` so a custom
793 // instance gets its own slash commands, skills, and model list — not
794 // the first snapshot for the same driver kind.
795 const selectedProviderEntry = useMemo(
796 () => providerInstanceEntries.find((entry) => entry.instanceId === selectedInstanceId),
797 [providerInstanceEntries, selectedInstanceId],
798 );
799 const selectedProviderStatus = useMemo(804 const selectedProviderStatus = useMemo(
800 () => selectedProviderEntry?.snapshot ?? null,805 () => selectedProviderEntry?.snapshot ?? null,
801 [selectedProviderEntry],806 [selectedProviderEntry],
...@@ -1158,6 +1163,7 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps)...@@ -1158,6 +1163,7 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps)
1158 phase === "running" ||1163 phase === "running" ||
1159 isSendBusy ||1164 isSendBusy ||
1160 isConnecting ||1165 isConnecting ||
1166 noProviderAvailable ||
1161 projectSelectionRequired ||1167 projectSelectionRequired ||
1162 environmentUnavailable !== null ||1168 environmentUnavailable !== null ||
1163 !composerSendState.hasSendableContent;1169 !composerSendState.hasSendableContent;
...@@ -1698,7 +1704,13 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps)...@@ -1698,7 +1704,13 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps)
16981704
1699 const shouldBlurMobileComposerOnSubmit = useCallback(() => {1705 const shouldBlurMobileComposerOnSubmit = useCallback(() => {
1700 if (!isMobileViewport) return false;1706 if (!isMobileViewport) return false;
1701 if (isSendBusy || isConnecting || environmentUnavailable !== null || phase === "running") {1707 if (
1708 isSendBusy ||
1709 isConnecting ||
1710 noProviderAvailable ||
1711 environmentUnavailable !== null ||
1712 phase === "running"
1713 ) {
1702 return false;1714 return false;
1703 }1715 }
1704 if (activePendingProgress) {1716 if (activePendingProgress) {
...@@ -1713,18 +1725,23 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps)...@@ -1713,18 +1725,23 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps)
1713 isConnecting,1725 isConnecting,
1714 isMobileViewport,1726 isMobileViewport,
1715 isSendBusy,1727 isSendBusy,
1728 noProviderAvailable,
1716 phase,1729 phase,
1717 showPlanFollowUpPrompt,1730 showPlanFollowUpPrompt,
1718 ]);1731 ]);
17191732
1720 const submitComposer = useCallback(1733 const submitComposer = useCallback(
1721 (event?: { preventDefault: () => void }) => {1734 (event?: { preventDefault: () => void }) => {
1735 if (noProviderAvailable) {
1736 event?.preventDefault();
1737 return;
1738 }
1722 onSend(event);1739 onSend(event);
1723 if (shouldBlurMobileComposerOnSubmit()) {1740 if (shouldBlurMobileComposerOnSubmit()) {
1724 blurMobileComposerAfterSend();1741 blurMobileComposerAfterSend();
1725 }1742 }
1726 },1743 },
1727 [blurMobileComposerAfterSend, onSend, shouldBlurMobileComposerOnSubmit],1744 [blurMobileComposerAfterSend, noProviderAvailable, onSend, shouldBlurMobileComposerOnSubmit],
1728 );1745 );
1729 const expandMobileComposer = useCallback(() => {1746 const expandMobileComposer = useCallback(() => {
1730 if (composerBlurFrameRef.current !== null) {1747 if (composerBlurFrameRef.current !== null) {
...@@ -2083,6 +2100,7 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps)...@@ -2083,6 +2100,7 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps)
2083 selectedPromptEffort,2100 selectedPromptEffort,
2084 selectedModelOptionsForDispatch,2101 selectedModelOptionsForDispatch,
2085 selectedModelSelection,2102 selectedModelSelection,
2103 providerAvailable: !noProviderAvailable,
2086 selectedProvider,2104 selectedProvider,
2087 selectedModel,2105 selectedModel,
2088 selectedProviderModels,2106 selectedProviderModels,
...@@ -2110,6 +2128,7 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps)...@@ -2110,6 +2128,7 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps)
2110 selectedModel,2128 selectedModel,
2111 selectedModelOptionsForDispatch,2129 selectedModelOptionsForDispatch,
2112 selectedModelSelection,2130 selectedModelSelection,
2131 noProviderAvailable,
2113 selectedPromptEffort,2132 selectedPromptEffort,
2114 selectedProvider,2133 selectedProvider,
2115 selectedProviderModels,2134 selectedProviderModels,
...@@ -2260,7 +2279,9 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps)...@@ -2260,7 +2279,9 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps)
2260 isSendBusy={isSendBusy}2279 isSendBusy={isSendBusy}
2261 isConnecting={isConnecting}2280 isConnecting={isConnecting}
2262 isEnvironmentUnavailable={2281 isEnvironmentUnavailable={
2263 environmentUnavailable !== null || projectSelectionRequired2282 environmentUnavailable !== null ||
2283 noProviderAvailable ||
2284 projectSelectionRequired
2264 }2285 }
2265 isPreparingWorktree={false}2286 isPreparingWorktree={false}
2266 hasSendableContent={false}2287 hasSendableContent={false}
...@@ -2292,7 +2313,8 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps)...@@ -2292,7 +2313,8 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps)
2292 {activePendingProgress2313 {activePendingProgress
2293 ? activePendingProgress.customAnswer ||2314 ? activePendingProgress.customAnswer ||
2294 "Type your own answer, or leave this blank to use the selected option"2315 "Type your own answer, or leave this blank to use the selected option"
2295 : prompt.trim() || "Ask anything..."}2316 : prompt.trim() ||
2317 (noProviderAvailable ? "Enable a provider in Settings" : "Ask anything...")}
2296 </button>2318 </button>
2297 <button2319 <button
2298 type="button"2320 type="button"
...@@ -2501,9 +2523,11 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps)...@@ -2501,9 +2523,11 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps)
2501 ? `${environmentUnavailable.label}: ${connectionStatusText(2523 ? `${environmentUnavailable.label}: ${connectionStatusText(
2502 environmentUnavailable.connection,2524 environmentUnavailable.connection,
2503 )}`2525 )}`
2504 : phase === "disconnected"2526 : noProviderAvailable
2505 ? "Ask for follow-up changes or attach images"2527 ? "Enable a provider in Settings to send a message"
2506 : "Ask anything, @tag files/folders, $use skills, or / for commands"2528 : phase === "disconnected"
2529 ? "Ask for follow-up changes or attach images"
2530 : "Ask anything, @tag files/folders, $use skills, or / for commands"
2507 }2531 }
2508 disabled={isConnecting || isComposerApprovalState || projectSelectionRequired}2532 disabled={isConnecting || isComposerApprovalState || projectSelectionRequired}
2509 />2533 />
...@@ -2521,7 +2545,9 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps)...@@ -2521,7 +2545,9 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps)
2521 isSendBusy={isSendBusy}2545 isSendBusy={isSendBusy}
2522 isConnecting={isConnecting}2546 isConnecting={isConnecting}
2523 isEnvironmentUnavailable={2547 isEnvironmentUnavailable={
2524 environmentUnavailable !== null || projectSelectionRequired2548 environmentUnavailable !== null ||
2549 noProviderAvailable ||
2550 projectSelectionRequired
2525 }2551 }
2526 isPreparingWorktree={false}2552 isPreparingWorktree={false}
2527 hasSendableContent={false}2553 hasSendableContent={false}
...@@ -2556,28 +2582,43 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps)...@@ -2556,28 +2582,43 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps)
2556 )}2582 )}
2557 >2583 >
2558 <div className="-m-1 flex min-w-0 flex-1 items-center gap-1 overflow-x-auto p-1 [scrollbar-width:none] [&::-webkit-scrollbar]:hidden">2584 <div className="-m-1 flex min-w-0 flex-1 items-center gap-1 overflow-x-auto p-1 [scrollbar-width:none] [&::-webkit-scrollbar]:hidden">
2559 <ProviderModelPicker2585 {noProviderAvailable ? (
2560 compact={isComposerFooterCompact}2586 <Button
2561 activeInstanceId={selectedInstanceId}2587 type="button"
2562 model={selectedModelForPickerWithCustomFallback}2588 size="sm"
2563 lockedProvider={lockedProvider}2589 variant="ghost"
2564 lockedContinuationGroupKey={lockedContinuationGroupKey}2590 disabled
2565 instanceEntries={providerInstanceEntries}2591 data-chat-provider-unavailable="true"
2566 keybindings={keybindings}2592 className="shrink-0 gap-2 px-2 text-muted-foreground/70 sm:px-3"
2567 modelOptionsByInstance={modelOptionsByInstance}2593 >
2568 terminalOpen={terminalOpen}2594 <CircleAlertIcon className="size-4" />
2569 open={isComposerModelPickerOpen}2595 No provider available
2570 {...(composerProviderState.modelPickerIconClassName2596 </Button>
2571 ? {2597 ) : (
2572 activeProviderIconClassName: composerProviderState.modelPickerIconClassName,2598 <ProviderModelPicker
2573 }2599 compact={isComposerFooterCompact}
2574 : {})}2600 activeInstanceId={selectedInstanceId}
2575 onOpenChange={(open) => {2601 model={selectedModelForPickerWithCustomFallback}
2576 setIsComposerModelPickerOpen(open);2602 lockedProvider={lockedProvider}
2577 }}2603 lockedContinuationGroupKey={lockedContinuationGroupKey}
2578 getModelDisabledReason={getModelDisabledReason}2604 instanceEntries={providerInstanceEntries}
2579 onInstanceModelChange={onProviderModelSelect}2605 keybindings={keybindings}
2580 />2606 modelOptionsByInstance={modelOptionsByInstance}
2607 terminalOpen={terminalOpen}
2608 open={isComposerModelPickerOpen}
2609 {...(composerProviderState.modelPickerIconClassName
2610 ? {
2611 activeProviderIconClassName:
2612 composerProviderState.modelPickerIconClassName,
2613 }
2614 : {})}
2615 onOpenChange={(open) => {
2616 setIsComposerModelPickerOpen(open);
2617 }}
2618 getModelDisabledReason={getModelDisabledReason}
2619 onInstanceModelChange={onProviderModelSelect}
2620 />
2621 )}
25812622
2582 {isComposerFooterCompact ? (2623 {isComposerFooterCompact ? (
2583 <CompactComposerControlsMenu2624 <CompactComposerControlsMenu
...@@ -2634,7 +2675,9 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps)...@@ -2634,7 +2675,9 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps)
2634 isSendBusy={isSendBusy}2675 isSendBusy={isSendBusy}
2635 isConnecting={isConnecting}2676 isConnecting={isConnecting}
2636 isEnvironmentUnavailable={2677 isEnvironmentUnavailable={
2637 environmentUnavailable !== null || projectSelectionRequired2678 environmentUnavailable !== null ||
2679 noProviderAvailable ||
2680 projectSelectionRequired
2638 }2681 }
2639 isPreparingWorktree={isPreparingWorktree}2682 isPreparingWorktree={isPreparingWorktree}
2640 hasSendableContent={composerSendState.hasSendableContent}2683 hasSendableContent={composerSendState.hasSendableContent}
apps/web/src/providerInstances.test.ts+256-2
...@@ -3,8 +3,10 @@ import { describe, expect, it } from "vite-plus/test";...@@ -3,8 +3,10 @@ import { describe, expect, it } from "vite-plus/test";
3import {3import {
4 applyProviderInstanceSettings,4 applyProviderInstanceSettings,
5 deriveProviderInstanceEntries,5 deriveProviderInstanceEntries,
6 getDefaultProviderInstanceModel,
6 isProviderInstancePickerReady,7 isProviderInstancePickerReady,
7 isProviderInstancePickerVisible,8 isProviderInstancePickerVisible,
9 resolveDefaultProviderModelSelection,
8 resolveSelectableProviderInstance,10 resolveSelectableProviderInstance,
9 resolveProviderDriverKindForInstanceSelection,11 resolveProviderDriverKindForInstanceSelection,
10} from "./providerInstances";12} from "./providerInstances";
...@@ -15,6 +17,8 @@ function provider(input: {...@@ -15,6 +17,8 @@ function provider(input: {
15 enabled?: boolean;17 enabled?: boolean;
16 availability?: ServerProvider["availability"];18 availability?: ServerProvider["availability"];
17 displayName?: string;19 displayName?: string;
20 status?: ServerProvider["status"];
21 models?: ServerProvider["models"];
18}): ServerProvider {22}): ServerProvider {
19 return {23 return {
20 instanceId: ProviderInstanceId.make(input.instanceId),24 instanceId: ProviderInstanceId.make(input.instanceId),
...@@ -23,16 +27,24 @@ function provider(input: {...@@ -23,16 +27,24 @@ function provider(input: {
23 enabled: input.enabled ?? true,27 enabled: input.enabled ?? true,
24 installed: true,28 installed: true,
25 version: null,29 version: null,
26 status: "ready",30 status: input.status ?? "ready",
27 ...(input.availability ? { availability: input.availability } : {}),31 ...(input.availability ? { availability: input.availability } : {}),
28 auth: { status: "authenticated" },32 auth: { status: "authenticated" },
29 checkedAt: "2026-01-01T00:00:00.000Z",33 checkedAt: "2026-01-01T00:00:00.000Z",
30 models: [],34 models: input.models ?? [],
31 slashCommands: [],35 slashCommands: [],
32 skills: [],36 skills: [],
33 };37 };
34}38}
3539
40const model = (slug: string, isCustom = false, isDefault = false) => ({
41 slug,
42 name: slug,
43 isCustom,
44 ...(isDefault ? { isDefault: true } : {}),
45 capabilities: {},
46});
47
36describe("isProviderInstancePickerReady", () => {48describe("isProviderInstancePickerReady", () => {
37 it("rejects a disabled instance even while its last probe status is ready", () => {49 it("rejects a disabled instance even while its last probe status is ready", () => {
38 const [entry] = deriveProviderInstanceEntries([50 const [entry] = deriveProviderInstanceEntries([
...@@ -146,6 +158,67 @@ describe("resolveSelectableProviderInstance", () => {...@@ -146,6 +158,67 @@ describe("resolveSelectableProviderInstance", () => {
146 expect(resolveSelectableProviderInstance(providers, disabled)).toBe(fallback);158 expect(resolveSelectableProviderInstance(providers, disabled)).toBe(fallback);
147 });159 });
148160
161 it("prefers a ready instance over an enabled one whose driver cannot start", () => {
162 const notInstalled = ProviderInstanceId.make("codex");
163 const ready = ProviderInstanceId.make("claudeAgent");
164 const providers = [
165 provider({
166 provider: ProviderDriverKind.make("codex"),
167 instanceId: notInstalled,
168 status: "error",
169 }),
170 provider({ provider: ProviderDriverKind.make("claudeAgent"), instanceId: ready }),
171 ];
172
173 expect(resolveSelectableProviderInstance(providers, undefined)).toBe(ready);
174 });
175
176 it("prefers an unprobed (warning) instance over one whose probe errored", () => {
177 const notInstalled = ProviderInstanceId.make("codex");
178 const unprobed = ProviderInstanceId.make("claudeAgent");
179 const providers = [
180 provider({
181 provider: ProviderDriverKind.make("codex"),
182 instanceId: notInstalled,
183 status: "error",
184 }),
185 provider({
186 provider: ProviderDriverKind.make("claudeAgent"),
187 instanceId: unprobed,
188 status: "warning",
189 }),
190 ];
191
192 expect(resolveSelectableProviderInstance(providers, undefined)).toBe(unprobed);
193 });
194
195 it("keeps a requested instance even when its probe errored", () => {
196 const requested = ProviderInstanceId.make("codex");
197 const providers = [
198 provider({
199 provider: ProviderDriverKind.make("codex"),
200 instanceId: requested,
201 status: "error",
202 }),
203 provider({ provider: ProviderDriverKind.make("claudeAgent"), instanceId: "claudeAgent" }),
204 ];
205
206 expect(resolveSelectableProviderInstance(providers, requested)).toBe(requested);
207 });
208
209 it("does not invent an errored instance as a new-user default", () => {
210 const notInstalled = ProviderInstanceId.make("codex");
211 const providers = [
212 provider({
213 provider: ProviderDriverKind.make("codex"),
214 instanceId: notInstalled,
215 status: "error",
216 }),
217 ];
218
219 expect(resolveSelectableProviderInstance(providers, undefined)).toBeUndefined();
220 });
221
149 it("does not return disabled, unavailable, or unknown instances when none are sendable", () => {222 it("does not return disabled, unavailable, or unknown instances when none are sendable", () => {
150 const disabled = ProviderInstanceId.make("codex");223 const disabled = ProviderInstanceId.make("codex");
151 const unavailable = ProviderInstanceId.make("claudeAgent");224 const unavailable = ProviderInstanceId.make("claudeAgent");
...@@ -206,3 +279,184 @@ describe("resolveProviderDriverKindForInstanceSelection", () => {...@@ -206,3 +279,184 @@ describe("resolveProviderDriverKindForInstanceSelection", () => {
206 ).toBeUndefined();279 ).toBeUndefined();
207 });280 });
208});281});
282
283describe("getDefaultProviderInstanceModel", () => {
284 it("uses the instance's own models, not the default instance of the kind", () => {
285 const providers = [
286 provider({
287 provider: ProviderDriverKind.make("claudeAgent"),
288 instanceId: "claude_openrouter",
289 models: [model("openai/gpt-5.5", true), model("claude-opus-4-8")],
290 }),
291 provider({
292 provider: ProviderDriverKind.make("claudeAgent"),
293 instanceId: "claudeAgent",
294 models: [model("claude-sonnet-5")],
295 }),
296 ];
297
298 expect(
299 getDefaultProviderInstanceModel(providers, ProviderInstanceId.make("claude_openrouter")),
300 ).toBe("claude-opus-4-8");
301 });
302
303 it("falls back to the driver default when the instance reports no models", () => {
304 const providers = [
305 provider({ provider: ProviderDriverKind.make("claudeAgent"), instanceId: "claudeAgent" }),
306 ];
307
308 const resolved = getDefaultProviderInstanceModel(
309 providers,
310 ProviderInstanceId.make("claudeAgent"),
311 );
312 expect(typeof resolved).toBe("string");
313 expect(resolved?.length).toBeGreaterThan(0);
314 });
315
316 it("honors the instance's declared default before model-list order", () => {
317 const providers = [
318 provider({
319 provider: ProviderDriverKind.make("claudeAgent"),
320 instanceId: "claudeAgent",
321 models: [model("claude-sonnet-5"), model("claude-opus-4-8", false, true)],
322 }),
323 ];
324
325 expect(getDefaultProviderInstanceModel(providers, ProviderInstanceId.make("claudeAgent"))).toBe(
326 "claude-opus-4-8",
327 );
328 });
329
330 it("returns undefined for an unknown instance", () => {
331 expect(
332 getDefaultProviderInstanceModel([], ProviderInstanceId.make("removed_instance")),
333 ).toBeUndefined();
334 });
335});
336
337describe("resolveDefaultProviderModelSelection", () => {
338 it.each([
339 ["codex", "codex", "gpt-5.6"],
340 ["claudeAgent", "claudeAgent", "claude-fable-5"],
341 ["cursor", "cursor", "composer-2"],
342 ])("uses the only available %s instance", (driver, instanceId, modelSlug) => {
343 const providers = [
344 provider({
345 provider: ProviderDriverKind.make(driver),
346 instanceId,
347 models: [model(modelSlug, false, true)],
348 }),
349 ];
350
351 expect(resolveDefaultProviderModelSelection(providers, null)).toEqual({
352 instanceId,
353 model: modelSlug,
354 });
355 });
356
357 it("preserves a valid stored selection including its options", () => {
358 const providers = [
359 provider({
360 provider: ProviderDriverKind.make("claudeAgent"),
361 instanceId: "claudeAgent",
362 models: [model("claude-opus-4-8")],
363 }),
364 ];
365 const stored = {
366 instanceId: ProviderInstanceId.make("claudeAgent"),
367 model: "custom-model",
368 options: [{ id: "effort", value: "high" }],
369 };
370
371 expect(resolveDefaultProviderModelSelection(providers, stored)).toBe(stored);
372 });
373
374 it("replaces a stale stored instance with the first ready instance and its model", () => {
375 const providers = [
376 provider({
377 provider: ProviderDriverKind.make("codex"),
378 instanceId: "codex",
379 status: "warning",
380 models: [model("gpt-5.6")],
381 }),
382 provider({
383 provider: ProviderDriverKind.make("claudeAgent"),
384 instanceId: "claudeAgent",
385 models: [model("claude-opus-4-8", false, true)],
386 }),
387 ];
388
389 expect(
390 resolveDefaultProviderModelSelection(providers, {
391 instanceId: ProviderInstanceId.make("removed-provider"),
392 model: "stale-model",
393 }),
394 ).toEqual({ instanceId: "claudeAgent", model: "claude-opus-4-8" });
395 });
396
397 it.each([{ enabled: false }, { availability: "unavailable" as const }])(
398 "replaces an unavailable stored instance deterministically",
399 (requestedState) => {
400 const providers = [
401 provider({
402 provider: ProviderDriverKind.make("codex"),
403 instanceId: "codex",
404 models: [model("gpt-5.6")],
405 ...requestedState,
406 }),
407 provider({
408 provider: ProviderDriverKind.make("claudeAgent"),
409 instanceId: "claudeAgent",
410 models: [model("claude-opus-4-8", false, true)],
411 }),
412 ];
413
414 expect(
415 resolveDefaultProviderModelSelection(providers, {
416 instanceId: ProviderInstanceId.make("codex"),
417 model: "gpt-5.6",
418 }),
419 ).toEqual({ instanceId: "claudeAgent", model: "claude-opus-4-8" });
420 },
421 );
422
423 it("returns no selection for empty, disabled, unavailable, or error-only profiles", () => {
424 expect(resolveDefaultProviderModelSelection([], null)).toBeNull();
425 expect(
426 resolveDefaultProviderModelSelection(
427 [
428 provider({
429 provider: ProviderDriverKind.make("codex"),
430 instanceId: "codex",
431 enabled: false,
432 }),
433 ],
434 null,
435 ),
436 ).toBeNull();
437 expect(
438 resolveDefaultProviderModelSelection(
439 [
440 provider({
441 provider: ProviderDriverKind.make("codex"),
442 instanceId: "codex",
443 availability: "unavailable",
444 }),
445 ],
446 null,
447 ),
448 ).toBeNull();
449 expect(
450 resolveDefaultProviderModelSelection(
451 [
452 provider({
453 provider: ProviderDriverKind.make("codex"),
454 instanceId: "codex",
455 status: "error",
456 }),
457 ],
458 null,
459 ),
460 ).toBeNull();
461 });
462});
apps/web/src/providerInstances.ts+80-13
...@@ -13,10 +13,12 @@...@@ -13,10 +13,12 @@
13 * @module providerInstances13 * @module providerInstances
14 */14 */
15import {15import {
16 DEFAULT_MODEL_BY_PROVIDER,
16 defaultInstanceIdForDriver,17 defaultInstanceIdForDriver,
17 PROVIDER_DISPLAY_NAMES,18 PROVIDER_DISPLAY_NAMES,
19 type ModelSelection,
18 type ProviderDriverKind,20 type ProviderDriverKind,
19 type ProviderInstanceId,21 ProviderInstanceId,
20 type ServerProvider,22 type ServerProvider,
21 type ServerProviderModel,23 type ServerProviderModel,
22 type ServerSettings,24 type ServerSettings,
...@@ -25,6 +27,16 @@ import {...@@ -25,6 +27,16 @@ import {
2527
26import { formatProviderDriverKindLabel } from "./providerModels";28import { formatProviderDriverKindLabel } from "./providerModels";
2729
30/**
31 * Local-only placeholder used while a draft has no provider it can safely
32 * target. It must never be persisted or dispatched; the composer disables
33 * send until a live provider replaces it.
34 */
35export const NO_PROVIDER_MODEL_SELECTION: ModelSelection = {
36 instanceId: ProviderInstanceId.make("t3code_no_provider"),
37 model: "",
38};
39
28/**40/**
29 * UI-facing projection of one configured provider instance. Carries the41 * UI-facing projection of one configured provider instance. Carries the
30 * snapshot verbatim for callers that need server-side fields we don't42 * snapshot verbatim for callers that need server-side fields we don't
...@@ -253,27 +265,82 @@ export function getProviderInstanceModels(...@@ -253,27 +265,82 @@ export function getProviderInstanceModels(
253 return getProviderInstanceEntry(providers, instanceId)?.models ?? [];265 return getProviderInstanceEntry(providers, instanceId)?.models ?? [];
254}266}
255267
268/**
269 * Default model slug for a specific instance: its declared built-in default,
270 * then its first built-in model, then any model it reports, then the driver-level default. Custom
271 * instances can serve a different model list than the default instance of
272 * the same driver kind, so the lookup must be instance-scoped rather than
273 * kind-scoped.
274 */
275export function getDefaultProviderInstanceModel(
276 providers: ReadonlyArray<ServerProvider>,
277 instanceId: ProviderInstanceId,
278): string | undefined {
279 const entry = getProviderInstanceEntry(providers, instanceId);
280 if (!entry) return undefined;
281 return (
282 entry.models.find((model) => model.isDefault && !model.isCustom)?.slug ??
283 entry.models.find((model) => !model.isCustom)?.slug ??
284 entry.models[0]?.slug ??
285 DEFAULT_MODEL_BY_PROVIDER[entry.driverKind]
286 );
287}
288
289const isSelectableProviderInstanceEntry = (entry: ProviderInstanceEntry): boolean =>
290 entry.enabled && entry.isAvailable;
291
292/**
293 * Resolve an exact stored instance when it remains enabled and available.
294 * Otherwise choose a deterministic fallback that can plausibly start now:
295 * ready first, then a non-error probe result. An errored provider is retained
296 * only when it was explicitly requested; it is never invented as a new-user
297 * default.
298 */
299export function resolveSelectableProviderInstanceEntry(
300 entries: ReadonlyArray<ProviderInstanceEntry>,
301 instanceId: ProviderInstanceId | undefined,
302): ProviderInstanceEntry | undefined {
303 if (instanceId !== undefined) {
304 const requested = entries.find((entry) => entry.instanceId === instanceId);
305 if (requested && isSelectableProviderInstanceEntry(requested)) {
306 return requested;
307 }
308 }
309 return (
310 entries.find(isProviderInstancePickerReady) ??
311 entries.find((entry) => isSelectableProviderInstanceEntry(entry) && entry.status !== "error")
312 );
313}
314
256/**315/**
257 * Resolve the routing key for a selection that may reference an instance316 * Resolve the routing key for a selection that may reference an instance
258 * id that no longer exists (e.g. a persisted thread selection after the317 * id that no longer exists (e.g. a persisted thread selection after the
259 * user deleted the custom instance). Returns the first enabled instance318 * user deleted the custom instance). Returns a ready or non-error fallback,
260 * as a fallback so downstream code can still send a turn.319 * or `undefined` when no provider can safely become a new selection.
261 */320 */
262export function resolveSelectableProviderInstance(321export function resolveSelectableProviderInstance(
263 providers: ReadonlyArray<ServerProvider>,322 providers: ReadonlyArray<ServerProvider>,
264 instanceId: ProviderInstanceId | undefined,323 instanceId: ProviderInstanceId | undefined,
265): ProviderInstanceId | undefined {324): ProviderInstanceId | undefined {
266 if (instanceId === undefined) {
267 return deriveProviderInstanceEntries(providers).find(
268 (entry) => entry.enabled && entry.isAvailable,
269 )?.instanceId;
270 }
271 const entries = deriveProviderInstanceEntries(providers);325 const entries = deriveProviderInstanceEntries(providers);
272 const requested = entries.find((entry) => entry.instanceId === instanceId);326 return resolveSelectableProviderInstanceEntry(entries, instanceId)?.instanceId;
273 if (requested && requested.enabled && requested.isAvailable) {327}
274 return instanceId;328
275 }329/**
276 return entries.find((entry) => entry.enabled && entry.isAvailable)?.instanceId;330 * Resolve the model selection persisted for a project or new thread. A valid
331 * stored selection is preserved byte-for-byte. Falling back to another
332 * instance also resets the model to that instance's own default, avoiding
333 * cross-provider instance/model pairs.
334 */
335export function resolveDefaultProviderModelSelection(
336 providers: ReadonlyArray<ServerProvider>,
337 selection: ModelSelection | null | undefined,
338): ModelSelection | null {
339 const instanceId = resolveSelectableProviderInstance(providers, selection?.instanceId);
340 if (instanceId === undefined) return null;
341 if (selection?.instanceId === instanceId) return selection;
342 const model = getDefaultProviderInstanceModel(providers, instanceId);
343 return model ? { instanceId, model } : null;
277}344}
278345
279/**346/**
packages/client-runtime/src/operations/projects.test.ts+1-5
...@@ -1,6 +1,5 @@...@@ -1,6 +1,5 @@
1import { describe, expect, it } from "vite-plus/test";1import { describe, expect, it } from "vite-plus/test";
2import {2import {
3 DEFAULT_MODEL,
4 EnvironmentId,3 EnvironmentId,
5 ProjectId,4 ProjectId,
6 CommandId,5 CommandId,
...@@ -138,10 +137,7 @@ describe("add project shared logic", () => {...@@ -138,10 +137,7 @@ describe("add project shared logic", () => {
138 title: "repo",137 title: "repo",
139 workspaceRoot: "/work/repo",138 workspaceRoot: "/work/repo",
140 createWorkspaceRootIfMissing: true,139 createWorkspaceRootIfMissing: true,
141 defaultModelSelection: {140 defaultModelSelection: null,
142 instanceId: "codex",
143 model: DEFAULT_MODEL,
144 },
145 });141 });
146 });142 });
147});143});
packages/client-runtime/src/operations/projects.ts+1-5
...@@ -7,7 +7,6 @@ import type {...@@ -7,7 +7,6 @@ import type {
7 SourceControlProviderKind,7 SourceControlProviderKind,
8 SourceControlRepositoryInfo,8 SourceControlRepositoryInfo,
9} from "@t3tools/contracts";9} from "@t3tools/contracts";
10import { DEFAULT_MODEL, ProviderInstanceId } from "@t3tools/contracts";
11import * as Arr from "effect/Array";10import * as Arr from "effect/Array";
12import * as Option from "effect/Option";11import * as Option from "effect/Option";
13import * as Order from "effect/Order";12import * as Order from "effect/Order";
...@@ -215,10 +214,7 @@ export function buildProjectCreateCommand(input: {...@@ -215,10 +214,7 @@ export function buildProjectCreateCommand(input: {
215 title: inferProjectTitleFromPath(input.workspaceRoot),214 title: inferProjectTitleFromPath(input.workspaceRoot),
216 workspaceRoot: input.workspaceRoot,215 workspaceRoot: input.workspaceRoot,
217 createWorkspaceRootIfMissing: true,216 createWorkspaceRootIfMissing: true,
218 defaultModelSelection: {217 defaultModelSelection: null,
219 instanceId: ProviderInstanceId.make("codex"),
220 model: DEFAULT_MODEL,
221 },
222 createdAt: input.createdAt,218 createdAt: input.createdAt,
223 };219 };
224}220}