| ... | @@ -6,6 +6,8 @@ import { | ... | @@ -6,6 +6,8 @@ import { |
| 6 | type EditorId, | 6 | type EditorId, |
| 7 | type KeybindingCommand, | 7 | type KeybindingCommand, |
| 8 | type MessageId, | 8 | type MessageId, |
| | 9 | getDefaultModel, |
| | 10 | getModelOptions, |
| 9 | type ProjectId, | 11 | type ProjectId, |
| 10 | type ProjectEntry, | 12 | type ProjectEntry, |
| 11 | type ProjectScript, | 13 | type ProjectScript, |
| ... | @@ -16,10 +18,11 @@ import { | ... | @@ -16,10 +18,11 @@ import { |
| 16 | type ResolvedKeybindingsConfig, | 18 | type ResolvedKeybindingsConfig, |
| 17 | type ProviderApprovalDecision, | 19 | type ProviderApprovalDecision, |
| 18 | type ServerProviderStatus, | 20 | type ServerProviderStatus, |
| | 21 | type ProviderKind, |
| 19 | type ThreadId, | 22 | type ThreadId, |
| 20 | type TurnId, | 23 | type TurnId, |
| 21 | normalizeModelSlug, | 24 | normalizeModelSlug, |
| 22 | resolveModelSlug, | 25 | resolveModelSlugForProvider, |
| 23 | OrchestrationThreadActivity, | 26 | OrchestrationThreadActivity, |
| 24 | } from "@t3tools/contracts"; | 27 | } from "@t3tools/contracts"; |
| 25 | import { memo, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react"; | 28 | import { memo, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react"; |
| ... | @@ -49,6 +52,7 @@ import { | ... | @@ -49,6 +52,7 @@ import { |
| 49 | derivePhase, | 52 | derivePhase, |
| 50 | deriveTimelineEntries, | 53 | deriveTimelineEntries, |
| 51 | type PendingApproval, | 54 | type PendingApproval, |
| | 55 | PROVIDER_OPTIONS, |
| 52 | deriveWorkLogEntries, | 56 | deriveWorkLogEntries, |
| 53 | hasToolActivityForTurn, | 57 | hasToolActivityForTurn, |
| 54 | isLatestTurnSettled, | 58 | isLatestTurnSettled, |
| ... | @@ -509,6 +513,9 @@ export default function ChatView({ threadId }: ChatViewProps) { | ... | @@ -509,6 +513,9 @@ export default function ChatView({ threadId }: ChatViewProps) { |
| 509 | const [sendPhase, setSendPhase] = useState<SendPhase>("idle"); | 513 | const [sendPhase, setSendPhase] = useState<SendPhase>("idle"); |
| 510 | const [isConnecting, _setIsConnecting] = useState(false); | 514 | const [isConnecting, _setIsConnecting] = useState(false); |
| 511 | const [isRevertingCheckpoint, setIsRevertingCheckpoint] = useState(false); | 515 | const [isRevertingCheckpoint, setIsRevertingCheckpoint] = useState(false); |
| | 516 | const [selectedProviderByThread, setSelectedProviderByThread] = useState< |
| | 517 | Partial<Record<ThreadId, ProviderKind>> |
| | 518 | >({}); |
| 512 | const [isSwitchingRuntimeMode, setIsSwitchingRuntimeMode] = useState(false); | 519 | const [isSwitchingRuntimeMode, setIsSwitchingRuntimeMode] = useState(false); |
| 513 | const [respondingRequestIds, setRespondingRequestIds] = useState<ApprovalRequestId[]>([]); | 520 | const [respondingRequestIds, setRespondingRequestIds] = useState<ApprovalRequestId[]>([]); |
| 514 | const [expandedWorkGroups, setExpandedWorkGroups] = useState<Record<string, boolean>>({}); | 521 | const [expandedWorkGroups, setExpandedWorkGroups] = useState<Record<string, boolean>>({}); |
| ... | @@ -632,20 +639,45 @@ export default function ChatView({ threadId }: ChatViewProps) { | ... | @@ -632,20 +639,45 @@ export default function ChatView({ threadId }: ChatViewProps) { |
| 632 | markThreadVisited, | 639 | markThreadVisited, |
| 633 | ]); | 640 | ]); |
| 634 | | 641 | |
| 635 | const baseThreadModel = resolveModelSlug( | 642 | const sessionProvider = activeThread?.session?.provider ?? null; |
| 636 | activeThread?.model ?? activeProject?.model ?? DEFAULT_MODEL, | 643 | const selectedProvider: ProviderKind = |
| | 644 | (activeThread?.id ? selectedProviderByThread[activeThread.id] : undefined) ?? |
| | 645 | sessionProvider ?? |
| | 646 | "codex"; |
| | 647 | const baseThreadModel = resolveModelSlugForProvider( |
| | 648 | selectedProvider, |
| | 649 | activeThread?.model ?? activeProject?.model ?? getDefaultModel(selectedProvider) ?? DEFAULT_MODEL, |
| 637 | ); | 650 | ); |
| 638 | const selectedModel = resolveModelSlug(composerDraft.model ?? baseThreadModel); | 651 | const selectedModel = resolveModelSlugForProvider(selectedProvider, composerDraft.model ?? baseThreadModel); |
| 639 | const selectedEffort = composerDraft.effort ?? DEFAULT_REASONING; | 652 | const selectedEffort = composerDraft.effort ?? DEFAULT_REASONING; |
| 640 | const modelOptions = useMemo( | 653 | const modelOptions = useMemo( |
| 641 | () => getAppModelOptions(settings.customCodexModels, selectedModel), | | |
| 642 | [selectedModel, settings.customCodexModels], | | |
| 643 | ); | | |
| 644 | const slashModelOptions = useMemo( | | |
| 645 | () => | 654 | () => |
| 646 | getSlashModelOptions(settings.customCodexModels, composerTrigger?.query ?? "", selectedModel), | 655 | selectedProvider === "codex" |
| 647 | [composerTrigger?.query, selectedModel, settings.customCodexModels], | 656 | ? getAppModelOptions(settings.customCodexModels, selectedModel) |
| 648 | ); | 657 | : getModelOptions(selectedProvider), |
| | 658 | [selectedModel, selectedProvider, settings.customCodexModels], |
| | 659 | ); |
| | 660 | const slashModelOptions = useMemo(() => { |
| | 661 | if (selectedProvider === "codex") { |
| | 662 | return getSlashModelOptions(settings.customCodexModels, composerTrigger?.query ?? "", selectedModel); |
| | 663 | } |
| | 664 | const query = (composerTrigger?.query ?? "").trim().toLowerCase(); |
| | 665 | if (!query) { |
| | 666 | return modelOptions; |
| | 667 | } |
| | 668 | return modelOptions.filter(({ slug, name }) => { |
| | 669 | const searchSlug = slug.toLowerCase(); |
| | 670 | const searchName = name.toLowerCase(); |
| | 671 | return searchSlug.includes(query) || searchName.includes(query); |
| | 672 | }); |
| | 673 | }, [composerTrigger?.query, modelOptions, selectedModel, selectedProvider, settings.customCodexModels]); |
| | 674 | useEffect(() => { |
| | 675 | if (!activeThread?.id || !sessionProvider) return; |
| | 676 | setSelectedProviderByThread((existing) => { |
| | 677 | if (existing[activeThread.id]) return existing; |
| | 678 | return { ...existing, [activeThread.id]: sessionProvider }; |
| | 679 | }); |
| | 680 | }, [activeThread?.id, sessionProvider]); |
| 649 | const phase = derivePhase(activeThread?.session ?? null); | 681 | const phase = derivePhase(activeThread?.session ?? null); |
| 650 | const isSendBusy = sendPhase !== "idle"; | 682 | const isSendBusy = sendPhase !== "idle"; |
| 651 | const isPreparingWorktree = sendPhase === "preparing-worktree"; | 683 | const isPreparingWorktree = sendPhase === "preparing-worktree"; |
| ... | @@ -2127,6 +2159,7 @@ export default function ChatView({ threadId }: ChatViewProps) { | ... | @@ -2127,6 +2159,7 @@ export default function ChatView({ threadId }: ChatViewProps) { |
| 2127 | attachments: turnAttachments, | 2159 | attachments: turnAttachments, |
| 2128 | }, | 2160 | }, |
| 2129 | model: selectedModel || undefined, | 2161 | model: selectedModel || undefined, |
| | 2162 | provider: selectedProvider, |
| 2130 | effort: selectedEffort || undefined, | 2163 | effort: selectedEffort || undefined, |
| 2131 | assistantDeliveryMode: settings.enableAssistantStreaming ? "streaming" : "buffered", | 2164 | assistantDeliveryMode: settings.enableAssistantStreaming ? "streaming" : "buffered", |
| 2132 | approvalPolicy, | 2165 | approvalPolicy, |
| ... | @@ -2225,19 +2258,23 @@ export default function ChatView({ threadId }: ChatViewProps) { | ... | @@ -2225,19 +2258,23 @@ export default function ChatView({ threadId }: ChatViewProps) { |
| 2225 | type: "thread.meta.update", | 2258 | type: "thread.meta.update", |
| 2226 | commandId: newCommandId(), | 2259 | commandId: newCommandId(), |
| 2227 | threadId: activeThread.id, | 2260 | threadId: activeThread.id, |
| 2228 | model: resolvedModel, | 2261 | model: resolveModelSlugForProvider(selectedProvider, model), |
| 2229 | }); | 2262 | }); |
| 2230 | } | 2263 | } |
| 2231 | scheduleComposerFocus(); | 2264 | scheduleComposerFocus(); |
| 2232 | }, | 2265 | }, |
| 2233 | [ | 2266 | [activeThread, scheduleComposerFocus, selectedProvider], |
| 2234 | activeThread, | 2267 | ); |
| 2235 | baseThreadModel, | 2268 | const onProviderSelect = useCallback( |
| 2236 | isServerThread, | 2269 | (provider: ProviderKind) => { |
| 2237 | scheduleComposerFocus, | 2270 | if (!activeThread) return; |
| 2238 | setComposerDraftModel, | 2271 | setSelectedProviderByThread((existing) => ({ |
| 2239 | threadId, | 2272 | ...existing, |
| 2240 | ], | 2273 | [activeThread.id]: provider, |
| | 2274 | })); |
| | 2275 | scheduleComposerFocus(); |
| | 2276 | }, |
| | 2277 | [activeThread, scheduleComposerFocus], |
| 2241 | ); | 2278 | ); |
| 2242 | const onEffortSelect = useCallback( | 2279 | const onEffortSelect = useCallback( |
| 2243 | (effort: ReasoningEffort) => { | 2280 | (effort: ReasoningEffort) => { |
| ... | @@ -2675,6 +2712,12 @@ export default function ChatView({ threadId }: ChatViewProps) { | ... | @@ -2675,6 +2712,12 @@ export default function ChatView({ threadId }: ChatViewProps) { |
| 2675 | {/* Bottom toolbar */} | 2712 | {/* Bottom toolbar */} |
| 2676 | <div className="flex flex-wrap items-center justify-between gap-2 px-2.5 pb-2.5 sm:flex-nowrap sm:gap-0 sm:px-3 sm:pb-3"> | 2713 | <div className="flex flex-wrap items-center justify-between gap-2 px-2.5 pb-2.5 sm:flex-nowrap sm:gap-0 sm:px-3 sm:pb-3"> |
| 2677 | <div className="flex min-w-0 flex-1 items-center gap-1 overflow-x-auto [scrollbar-width:none] [&::-webkit-scrollbar]:hidden sm:min-w-max sm:overflow-visible"> | 2714 | <div className="flex min-w-0 flex-1 items-center gap-1 overflow-x-auto [scrollbar-width:none] [&::-webkit-scrollbar]:hidden sm:min-w-max sm:overflow-visible"> |
| | 2715 | {/* Model picker */} |
| | 2716 | <ProviderPicker provider={selectedProvider} onProviderChange={onProviderSelect} /> |
| | 2717 | |
| | 2718 | {/* Divider */} |
| | 2719 | <Separator orientation="vertical" className="mx-0.5 hidden h-4 sm:block" /> |
| | 2720 | |
| 2678 | {/* Model picker */} | 2721 | {/* Model picker */} |
| 2679 | <ModelPicker | 2722 | <ModelPicker |
| 2680 | model={selectedModel} | 2723 | model={selectedModel} |
| ... | @@ -3824,9 +3867,9 @@ const MessagesTimeline = memo(function MessagesTimeline({ | ... | @@ -3824,9 +3867,9 @@ const MessagesTimeline = memo(function MessagesTimeline({ |
| 3824 | }); | 3867 | }); |
| 3825 | | 3868 | |
| 3826 | const ModelPicker = memo(function ModelPicker(props: { | 3869 | const ModelPicker = memo(function ModelPicker(props: { |
| 3827 | model: string; | 3870 | model: ModelSlug; |
| 3828 | options: ReadonlyArray<{ slug: string; name: string }>; | 3871 | options: ReadonlyArray<{ readonly slug: ModelSlug; readonly name: string }>; |
| 3829 | onModelChange: (model: string) => void; | 3872 | onModelChange: (model: ModelSlug) => void; |
| 3830 | }) { | 3873 | }) { |
| 3831 | return ( | 3874 | return ( |
| 3832 | <Select | 3875 | <Select |
| ... | @@ -3848,6 +3891,35 @@ const ModelPicker = memo(function ModelPicker(props: { | ... | @@ -3848,6 +3891,35 @@ const ModelPicker = memo(function ModelPicker(props: { |
| 3848 | ); | 3891 | ); |
| 3849 | }); | 3892 | }); |
| 3850 | | 3893 | |
| | 3894 | const ProviderPicker = memo(function ProviderPicker(props: { |
| | 3895 | provider: ProviderKind; |
| | 3896 | onProviderChange: (provider: ProviderKind) => void; |
| | 3897 | }) { |
| | 3898 | return ( |
| | 3899 | <Select |
| | 3900 | items={PROVIDER_OPTIONS.filter((option) => option.available).map((option) => ({ |
| | 3901 | label: option.label, |
| | 3902 | value: option.value, |
| | 3903 | }))} |
| | 3904 | value={props.provider} |
| | 3905 | onValueChange={(value) => |
| | 3906 | value === "codex" || value === "claudeCode" ? props.onProviderChange(value) : undefined |
| | 3907 | } |
| | 3908 | > |
| | 3909 | <SelectTrigger size="sm" variant="ghost"> |
| | 3910 | <SelectValue /> |
| | 3911 | </SelectTrigger> |
| | 3912 | <SelectPopup alignItemWithTrigger={false}> |
| | 3913 | {PROVIDER_OPTIONS.filter((option) => option.available).map(({ value, label }) => ( |
| | 3914 | <SelectItem key={value} value={value}> |
| | 3915 | {label} |
| | 3916 | </SelectItem> |
| | 3917 | ))} |
| | 3918 | </SelectPopup> |
| | 3919 | </Select> |
| | 3920 | ); |
| | 3921 | }); |
| | 3922 | |
| 3851 | const ReasoningEffortPicker = memo(function ReasoningEffortPicker(props: { | 3923 | const ReasoningEffortPicker = memo(function ReasoningEffortPicker(props: { |
| 3852 | effort: ReasoningEffort; | 3924 | effort: ReasoningEffort; |
| 3853 | onEffortChange: (effort: ReasoningEffort) => void; | 3925 | onEffortChange: (effort: ReasoningEffort) => void; |