Skip to content

Commit 575e9e2

Browse files
committed
Share Codex usage provider resolution
1 parent 7c036a0 commit 575e9e2

4 files changed

Lines changed: 112 additions & 107 deletions

File tree

apps/web/src/components/BranchToolbar.tsx

Lines changed: 6 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,5 @@
11
import { scopeProjectRef, scopeThreadRef } from "@t3tools/client-runtime";
2-
import {
3-
ProviderDriverKind,
4-
type EnvironmentId,
5-
type ServerProvider,
6-
type ThreadId,
7-
} from "@t3tools/contracts";
2+
import { type EnvironmentId, type ProviderInstanceId, type ThreadId } from "@t3tools/contracts";
83
import type { CodexUsageIndicatorMode } from "@t3tools/contracts/settings";
94
import {
105
ChevronDownIcon,
@@ -44,11 +39,6 @@ import {
4439
MenuTrigger,
4540
} from "./ui/menu";
4641
import { Separator } from "./ui/separator";
47-
import {
48-
deriveProviderInstanceEntries,
49-
resolveProviderDriverKindForInstanceSelection,
50-
sortProviderInstanceEntries,
51-
} from "../providerInstances";
5242

5343
interface BranchToolbarProps {
5444
environmentId: EnvironmentId;
@@ -63,8 +53,8 @@ interface BranchToolbarProps {
6353
onComposerFocusRequest?: () => void;
6454
availableEnvironments?: readonly EnvironmentOption[];
6555
onEnvironmentChange?: (environmentId: EnvironmentId) => void;
66-
providerStatuses: readonly ServerProvider[];
6756
codexUsageIndicatorMode: CodexUsageIndicatorMode;
57+
codexUsageInstanceId: ProviderInstanceId | null;
6858
}
6959

7060
interface MobileRunContextSelectorProps {
@@ -216,8 +206,8 @@ export const BranchToolbar = memo(function BranchToolbar({
216206
onComposerFocusRequest,
217207
availableEnvironments,
218208
onEnvironmentChange,
219-
providerStatuses,
220209
codexUsageIndicatorMode,
210+
codexUsageInstanceId,
221211
}: BranchToolbarProps) {
222212
const threadRef = useMemo(
223213
() => scopeThreadRef(environmentId, threadId),
@@ -228,9 +218,6 @@ export const BranchToolbar = memo(function BranchToolbar({
228218
const draftThread = useComposerDraftStore((store) =>
229219
draftId ? store.getDraftSession(draftId) : store.getDraftThreadByRef(threadRef),
230220
);
231-
const composerDraft = useComposerDraftStore((store) =>
232-
store.getComposerDraft(draftId ?? threadRef),
233-
);
234221
const activeProjectRef = serverThread
235222
? scopeProjectRef(serverThread.environmentId, serverThread.projectId)
236223
: draftThread
@@ -255,30 +242,7 @@ export const BranchToolbar = memo(function BranchToolbar({
255242
const showEnvironmentPicker = Boolean(
256243
availableEnvironments && availableEnvironments.length > 1 && onEnvironmentChange,
257244
);
258-
const providerInstanceEntries = useMemo(
259-
() => sortProviderInstanceEntries(deriveProviderInstanceEntries(providerStatuses)),
260-
[providerStatuses],
261-
);
262-
const selectedInstanceId =
263-
composerDraft?.activeProvider ??
264-
serverThread?.session?.providerInstanceId ??
265-
serverThread?.modelSelection.instanceId ??
266-
activeProject?.defaultModelSelection?.instanceId ??
267-
null;
268-
const selectedProvider =
269-
resolveProviderDriverKindForInstanceSelection(
270-
providerInstanceEntries,
271-
providerStatuses,
272-
selectedInstanceId,
273-
) ?? ProviderDriverKind.make("codex");
274-
const selectedEntry = selectedInstanceId
275-
? providerInstanceEntries.find((entry) => entry.instanceId === selectedInstanceId)
276-
: providerInstanceEntries.find(
277-
(entry) => entry.driverKind === selectedProvider && entry.enabled,
278-
);
279-
const showCodexUsage =
280-
codexUsageIndicatorMode !== "off" &&
281-
selectedEntry?.driverKind === ProviderDriverKind.make("codex");
245+
const showCodexUsage = codexUsageIndicatorMode !== "off" && codexUsageInstanceId !== null;
282246
const isMobile = useIsMobile();
283247

284248
if (!hasActiveThread || !activeProject) return null;
@@ -316,11 +280,11 @@ export const BranchToolbar = memo(function BranchToolbar({
316280
activeWorktreePath={activeWorktreePath}
317281
onEnvModeChange={onEnvModeChange}
318282
/>
319-
{showCodexUsage && selectedEntry ? (
283+
{showCodexUsage ? (
320284
<>
321285
<Separator orientation="vertical" className="mx-0.5 h-3.5!" />
322286
<CodexUsageIndicator
323-
instanceId={selectedEntry.instanceId}
287+
instanceId={codexUsageInstanceId}
324288
mode={codexUsageIndicatorMode}
325289
/>
326290
</>

apps/web/src/components/ChatView.tsx

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,11 @@ import { newCommandId, newDraftId, newMessageId, newThreadId } from "~/lib/utils
118118
import { getProviderModelCapabilities, resolveSelectableProvider } from "../providerModels";
119119
import { useSettings } from "../hooks/useSettings";
120120
import { resolveAppModelSelectionForInstance } from "../modelSelection";
121+
import {
122+
deriveProviderInstanceEntries,
123+
resolveSelectedProviderInstanceId,
124+
sortProviderInstanceEntries,
125+
} from "../providerInstances";
121126
import { isTerminalFocused } from "../lib/terminalFocus";
122127
import { deriveLogicalProjectKeyFromSettings } from "../logicalProject";
123128
import {
@@ -1257,11 +1262,56 @@ export default function ChatView(props: ChatViewProps) {
12571262
versionMismatchServerLabel,
12581263
]);
12591264
const providerStatuses = serverConfig?.providers ?? EMPTY_PROVIDERS;
1265+
const providerInstanceEntries = useMemo(
1266+
() => sortProviderInstanceEntries(deriveProviderInstanceEntries(providerStatuses)),
1267+
[providerStatuses],
1268+
);
12601269
const unlockedSelectedProvider = resolveSelectableProvider(
12611270
providerStatuses,
12621271
selectedProviderByThreadId ?? threadProvider ?? ProviderDriverKind.make("codex"),
12631272
);
12641273
const selectedProvider: ProviderDriverKind = lockedProvider ?? unlockedSelectedProvider;
1274+
const lockedContinuationGroupKey = useMemo((): string | null => {
1275+
if (!lockedProvider || !activeThread) return null;
1276+
const lockedInstanceId =
1277+
activeThread.session?.providerInstanceId ?? activeThread.modelSelection.instanceId;
1278+
if (!lockedInstanceId) return null;
1279+
return (
1280+
providerInstanceEntries.find((entry) => entry.instanceId === lockedInstanceId)
1281+
?.continuationGroupKey ?? null
1282+
);
1283+
}, [activeThread, lockedProvider, providerInstanceEntries]);
1284+
const codexUsageInstanceId = useMemo(() => {
1285+
if (settings.codexUsageIndicatorMode === "off") return null;
1286+
const selectedInstanceId = resolveSelectedProviderInstanceId({
1287+
entries: providerInstanceEntries,
1288+
candidates: [
1289+
composerActiveProvider,
1290+
activeThread?.session?.providerInstanceId,
1291+
activeThread?.modelSelection.instanceId,
1292+
activeProject?.defaultModelSelection?.instanceId,
1293+
],
1294+
selectedProvider,
1295+
lockedProvider,
1296+
lockedContinuationGroupKey,
1297+
});
1298+
const selectedEntry = providerInstanceEntries.find(
1299+
(entry) => entry.instanceId === selectedInstanceId,
1300+
);
1301+
return selectedEntry?.driverKind === ProviderDriverKind.make("codex")
1302+
? selectedEntry.instanceId
1303+
: null;
1304+
}, [
1305+
activeProject?.defaultModelSelection?.instanceId,
1306+
activeThread?.modelSelection.instanceId,
1307+
activeThread?.session?.providerInstanceId,
1308+
composerActiveProvider,
1309+
lockedContinuationGroupKey,
1310+
lockedProvider,
1311+
providerInstanceEntries,
1312+
selectedProvider,
1313+
settings.codexUsageIndicatorMode,
1314+
]);
12651315
const phase = derivePhase(activeThread?.session ?? null);
12661316
const threadActivities = activeThread?.activities ?? EMPTY_ACTIVITIES;
12671317
const workLogEntries = useMemo(
@@ -3642,6 +3692,7 @@ export default function ChatView(props: ChatViewProps) {
36423692
interactionMode={interactionMode}
36433693
lockedProvider={lockedProvider}
36443694
providerStatuses={providerStatuses as ServerProvider[]}
3695+
providerInstanceEntries={providerInstanceEntries}
36453696
activeProjectDefaultModelSelection={activeProject?.defaultModelSelection}
36463697
activeThreadModelSelection={activeThread?.modelSelection}
36473698
activeThreadActivities={activeThread?.activities}
@@ -3685,8 +3736,8 @@ export default function ChatView(props: ChatViewProps) {
36853736
threadId={activeThread.id}
36863737
{...(routeKind === "draft" && draftId ? { draftId } : {})}
36873738
onEnvModeChange={onEnvModeChange}
3688-
providerStatuses={providerStatuses as ServerProvider[]}
36893739
codexUsageIndicatorMode={settings.codexUsageIndicatorMode}
3740+
codexUsageInstanceId={codexUsageInstanceId}
36903741
{...(canOverrideServerThreadEnvMode ? { effectiveEnvModeOverride: envMode } : {})}
36913742
{...(canOverrideServerThreadEnvMode
36923743
? {

apps/web/src/components/chat/ChatComposer.tsx

Lines changed: 15 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -98,9 +98,8 @@ import {
9898
import { proposedPlanTitle } from "../../proposedPlan";
9999
import { getProviderInteractionModeToggle } from "../../providerModels";
100100
import {
101-
deriveProviderInstanceEntries,
102101
resolveProviderDriverKindForInstanceSelection,
103-
sortProviderInstanceEntries,
102+
resolveSelectedProviderInstanceId,
104103
type ProviderInstanceEntry,
105104
} from "../../providerInstances";
106105
import { type AppModelOption, getAppModelOptionsForInstance } from "../../modelSelection";
@@ -432,6 +431,7 @@ export interface ChatComposerProps {
432431
// Provider / model
433432
lockedProvider: ProviderDriverKind | null;
434433
providerStatuses: ServerProvider[];
434+
providerInstanceEntries: ReadonlyArray<ProviderInstanceEntry>;
435435
activeProjectDefaultModelSelection: ModelSelection | null | undefined;
436436
activeThreadModelSelection: ModelSelection | null | undefined;
437437

@@ -526,6 +526,7 @@ export const ChatComposer = memo(
526526
interactionMode,
527527
lockedProvider,
528528
providerStatuses,
529+
providerInstanceEntries,
529530
activeProjectDefaultModelSelection,
530531
activeThreadModelSelection,
531532
activeThreadActivities,
@@ -591,13 +592,6 @@ export const ChatComposer = memo(
591592
// ------------------------------------------------------------------
592593
// Model state
593594
// ------------------------------------------------------------------
594-
// Instance-aware projection of the wire provider list. One entry per
595-
// configured instance (default built-in + any custom `providerInstances.*`),
596-
// sorted default-first per driver kind for a stable picker order.
597-
const providerInstanceEntries = useMemo<ReadonlyArray<ProviderInstanceEntry>>(
598-
() => sortProviderInstanceEntries(deriveProviderInstanceEntries(providerStatuses)),
599-
[providerStatuses],
600-
);
601595
const selectedProviderByThreadId = composerDraft.activeProvider ?? null;
602596
const threadProvider =
603597
activeThread?.session?.providerInstanceId ??
@@ -629,66 +623,24 @@ export const ChatComposer = memo(
629623
providerInstanceEntries,
630624
]);
631625

632-
// Resolve which configured instance the composer is currently targeting.
633-
// Priority:
634-
// 1. The composer draft's `activeProvider` — the user's unsaved pick
635-
// from the model picker (must win, otherwise the UI appears to
636-
// ignore picker selections).
637-
// 2. Thread's persisted instance id (server-side saved selection).
638-
// 3. Project default's instance id.
639-
// 4. First enabled entry matching the current driver kind.
640-
// 5. First enabled entry overall / default instance for the kind.
641-
//
642626
const selectedInstanceId = useMemo<ProviderInstanceId>(() => {
643-
const candidates: Array<string | null | undefined> = [
644-
composerDraft.activeProvider,
645-
activeThread?.session?.providerInstanceId,
646-
activeThreadModelSelection?.instanceId,
647-
activeProjectDefaultModelSelection?.instanceId,
648-
];
649-
for (const candidate of candidates) {
650-
if (!candidate) continue;
651-
const match = providerInstanceEntries.find(
652-
(entry) => entry.instanceId === candidate && entry.enabled,
653-
);
654-
if (match) {
655-
// When locked to a specific driver kind, ignore persisted instance
656-
// ids from a different kind or continuation group.
657-
if (lockedProvider && match.driverKind !== lockedProvider) continue;
658-
if (
659-
lockedContinuationGroupKey &&
660-
match.continuationGroupKey !== lockedContinuationGroupKey
661-
) {
662-
continue;
663-
}
664-
return match.instanceId;
665-
}
666-
}
667-
if (explicitSelectedInstanceId) {
668-
return ProviderInstanceId.make(explicitSelectedInstanceId);
669-
}
670-
const byKind = providerInstanceEntries.find(
671-
(entry) =>
672-
entry.enabled &&
673-
entry.driverKind === selectedProvider &&
674-
(!lockedContinuationGroupKey ||
675-
entry.continuationGroupKey === lockedContinuationGroupKey),
676-
);
677-
if (byKind) return byKind.instanceId;
678-
const anyEnabled = providerInstanceEntries.find((entry) => entry.enabled);
679-
return (
680-
anyEnabled?.instanceId ??
681-
providerInstanceEntries[0]?.instanceId ??
682-
activeThreadModelSelection?.instanceId ??
683-
activeProjectDefaultModelSelection?.instanceId ??
684-
ProviderInstanceId.make("codex")
685-
);
627+
return resolveSelectedProviderInstanceId({
628+
entries: providerInstanceEntries,
629+
candidates: [
630+
composerDraft.activeProvider,
631+
activeThread?.session?.providerInstanceId,
632+
activeThreadModelSelection?.instanceId,
633+
activeProjectDefaultModelSelection?.instanceId,
634+
],
635+
selectedProvider,
636+
lockedProvider,
637+
lockedContinuationGroupKey,
638+
});
686639
}, [
687640
activeProjectDefaultModelSelection?.instanceId,
688641
activeThread?.session?.providerInstanceId,
689642
activeThreadModelSelection?.instanceId,
690643
composerDraft.activeProvider,
691-
explicitSelectedInstanceId,
692644
lockedContinuationGroupKey,
693645
lockedProvider,
694646
providerInstanceEntries,

apps/web/src/providerInstances.ts

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import {
1616
defaultInstanceIdForDriver,
1717
PROVIDER_DISPLAY_NAMES,
1818
type ProviderDriverKind,
19-
type ProviderInstanceId,
19+
ProviderInstanceId,
2020
type ServerProvider,
2121
type ServerProviderModel,
2222
type ServerProviderState,
@@ -184,6 +184,44 @@ export function sortProviderInstanceEntries(
184184
return sorted;
185185
}
186186

187+
export function resolveSelectedProviderInstanceId(input: {
188+
entries: ReadonlyArray<ProviderInstanceEntry>;
189+
candidates: ReadonlyArray<ProviderInstanceId | null | undefined>;
190+
selectedProvider: ProviderDriverKind;
191+
lockedProvider?: ProviderDriverKind | null | undefined;
192+
lockedContinuationGroupKey?: string | null | undefined;
193+
}): ProviderInstanceId {
194+
const lockedProvider = input.lockedProvider ?? null;
195+
const lockedContinuationGroupKey = input.lockedContinuationGroupKey ?? null;
196+
const explicitSelection = input.candidates.find((candidate) => candidate != null);
197+
198+
for (const candidate of input.candidates) {
199+
if (!candidate) continue;
200+
const match = input.entries.find((entry) => entry.instanceId === candidate && entry.enabled);
201+
if (!match) continue;
202+
if (lockedProvider && match.driverKind !== lockedProvider) continue;
203+
if (lockedContinuationGroupKey && match.continuationGroupKey !== lockedContinuationGroupKey) {
204+
continue;
205+
}
206+
return match.instanceId;
207+
}
208+
209+
if (explicitSelection) {
210+
return ProviderInstanceId.make(explicitSelection);
211+
}
212+
213+
const byKind = input.entries.find(
214+
(entry) =>
215+
entry.enabled &&
216+
entry.driverKind === input.selectedProvider &&
217+
(!lockedContinuationGroupKey || entry.continuationGroupKey === lockedContinuationGroupKey),
218+
);
219+
if (byKind) return byKind.instanceId;
220+
221+
const anyEnabled = input.entries.find((entry) => entry.enabled);
222+
return anyEnabled?.instanceId ?? input.entries[0]?.instanceId ?? ProviderInstanceId.make("codex");
223+
}
224+
187225
/**
188226
* Look up a single instance entry by exact `instanceId`. Missing snapshots
189227
* are not inferred from driver kind in UI routing code.

0 commit comments

Comments
 (0)