| author | |
| committer | |
| log | b6e1b39335ffd34583c94f5b1e5e2c80520729fe |
| tree | 1ccbee9c94905fc17de762a7edb7427d38f22a92 |
| parent | c7b21ff172f8e64318ec681e8839ee60ca660fa4 |
| signature | Signed by PGP key B5690EEEBB952194 |
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 | 152 | } from "~/projectScripts"; |
| 153 | 153 | import { newDraftId, newMessageId, newThreadId } from "~/lib/utils"; |
| 154 | 154 | import { getProviderModelCapabilities, resolveSelectableProvider } from "../providerModels"; |
| 155 | import { NO_PROVIDER_MODEL_SELECTION } from "../providerInstances"; | |
| 155 | 156 | import { useEnvironmentSettings } from "../hooks/useSettings"; |
| 156 | 157 | import { resolveAppModelSelectionForInstance } from "../modelSelection"; |
| 157 | 158 | import { getTerminalFocusOwner } from "../lib/terminalFocus"; |
| ... | ... | @@ -1361,10 +1362,7 @@ function ChatViewContent(props: ChatViewProps) { |
| 1361 | 1362 | ? buildLocalDraftThread( |
| 1362 | 1363 | threadId, |
| 1363 | 1364 | draftThread, |
| 1364 | fallbackDraftProject?.defaultModelSelection ?? { | |
| 1365 | instanceId: ProviderInstanceId.make("codex"), | |
| 1366 | model: DEFAULT_MODEL, | |
| 1367 | }, | |
| 1365 | fallbackDraftProject?.defaultModelSelection ?? NO_PROVIDER_MODEL_SELECTION, | |
| 1368 | 1366 | ) |
| 1369 | 1367 | : undefined, |
| 1370 | 1368 | [draftThread, fallbackDraftProject?.defaultModelSelection, threadId], |
| ... | ... | @@ -1856,7 +1854,7 @@ function ChatViewContent(props: ChatViewProps) { |
| 1856 | 1854 | const providerStatuses = serverConfig?.providers ?? EMPTY_PROVIDERS; |
| 1857 | 1855 | const unlockedSelectedProvider = resolveSelectableProvider( |
| 1858 | 1856 | providerStatuses, |
| 1859 | selectedProviderByThreadId ?? threadProvider ?? ProviderDriverKind.make("codex"), | |
| 1857 | selectedProviderByThreadId ?? threadProvider, | |
| 1860 | 1858 | ); |
| 1861 | 1859 | const selectedProvider: ProviderDriverKind = lockedProvider ?? unlockedSelectedProvider; |
| 1862 | 1860 | const phase = derivePhase(activeThread?.session ?? null); |
| ... | ... | @@ -4038,7 +4036,7 @@ function ChatViewContent(props: ChatViewProps) { |
| 4038 | 4036 | return; |
| 4039 | 4037 | } |
| 4040 | 4038 | const sendCtx = composerRef.current?.getSendContext(); |
| 4041 | if (!sendCtx) return; | |
| 4039 | if (!sendCtx?.providerAvailable) return; | |
| 4042 | 4040 | const { |
| 4043 | 4041 | images: composerImages, |
| 4044 | 4042 | terminalContexts: composerTerminalContexts, |
| ... | ... | @@ -4614,7 +4612,7 @@ function ChatViewContent(props: ChatViewProps) { |
| 4614 | 4612 | } |
| 4615 | 4613 | |
| 4616 | 4614 | const sendCtx = composerRef.current?.getSendContext(); |
| 4617 | if (!sendCtx) { | |
| 4615 | if (!sendCtx?.providerAvailable) { | |
| 4618 | 4616 | return; |
| 4619 | 4617 | } |
| 4620 | 4618 | const { |
| ... | ... | @@ -4773,7 +4771,7 @@ function ChatViewContent(props: ChatViewProps) { |
| 4773 | 4771 | } |
| 4774 | 4772 | |
| 4775 | 4773 | const sendCtx = composerRef.current?.getSendContext(); |
| 4776 | if (!sendCtx) { | |
| 4774 | if (!sendCtx?.providerAvailable) { | |
| 4777 | 4775 | return; |
| 4778 | 4776 | } |
| 4779 | 4777 | const { |
apps/web/src/components/CommandPalette.tsx+14-7| ... | ... | @@ -7,12 +7,10 @@ import { |
| 7 | 7 | squashAtomCommandFailure, |
| 8 | 8 | } from "@t3tools/client-runtime/state/runtime"; |
| 9 | 9 | import { |
| 10 | DEFAULT_MODEL, | |
| 11 | 10 | type DesktopWslState, |
| 12 | 11 | type EnvironmentId, |
| 13 | 12 | type FilesystemBrowseResult, |
| 14 | 13 | type ProjectId, |
| 15 | ProviderInstanceId, | |
| 16 | 14 | type SourceControlDiscoveryResult, |
| 17 | 15 | type SourceControlProviderKind, |
| 18 | 16 | type SourceControlRepositoryInfo, |
| ... | ... | @@ -111,7 +109,8 @@ import { CommandPaletteResults } from "./CommandPaletteResults"; |
| 111 | 109 | import { AzureDevOpsIcon, BitbucketIcon, GitHubIcon, GitLabIcon } from "./Icons"; |
| 112 | 110 | import { ProjectFavicon } from "./ProjectFavicon"; |
| 113 | 111 | import { ThreadRowLeadingStatus, ThreadRowTrailingStatus } from "./ThreadStatusIndicators"; |
| 114 | import { primaryServerKeybindingsAtom } from "../state/server"; | |
| 112 | import { primaryServerKeybindingsAtom, primaryServerProvidersAtom } from "../state/server"; | |
| 113 | import { resolveDefaultProviderModelSelection } from "../providerInstances"; | |
| 115 | 114 | import { resolveShortcutCommand } from "../keybindings"; |
| 116 | 115 | import { |
| 117 | 116 | Command, |
| ... | ... | @@ -476,6 +475,7 @@ function OpenCommandPaletteDialog(props: { |
| 476 | 475 | const projects = useProjects(); |
| 477 | 476 | const threads = useThreadShells(); |
| 478 | 477 | const keybindings = useAtomValue(primaryServerKeybindingsAtom); |
| 478 | const providers = useAtomValue(primaryServerProvidersAtom); | |
| 479 | 479 | const [viewStack, setViewStack] = useState<CommandPaletteView[]>([]); |
| 480 | 480 | const currentView = viewStack.at(-1) ?? null; |
| 481 | 481 | const [browseGeneration, setBrowseGeneration] = useState(0); |
| ... | ... | @@ -1149,6 +1149,10 @@ function OpenCommandPaletteDialog(props: { |
| 1149 | 1149 | } |
| 1150 | 1150 | |
| 1151 | 1151 | const projectId = newProjectId(); |
| 1152 | const targetEnvironmentProviders = | |
| 1153 | environments.find((environment) => environment.environmentId === input.environmentId) | |
| 1154 | ?.serverConfig?.providers ?? | |
| 1155 | (input.environmentId === primaryEnvironmentId ? providers : []); | |
| 1152 | 1156 | const createResult = await createProject({ |
| 1153 | 1157 | environmentId: input.environmentId, |
| 1154 | 1158 | input: { |
| ... | ... | @@ -1156,10 +1160,10 @@ function OpenCommandPaletteDialog(props: { |
| 1156 | 1160 | title: inferProjectTitleFromPath(cwd), |
| 1157 | 1161 | workspaceRoot: cwd, |
| 1158 | 1162 | createWorkspaceRootIfMissing: true, |
| 1159 | defaultModelSelection: { | |
| 1160 | instanceId: ProviderInstanceId.make("codex"), | |
| 1161 | model: DEFAULT_MODEL, | |
| 1162 | }, | |
| 1163 | defaultModelSelection: resolveDefaultProviderModelSelection( | |
| 1164 | targetEnvironmentProviders, | |
| 1165 | null, | |
| 1166 | ), | |
| 1163 | 1167 | }, |
| 1164 | 1168 | }); |
| 1165 | 1169 | if (createResult._tag === "Failure") { |
| ... | ... | @@ -1195,8 +1199,11 @@ function OpenCommandPaletteDialog(props: { |
| 1195 | 1199 | [ |
| 1196 | 1200 | handleNewThread, |
| 1197 | 1201 | createProject, |
| 1202 | environments, | |
| 1198 | 1203 | navigate, |
| 1204 | primaryEnvironmentId, | |
| 1199 | 1205 | projects, |
| 1206 | providers, | |
| 1200 | 1207 | setOpen, |
| 1201 | 1208 | clientSettings.sidebarThreadSortOrder, |
| 1202 | 1209 | threads, |
apps/web/src/components/chat/ChatComposer.tsx+100-57| ... | ... | @@ -114,7 +114,9 @@ import { getProviderDisplayName, getProviderInteractionModeToggle } from "../../ |
| 114 | 114 | import { |
| 115 | 115 | applyProviderInstanceSettings, |
| 116 | 116 | deriveProviderInstanceEntries, |
| 117 | NO_PROVIDER_MODEL_SELECTION, | |
| 117 | 118 | resolveProviderDriverKindForInstanceSelection, |
| 119 | resolveSelectableProviderInstanceEntry, | |
| 118 | 120 | sortProviderInstanceEntries, |
| 119 | 121 | type ProviderInstanceEntry, |
| 120 | 122 | } from "../../providerInstances"; |
| ... | ... | @@ -433,6 +435,7 @@ export interface ChatComposerHandle { |
| 433 | 435 | selectedPromptEffort: string | null; |
| 434 | 436 | selectedModelOptionsForDispatch: unknown; |
| 435 | 437 | selectedModelSelection: ModelSelection; |
| 438 | providerAvailable: boolean; | |
| 436 | 439 | selectedProvider: ProviderDriverKind; |
| 437 | 440 | selectedModel: string; |
| 438 | 441 | selectedProviderModels: ReadonlyArray<ServerProvider["models"][number]>; |
| ... | ... | @@ -696,8 +699,10 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) |
| 696 | 699 | providerInstanceEntries, |
| 697 | 700 | providerStatuses, |
| 698 | 701 | explicitSelectedInstanceId, |
| 699 | ) ?? ProviderDriverKind.make("codex"); | |
| 700 | const selectedProvider: ProviderDriverKind = lockedProvider ?? unlockedSelectedProvider; | |
| 702 | ) ?? | |
| 703 | providerInstanceEntries[0]?.driverKind ?? | |
| 704 | ProviderDriverKind.make("unconfigured"); | |
| 705 | const requestedDriverKind: ProviderDriverKind = lockedProvider ?? unlockedSelectedProvider; | |
| 701 | 706 | const lockedContinuationGroupKey = useMemo((): string | null => { |
| 702 | 707 | if (!lockedProvider || !activeThread) return null; |
| 703 | 708 | const lockedInstanceId = |
| ... | ... | @@ -734,7 +739,7 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) |
| 734 | 739 | for (const candidate of candidates) { |
| 735 | 740 | if (!candidate) continue; |
| 736 | 741 | const match = providerInstanceEntries.find( |
| 737 | (entry) => entry.instanceId === candidate && entry.enabled, | |
| 742 | (entry) => entry.instanceId === candidate && entry.enabled && entry.isAvailable, | |
| 738 | 743 | ); |
| 739 | 744 | if (match) { |
| 740 | 745 | // When locked to a specific driver kind, ignore persisted instance |
| ... | ... | @@ -749,36 +754,44 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) |
| 749 | 754 | return match.instanceId; |
| 750 | 755 | } |
| 751 | 756 | } |
| 752 | if (explicitSelectedInstanceId) { | |
| 753 | return ProviderInstanceId.make(explicitSelectedInstanceId); | |
| 754 | } | |
| 755 | const byKind = providerInstanceEntries.find( | |
| 757 | const compatibleEntries = providerInstanceEntries.filter( | |
| 756 | 758 | (entry) => |
| 757 | entry.enabled && | |
| 758 | entry.driverKind === selectedProvider && | |
| 759 | (!lockedProvider || entry.driverKind === lockedProvider) && | |
| 759 | 760 | (!lockedContinuationGroupKey || entry.continuationGroupKey === lockedContinuationGroupKey), |
| 760 | 761 | ); |
| 761 | if (byKind) return byKind.instanceId; | |
| 762 | const anyEnabled = providerInstanceEntries.find((entry) => entry.enabled); | |
| 762 | const requestedDriverEntries = compatibleEntries.filter( | |
| 763 | (entry) => entry.driverKind === requestedDriverKind, | |
| 764 | ); | |
| 763 | 765 | return ( |
| 764 | anyEnabled?.instanceId ?? | |
| 765 | providerInstanceEntries[0]?.instanceId ?? | |
| 766 | activeThreadModelSelection?.instanceId ?? | |
| 767 | activeProjectDefaultModelSelection?.instanceId ?? | |
| 768 | ProviderInstanceId.make("codex") | |
| 766 | resolveSelectableProviderInstanceEntry(requestedDriverEntries, undefined)?.instanceId ?? | |
| 767 | resolveSelectableProviderInstanceEntry(compatibleEntries, undefined)?.instanceId ?? | |
| 768 | NO_PROVIDER_MODEL_SELECTION.instanceId | |
| 769 | 769 | ); |
| 770 | 770 | }, [ |
| 771 | 771 | activeProjectDefaultModelSelection?.instanceId, |
| 772 | 772 | activeThread?.session?.providerInstanceId, |
| 773 | 773 | activeThreadModelSelection?.instanceId, |
| 774 | 774 | composerDraft.activeProvider, |
| 775 | explicitSelectedInstanceId, | |
| 776 | 775 | lockedContinuationGroupKey, |
| 777 | 776 | lockedProvider, |
| 778 | 777 | providerInstanceEntries, |
| 779 | selectedProvider, | |
| 778 | requestedDriverKind, | |
| 780 | 779 | ]); |
| 781 | 780 | |
| 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 | 795 | const { modelOptions: composerModelOptions, selectedModel } = useEffectiveComposerModelState({ |
| 783 | 796 | threadRef: composerDraftTarget, |
| 784 | 797 | providers: providerStatuses, |
| ... | ... | @@ -788,14 +801,6 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) |
| 788 | 801 | projectModelSelection: activeProjectDefaultModelSelection, |
| 789 | 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 | 804 | const selectedProviderStatus = useMemo( |
| 800 | 805 | () => selectedProviderEntry?.snapshot ?? null, |
| 801 | 806 | [selectedProviderEntry], |
| ... | ... | @@ -1158,6 +1163,7 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) |
| 1158 | 1163 | phase === "running" || |
| 1159 | 1164 | isSendBusy || |
| 1160 | 1165 | isConnecting || |
| 1166 | noProviderAvailable || | |
| 1161 | 1167 | projectSelectionRequired || |
| 1162 | 1168 | environmentUnavailable !== null || |
| 1163 | 1169 | !composerSendState.hasSendableContent; |
| ... | ... | @@ -1698,7 +1704,13 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) |
| 1698 | 1704 | |
| 1699 | 1705 | const shouldBlurMobileComposerOnSubmit = useCallback(() => { |
| 1700 | 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 | 1714 | return false; |
| 1703 | 1715 | } |
| 1704 | 1716 | if (activePendingProgress) { |
| ... | ... | @@ -1713,18 +1725,23 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) |
| 1713 | 1725 | isConnecting, |
| 1714 | 1726 | isMobileViewport, |
| 1715 | 1727 | isSendBusy, |
| 1728 | noProviderAvailable, | |
| 1716 | 1729 | phase, |
| 1717 | 1730 | showPlanFollowUpPrompt, |
| 1718 | 1731 | ]); |
| 1719 | 1732 | |
| 1720 | 1733 | const submitComposer = useCallback( |
| 1721 | 1734 | (event?: { preventDefault: () => void }) => { |
| 1735 | if (noProviderAvailable) { | |
| 1736 | event?.preventDefault(); | |
| 1737 | return; | |
| 1738 | } | |
| 1722 | 1739 | onSend(event); |
| 1723 | 1740 | if (shouldBlurMobileComposerOnSubmit()) { |
| 1724 | 1741 | blurMobileComposerAfterSend(); |
| 1725 | 1742 | } |
| 1726 | 1743 | }, |
| 1727 | [blurMobileComposerAfterSend, onSend, shouldBlurMobileComposerOnSubmit], | |
| 1744 | [blurMobileComposerAfterSend, noProviderAvailable, onSend, shouldBlurMobileComposerOnSubmit], | |
| 1728 | 1745 | ); |
| 1729 | 1746 | const expandMobileComposer = useCallback(() => { |
| 1730 | 1747 | if (composerBlurFrameRef.current !== null) { |
| ... | ... | @@ -2083,6 +2100,7 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) |
| 2083 | 2100 | selectedPromptEffort, |
| 2084 | 2101 | selectedModelOptionsForDispatch, |
| 2085 | 2102 | selectedModelSelection, |
| 2103 | providerAvailable: !noProviderAvailable, | |
| 2086 | 2104 | selectedProvider, |
| 2087 | 2105 | selectedModel, |
| 2088 | 2106 | selectedProviderModels, |
| ... | ... | @@ -2110,6 +2128,7 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) |
| 2110 | 2128 | selectedModel, |
| 2111 | 2129 | selectedModelOptionsForDispatch, |
| 2112 | 2130 | selectedModelSelection, |
| 2131 | noProviderAvailable, | |
| 2113 | 2132 | selectedPromptEffort, |
| 2114 | 2133 | selectedProvider, |
| 2115 | 2134 | selectedProviderModels, |
| ... | ... | @@ -2260,7 +2279,9 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) |
| 2260 | 2279 | isSendBusy={isSendBusy} |
| 2261 | 2280 | isConnecting={isConnecting} |
| 2262 | 2281 | isEnvironmentUnavailable={ |
| 2263 | environmentUnavailable !== null || projectSelectionRequired | |
| 2282 | environmentUnavailable !== null || | |
| 2283 | noProviderAvailable || | |
| 2284 | projectSelectionRequired | |
| 2264 | 2285 | } |
| 2265 | 2286 | isPreparingWorktree={false} |
| 2266 | 2287 | hasSendableContent={false} |
| ... | ... | @@ -2292,7 +2313,8 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) |
| 2292 | 2313 | {activePendingProgress |
| 2293 | 2314 | ? activePendingProgress.customAnswer || |
| 2294 | 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 | 2318 | </button> |
| 2297 | 2319 | <button |
| 2298 | 2320 | type="button" |
| ... | ... | @@ -2501,9 +2523,11 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) |
| 2501 | 2523 | ? `${environmentUnavailable.label}: ${connectionStatusText( |
| 2502 | 2524 | environmentUnavailable.connection, |
| 2503 | 2525 | )}` |
| 2504 | : phase === "disconnected" | |
| 2505 | ? "Ask for follow-up changes or attach images" | |
| 2506 | : "Ask anything, @tag files/folders, $use skills, or / for commands" | |
| 2526 | : noProviderAvailable | |
| 2527 | ? "Enable a provider in Settings to send a message" | |
| 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 | 2532 | disabled={isConnecting || isComposerApprovalState || projectSelectionRequired} |
| 2509 | 2533 | /> |
| ... | ... | @@ -2521,7 +2545,9 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) |
| 2521 | 2545 | isSendBusy={isSendBusy} |
| 2522 | 2546 | isConnecting={isConnecting} |
| 2523 | 2547 | isEnvironmentUnavailable={ |
| 2524 | environmentUnavailable !== null || projectSelectionRequired | |
| 2548 | environmentUnavailable !== null || | |
| 2549 | noProviderAvailable || | |
| 2550 | projectSelectionRequired | |
| 2525 | 2551 | } |
| 2526 | 2552 | isPreparingWorktree={false} |
| 2527 | 2553 | hasSendableContent={false} |
| ... | ... | @@ -2556,28 +2582,43 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) |
| 2556 | 2582 | )} |
| 2557 | 2583 | > |
| 2558 | 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 | <ProviderModelPicker | |
| 2560 | compact={isComposerFooterCompact} | |
| 2561 | activeInstanceId={selectedInstanceId} | |
| 2562 | model={selectedModelForPickerWithCustomFallback} | |
| 2563 | lockedProvider={lockedProvider} | |
| 2564 | lockedContinuationGroupKey={lockedContinuationGroupKey} | |
| 2565 | instanceEntries={providerInstanceEntries} | |
| 2566 | keybindings={keybindings} | |
| 2567 | modelOptionsByInstance={modelOptionsByInstance} | |
| 2568 | terminalOpen={terminalOpen} | |
| 2569 | open={isComposerModelPickerOpen} | |
| 2570 | {...(composerProviderState.modelPickerIconClassName | |
| 2571 | ? { | |
| 2572 | activeProviderIconClassName: composerProviderState.modelPickerIconClassName, | |
| 2573 | } | |
| 2574 | : {})} | |
| 2575 | onOpenChange={(open) => { | |
| 2576 | setIsComposerModelPickerOpen(open); | |
| 2577 | }} | |
| 2578 | getModelDisabledReason={getModelDisabledReason} | |
| 2579 | onInstanceModelChange={onProviderModelSelect} | |
| 2580 | /> | |
| 2585 | {noProviderAvailable ? ( | |
| 2586 | <Button | |
| 2587 | type="button" | |
| 2588 | size="sm" | |
| 2589 | variant="ghost" | |
| 2590 | disabled | |
| 2591 | data-chat-provider-unavailable="true" | |
| 2592 | className="shrink-0 gap-2 px-2 text-muted-foreground/70 sm:px-3" | |
| 2593 | > | |
| 2594 | <CircleAlertIcon className="size-4" /> | |
| 2595 | No provider available | |
| 2596 | </Button> | |
| 2597 | ) : ( | |
| 2598 | <ProviderModelPicker | |
| 2599 | compact={isComposerFooterCompact} | |
| 2600 | activeInstanceId={selectedInstanceId} | |
| 2601 | model={selectedModelForPickerWithCustomFallback} | |
| 2602 | lockedProvider={lockedProvider} | |
| 2603 | lockedContinuationGroupKey={lockedContinuationGroupKey} | |
| 2604 | instanceEntries={providerInstanceEntries} | |
| 2605 | keybindings={keybindings} | |
| 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 | )} | |
| 2581 | 2622 | |
| 2582 | 2623 | {isComposerFooterCompact ? ( |
| 2583 | 2624 | <CompactComposerControlsMenu |
| ... | ... | @@ -2634,7 +2675,9 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) |
| 2634 | 2675 | isSendBusy={isSendBusy} |
| 2635 | 2676 | isConnecting={isConnecting} |
| 2636 | 2677 | isEnvironmentUnavailable={ |
| 2637 | environmentUnavailable !== null || projectSelectionRequired | |
| 2678 | environmentUnavailable !== null || | |
| 2679 | noProviderAvailable || | |
| 2680 | projectSelectionRequired | |
| 2638 | 2681 | } |
| 2639 | 2682 | isPreparingWorktree={isPreparingWorktree} |
| 2640 | 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 | 3 | import { |
| 4 | 4 | applyProviderInstanceSettings, |
| 5 | 5 | deriveProviderInstanceEntries, |
| 6 | getDefaultProviderInstanceModel, | |
| 6 | 7 | isProviderInstancePickerReady, |
| 7 | 8 | isProviderInstancePickerVisible, |
| 9 | resolveDefaultProviderModelSelection, | |
| 8 | 10 | resolveSelectableProviderInstance, |
| 9 | 11 | resolveProviderDriverKindForInstanceSelection, |
| 10 | 12 | } from "./providerInstances"; |
| ... | ... | @@ -15,6 +17,8 @@ function provider(input: { |
| 15 | 17 | enabled?: boolean; |
| 16 | 18 | availability?: ServerProvider["availability"]; |
| 17 | 19 | displayName?: string; |
| 20 | status?: ServerProvider["status"]; | |
| 21 | models?: ServerProvider["models"]; | |
| 18 | 22 | }): ServerProvider { |
| 19 | 23 | return { |
| 20 | 24 | instanceId: ProviderInstanceId.make(input.instanceId), |
| ... | ... | @@ -23,16 +27,24 @@ function provider(input: { |
| 23 | 27 | enabled: input.enabled ?? true, |
| 24 | 28 | installed: true, |
| 25 | 29 | version: null, |
| 26 | status: "ready", | |
| 30 | status: input.status ?? "ready", | |
| 27 | 31 | ...(input.availability ? { availability: input.availability } : {}), |
| 28 | 32 | auth: { status: "authenticated" }, |
| 29 | 33 | checkedAt: "2026-01-01T00:00:00.000Z", |
| 30 | models: [], | |
| 34 | models: input.models ?? [], | |
| 31 | 35 | slashCommands: [], |
| 32 | 36 | skills: [], |
| 33 | 37 | }; |
| 34 | 38 | } |
| 35 | 39 | |
| 40 | const model = (slug: string, isCustom = false, isDefault = false) => ({ | |
| 41 | slug, | |
| 42 | name: slug, | |
| 43 | isCustom, | |
| 44 | ...(isDefault ? { isDefault: true } : {}), | |
| 45 | capabilities: {}, | |
| 46 | }); | |
| 47 | ||
| 36 | 48 | describe("isProviderInstancePickerReady", () => { |
| 37 | 49 | it("rejects a disabled instance even while its last probe status is ready", () => { |
| 38 | 50 | const [entry] = deriveProviderInstanceEntries([ |
| ... | ... | @@ -146,6 +158,67 @@ describe("resolveSelectableProviderInstance", () => { |
| 146 | 158 | expect(resolveSelectableProviderInstance(providers, disabled)).toBe(fallback); |
| 147 | 159 | }); |
| 148 | 160 | |
| 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 | 222 | it("does not return disabled, unavailable, or unknown instances when none are sendable", () => { |
| 150 | 223 | const disabled = ProviderInstanceId.make("codex"); |
| 151 | 224 | const unavailable = ProviderInstanceId.make("claudeAgent"); |
| ... | ... | @@ -206,3 +279,184 @@ describe("resolveProviderDriverKindForInstanceSelection", () => { |
| 206 | 279 | ).toBeUndefined(); |
| 207 | 280 | }); |
| 208 | 281 | }); |
| 282 | ||
| 283 | describe("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 | ||
| 337 | describe("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 | 13 | * @module providerInstances |
| 14 | 14 | */ |
| 15 | 15 | import { |
| 16 | DEFAULT_MODEL_BY_PROVIDER, | |
| 16 | 17 | defaultInstanceIdForDriver, |
| 17 | 18 | PROVIDER_DISPLAY_NAMES, |
| 19 | type ModelSelection, | |
| 18 | 20 | type ProviderDriverKind, |
| 19 | type ProviderInstanceId, | |
| 21 | ProviderInstanceId, | |
| 20 | 22 | type ServerProvider, |
| 21 | 23 | type ServerProviderModel, |
| 22 | 24 | type ServerSettings, |
| ... | ... | @@ -25,6 +27,16 @@ import { |
| 25 | 27 | |
| 26 | 28 | import { formatProviderDriverKindLabel } from "./providerModels"; |
| 27 | 29 | |
| 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 | */ | |
| 35 | export const NO_PROVIDER_MODEL_SELECTION: ModelSelection = { | |
| 36 | instanceId: ProviderInstanceId.make("t3code_no_provider"), | |
| 37 | model: "", | |
| 38 | }; | |
| 39 | ||
| 28 | 40 | /** |
| 29 | 41 | * UI-facing projection of one configured provider instance. Carries the |
| 30 | 42 | * snapshot verbatim for callers that need server-side fields we don't |
| ... | ... | @@ -253,27 +265,82 @@ export function getProviderInstanceModels( |
| 253 | 265 | return getProviderInstanceEntry(providers, instanceId)?.models ?? []; |
| 254 | 266 | } |
| 255 | 267 | |
| 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 | */ | |
| 275 | export 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 | ||
| 289 | const 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 | */ | |
| 299 | export 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 | 316 | * Resolve the routing key for a selection that may reference an instance |
| 258 | 317 | * id that no longer exists (e.g. a persisted thread selection after the |
| 259 | * user deleted the custom instance). Returns the first enabled instance | |
| 260 | * as a fallback so downstream code can still send a turn. | |
| 318 | * user deleted the custom instance). Returns a ready or non-error fallback, | |
| 319 | * or `undefined` when no provider can safely become a new selection. | |
| 261 | 320 | */ |
| 262 | 321 | export function resolveSelectableProviderInstance( |
| 263 | 322 | providers: ReadonlyArray<ServerProvider>, |
| 264 | 323 | instanceId: ProviderInstanceId | undefined, |
| 265 | 324 | ): ProviderInstanceId | undefined { |
| 266 | if (instanceId === undefined) { | |
| 267 | return deriveProviderInstanceEntries(providers).find( | |
| 268 | (entry) => entry.enabled && entry.isAvailable, | |
| 269 | )?.instanceId; | |
| 270 | } | |
| 271 | 325 | const entries = deriveProviderInstanceEntries(providers); |
| 272 | const requested = entries.find((entry) => entry.instanceId === instanceId); | |
| 273 | if (requested && requested.enabled && requested.isAvailable) { | |
| 274 | return instanceId; | |
| 275 | } | |
| 276 | return entries.find((entry) => entry.enabled && entry.isAvailable)?.instanceId; | |
| 326 | return resolveSelectableProviderInstanceEntry(entries, instanceId)?.instanceId; | |
| 327 | } | |
| 328 | ||
| 329 | /** | |
| 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 | */ | |
| 335 | export 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 | } |
| 278 | 345 | |
| 279 | 346 | /** |
packages/client-runtime/src/operations/projects.test.ts+1-5| ... | ... | @@ -1,6 +1,5 @@ |
| 1 | 1 | import { describe, expect, it } from "vite-plus/test"; |
| 2 | 2 | import { |
| 3 | DEFAULT_MODEL, | |
| 4 | 3 | EnvironmentId, |
| 5 | 4 | ProjectId, |
| 6 | 5 | CommandId, |
| ... | ... | @@ -138,10 +137,7 @@ describe("add project shared logic", () => { |
| 138 | 137 | title: "repo", |
| 139 | 138 | workspaceRoot: "/work/repo", |
| 140 | 139 | createWorkspaceRootIfMissing: true, |
| 141 | defaultModelSelection: { | |
| 142 | instanceId: "codex", | |
| 143 | model: DEFAULT_MODEL, | |
| 144 | }, | |
| 140 | defaultModelSelection: null, | |
| 145 | 141 | }); |
| 146 | 142 | }); |
| 147 | 143 | }); |
packages/client-runtime/src/operations/projects.ts+1-5| ... | ... | @@ -7,7 +7,6 @@ import type { |
| 7 | 7 | SourceControlProviderKind, |
| 8 | 8 | SourceControlRepositoryInfo, |
| 9 | 9 | } from "@t3tools/contracts"; |
| 10 | import { DEFAULT_MODEL, ProviderInstanceId } from "@t3tools/contracts"; | |
| 11 | 10 | import * as Arr from "effect/Array"; |
| 12 | 11 | import * as Option from "effect/Option"; |
| 13 | 12 | import * as Order from "effect/Order"; |
| ... | ... | @@ -215,10 +214,7 @@ export function buildProjectCreateCommand(input: { |
| 215 | 214 | title: inferProjectTitleFromPath(input.workspaceRoot), |
| 216 | 215 | workspaceRoot: input.workspaceRoot, |
| 217 | 216 | createWorkspaceRootIfMissing: true, |
| 218 | defaultModelSelection: { | |
| 219 | instanceId: ProviderInstanceId.make("codex"), | |
| 220 | model: DEFAULT_MODEL, | |
| 221 | }, | |
| 217 | defaultModelSelection: null, | |
| 222 | 218 | createdAt: input.createdAt, |
| 223 | 219 | }; |
| 224 | 220 | } |