From 0dfabf33143759ee89adcb615a6732af2146e44a Mon Sep 17 00:00:00 2001 From: Rakshith Bhat Date: Fri, 9 Oct 2026 12:23:59 +0530 Subject: [PATCH 1/6] feat(web): subagent rows show model, effort, and fast mode Co-authored-by: Claude Opus 5.5 --- .../src/features/threads/SubagentRow.tsx | 21 +- .../threads/thread-subagent-group.tsx | 1 + .../Orchestrator.control-reads.test.ts | 57 ++++++ .../src/orchestration-v2/Orchestrator.ts | 2 + .../chat/SubagentTooltipContent.tsx | 63 +----- .../src/components/chat/V2LifecycleRow.tsx | 39 +++- .../src/state/subagentDisplay.test.ts | 191 +++++++++++++++++- .../src/state/subagentDisplay.ts | 77 ++++++- 8 files changed, 388 insertions(+), 63 deletions(-) diff --git a/apps/mobile/src/features/threads/SubagentRow.tsx b/apps/mobile/src/features/threads/SubagentRow.tsx index 14adfdfa411b6..5b11150991f93 100644 --- a/apps/mobile/src/features/threads/SubagentRow.tsx +++ b/apps/mobile/src/features/threads/SubagentRow.tsx @@ -1,6 +1,9 @@ import { scopeProjectRef, scopeThreadRef } from "@t3tools/client-runtime/environment"; import { resolveProviderInstanceDisplayName } from "@t3tools/client-runtime/state/provider-instance-display"; -import { resolveSubagentMetadata } from "@t3tools/client-runtime/state/subagent-display"; +import { + resolveSubagentMetadata, + resolveSubagentModelTraits, +} from "@t3tools/client-runtime/state/subagent-display"; import type { EnvironmentId, OrchestrationV2Subagent } from "@t3tools/contracts"; import type { ReactNode } from "react"; import { View } from "react-native"; @@ -19,6 +22,8 @@ type SubagentRowSubagent = Pick< | "threadId" | "childThreadId" | "model" + | "modelSelection" + | "origin" | "driver" | "providerInstanceId" | "title" @@ -118,6 +123,18 @@ function SubagentMetadata(props: { parentProject, childProject, }); + const { effortLabel, speed } = resolveSubagentModelTraits({ + model: subagent.model, + providerInstanceId: subagent.providerInstanceId, + origin: subagent.origin, + modelSelection: subagent.modelSelection, + childThread: child, + provider, + }); + const traits = [ + effortLabel, + speed === "ultrafast" ? "Ultrafast" : speed === "fast" ? "Fast" : null, + ].filter((trait) => trait !== null); return ( {provider ? `${resolveProviderInstanceDisplayName(provider)} · ` : ""} - {modelLabel} + {[modelLabel, ...traits].join(" · ")} {workspace.map(({ label, value }) => ( // The row reads this label in place of the icon. collapsable keeps the diff --git a/apps/mobile/src/features/threads/thread-subagent-group.tsx b/apps/mobile/src/features/threads/thread-subagent-group.tsx index aacc2c07353f1..66c32c7c12daf 100644 --- a/apps/mobile/src/features/threads/thread-subagent-group.tsx +++ b/apps/mobile/src/features/threads/thread-subagent-group.tsx @@ -89,6 +89,7 @@ export function ThreadSubagentGroup(props: { result: live?.result ?? item.result, progress: live?.progress ?? item.progress, model: live?.model ?? null, + ...(live?.modelSelection ? { modelSelection: live.modelSelection } : {}), }; }); const grouped = agents.length > 1; diff --git a/apps/server/src/orchestration-v2/Orchestrator.control-reads.test.ts b/apps/server/src/orchestration-v2/Orchestrator.control-reads.test.ts index 76a4bd8c1bece..feed5ba55b8b6 100644 --- a/apps/server/src/orchestration-v2/Orchestrator.control-reads.test.ts +++ b/apps/server/src/orchestration-v2/Orchestrator.control-reads.test.ts @@ -624,3 +624,60 @@ it.effect("keeps delegated child pull-request links independent of the parent", assert.deepEqual(parentAfterChildLink.thread.pullRequests, parent.thread.pullRequests); }).pipe(Effect.provide(layerTest)), ); + +it.effect("records the selection a delegated task was launched with on its subagent", () => + Effect.gen(function* () { + const orchestrator = yield* Orchestrator.OrchestratorV2; + const projections = yield* ProjectionStore.ProjectionStoreV2; + const parentThreadId = ThreadId.make("thread:parent-selection"); + yield* orchestrator.dispatch({ + type: "thread.create", + commandId: CommandId.make("create-parent-selection"), + threadId: parentThreadId, + projectId: ProjectId.make("project:parent-selection"), + title: "Parent delegating at a chosen speed", + modelSelection, + runtimeMode: "full-access", + interactionMode: "default", + branch: null, + worktreePath: null, + createdBy: "user", + creationSource: "web", + }); + yield* orchestrator.dispatch({ + type: "message.dispatch", + commandId: CommandId.make("start-parent-selection"), + threadId: parentThreadId, + messageId: MessageId.make("message:parent-selection"), + text: "Delegate a review", + attachments: [], + dispatchMode: { type: "start_immediately" }, + createdBy: "user", + creationSource: "web", + }); + const parentRun = (yield* projections.getThreadProjection(parentThreadId)).runs[0]!; + const delegatedSelection = { + ...modelSelection, + options: [ + { id: "reasoningEffort", value: "high" }, + { id: "serviceTier", value: "priority" }, + ], + }; + yield* orchestrator.dispatch({ + type: "delegated_task.request", + commandId: CommandId.make("delegate-parent-selection"), + parentThreadId, + parentRunId: parentRun.id, + parentNodeId: parentRun.rootNodeId!, + task: "Review the changes", + modelSelection: delegatedSelection, + runtimeMode: "full-access", + interactionMode: "default", + createdBy: "agent", + creationSource: "mcp", + }); + + const parent = yield* projections.getThreadProjection(parentThreadId); + assert.deepEqual(parent.subagents[0]?.modelSelection, delegatedSelection); + }).pipe(Effect.provide(layerTest)), +); diff --git a/apps/server/src/orchestration-v2/Orchestrator.ts b/apps/server/src/orchestration-v2/Orchestrator.ts index c85f11c568350..77953383fb71f 100644 --- a/apps/server/src/orchestration-v2/Orchestrator.ts +++ b/apps/server/src/orchestration-v2/Orchestrator.ts @@ -6583,6 +6583,8 @@ const makeOrchestrator = Effect.fn("orchestrationV2.Orchestrator.layer")(functio prompt: command.task, title: command.title ?? null, model: command.modelSelection.model, + // A snapshot: the child thread's own selection can change on later turns. + modelSelection: command.modelSelection, ...(command.completionWake === undefined ? {} : { completionWake: command.completionWake }), status: "running", result: null, diff --git a/apps/web/src/components/chat/SubagentTooltipContent.tsx b/apps/web/src/components/chat/SubagentTooltipContent.tsx index 9bd96292cf9c1..bf49c53e25ee6 100644 --- a/apps/web/src/components/chat/SubagentTooltipContent.tsx +++ b/apps/web/src/components/chat/SubagentTooltipContent.tsx @@ -9,10 +9,10 @@ import type { } from "@t3tools/contracts"; import { resolveSubagentMetadata, + resolveSubagentModelTraits, subagentDetailPreview, } from "@t3tools/client-runtime/state/subagent-display"; -import { getModelSelectionStringOptionValue, resolveSelectableModel } from "@t3tools/shared/model"; -import { getTraitsSpeedDisplay, TraitsSpeedIcon } from "./TraitsSpeed"; +import { TraitsSpeedIcon } from "./TraitsSpeed"; import type { ReactNode } from "react"; import { BotIcon, @@ -53,59 +53,8 @@ export function SubagentTooltipContent(props: { }) { const { modelLabel, workspace: metadata } = resolveSubagentMetadata(props); const preview = subagentDetailPreview(props); - const model = props.model?.trim(); - const modelSlug = props.provider - ? resolveSelectableModel(props.provider.driver, model, props.provider.models) - : model; - const providerModel = props.provider?.models.find((candidate) => candidate.slug === modelSlug); - const childSelection = - props.modelSelection ?? - (props.origin === "app_owned" ? props.childThread?.modelSelection : undefined); - const childModel = props.provider - ? (resolveSelectableModel( - props.provider.driver, - childSelection?.model, - props.provider.models, - ) ?? childSelection?.model.trim()) - : childSelection?.model.trim(); + const { effortLabel: effort, speed } = resolveSubagentModelTraits(props); const provider = props.provider; - const matchingSelection = - childModel === (modelSlug ?? model) && childSelection?.instanceId === props.providerInstanceId - ? childSelection - : undefined; - const effort = ["reasoningEffort", "effort", "reasoning", "variant"] - .map((id) => getModelSelectionStringOptionValue(matchingSelection, id)) - .find(Boolean); - const speed = provider - ? providerModel?.capabilities?.optionDescriptors - ?.map((descriptor) => { - const saved = matchingSelection?.options?.find((option) => option.id === descriptor.id); - if ( - descriptor.id === "fastMode" && - descriptor.type === "boolean" && - typeof saved?.value === "boolean" - ) { - return getTraitsSpeedDisplay(provider.driver, { - ...descriptor, - currentValue: saved.value, - }); - } - if ( - provider.driver === "codex" && - descriptor.id === "serviceTier" && - descriptor.type === "select" && - typeof saved?.value === "string" && - descriptor.options.some((option) => option.id === saved.value) - ) { - return getTraitsSpeedDisplay(provider.driver, { - ...descriptor, - currentValue: saved.value, - }); - } - return null; - }) - .find(Boolean) - : undefined; const driver = props.provider?.driver ?? props.driver; const entries = deriveProviderInstanceEntries(props.providers ?? []); const entry = entries.find((candidate) => candidate.instanceId === props.provider?.instanceId); @@ -141,11 +90,11 @@ export function SubagentTooltipContent(props: { {showInstanceBadge ? `${modelLabel} · ${entry.displayName}` : modelLabel} - {effort || speed?.speedIcon ? ( + {effort || speed ? ( {effort ? " · " : null} - {speed?.speedIcon && provider ? ( - + {speed && provider ? ( + ) : null} {effort} diff --git a/apps/web/src/components/chat/V2LifecycleRow.tsx b/apps/web/src/components/chat/V2LifecycleRow.tsx index fc90e90977a6c..f54166aa7994b 100644 --- a/apps/web/src/components/chat/V2LifecycleRow.tsx +++ b/apps/web/src/components/chat/V2LifecycleRow.tsx @@ -12,7 +12,11 @@ import * as DateTime from "effect/DateTime"; import { WorkLogRow } from "./WorkLog"; import { resolveHandoffEndpoints, type HandoffTimelineRun } from "@t3tools/client-runtime/handoff"; import { Fragment } from "react"; -import { formatSubagentDisplayTitle } from "@t3tools/client-runtime/state/subagent-display"; +import { + formatSubagentDisplayTitle, + resolveSubagentMetadata, + resolveSubagentModelTraits, +} from "@t3tools/client-runtime/state/subagent-display"; import { ProviderDriverKind, type OrchestrationV2Notification, @@ -42,6 +46,7 @@ import { getProviderInstanceEntry } from "../../providerInstances"; import { formatShortTimestamp } from "../../timestampFormat"; import { getTriggerDisplayModelName } from "./providerIconUtils"; import { ProviderInstanceIcon, providerTextColor } from "./ProviderInstanceIcon"; +import { TraitsSpeedIcon } from "./TraitsSpeed"; import { cn } from "~/lib/utils"; import { TimelineSystemDivider } from "./TimelineSystemDivider"; import { Button, InlineButton } from "../ui/button"; @@ -440,6 +445,23 @@ function SubagentTimelineLink(props: { startedAt: isoOrNull(agent?.startedAt ?? props.startedAt), completedAt: isoOrNull(agent?.completedAt ?? props.completedAt), }; + // Older app-owned records keep their selection only on the child thread. + const childModelSelection = useThreadShell( + props.origin === "app_owned" && threadId !== null && agent?.modelSelection === undefined + ? scopeThreadRef(props.parentRef.environmentId, threadId) + : null, + )?.source.modelSelection; + const model = agent?.model ?? null; + const modelLabel = + model === null ? null : resolveSubagentMetadata({ model, provider: props.provider }).modelLabel; + const { effortLabel, speed } = resolveSubagentModelTraits({ + model, + providerInstanceId: props.providerInstanceId, + origin: props.origin, + modelSelection: agent?.modelSelection, + childThread: childModelSelection ? { modelSelection: childModelSelection } : null, + provider: props.provider, + }); const content = ( <> + {modelLabel === null ? null : ( + // Same order as the hover card: speed icon, then effort. + + {modelLabel} + {effortLabel !== null || speed !== null ? ( + + {effortLabel !== null ? " · " : null} + {speed !== null ? ( + + ) : null} + {effortLabel} + + ) : null} + + )} {props.event ? props.event.timestamp : } diff --git a/packages/client-runtime/src/state/subagentDisplay.test.ts b/packages/client-runtime/src/state/subagentDisplay.test.ts index 3485cd4f2da2d..8605601246862 100644 --- a/packages/client-runtime/src/state/subagentDisplay.test.ts +++ b/packages/client-runtime/src/state/subagentDisplay.test.ts @@ -1,9 +1,10 @@ -import { ProjectId, ProviderDriverKind } from "@t3tools/contracts"; -import type { OrchestrationV2TurnItemStatus } from "@t3tools/contracts"; +import { ProjectId, ProviderDriverKind, ProviderInstanceId } from "@t3tools/contracts"; +import type { OrchestrationV2TurnItemStatus, ProviderOptionDescriptor } from "@t3tools/contracts"; import { describe, expect, it } from "vite-plus/test"; import { subagentGroupSummary, resolveSubagentMetadata, + resolveSubagentModelTraits, subagentDetailPreview, } from "./subagentDisplay.js"; @@ -164,3 +165,189 @@ describe("subagentDetailPreview", () => { expect(subagentDetailPreview({ status: "pending" })).toBeNull(); }); }); + +describe("resolveSubagentModelTraits", () => { + const claudeInstance = ProviderInstanceId.make("claude"); + const codexInstance = ProviderInstanceId.make("codex"); + const effort = { + id: "effort", + label: "Reasoning", + type: "select" as const, + options: [ + { id: "high", label: "High", isDefault: true }, + { id: "xhigh", label: "Extra High" }, + ], + }; + const claude = (extra: ReadonlyArray = []) => ({ + driver: ProviderDriverKind.make("claudeAgent"), + models: [ + { + slug: "claude-opus-4-7", + name: "Claude Opus 4.7", + aliases: ["claude-opus-4-7-20260101"], + isCustom: false, + capabilities: { + optionDescriptors: [ + effort, + { id: "fastMode", label: "Fast Mode", type: "boolean" as const }, + ...extra, + ], + }, + }, + ], + }); + const codex = (tiers: ReadonlyArray<{ id: string; label: string }>) => ({ + driver: ProviderDriverKind.make("codex"), + models: [ + { + slug: "gpt-6.1-sol", + name: "GPT-6.1-Sol", + isCustom: false, + capabilities: { + optionDescriptors: [ + { ...effort, id: "reasoningEffort" }, + { id: "serviceTier", label: "Service Tier", type: "select" as const, options: tiers }, + ], + }, + }, + ], + }); + const codexTiers = [ + { id: "default", label: "Standard" }, + { id: "priority", label: "Fast" }, + ]; + const selection = ( + instanceId: ProviderInstanceId, + model: string, + options: ReadonlyArray<{ id: string; value: string | boolean }>, + ) => ({ instanceId, model, options }); + + it("falls back to an app-owned child's selection but not a provider-native one's", () => { + const traits = (origin: "app_owned" | "provider_native") => + resolveSubagentModelTraits({ + model: "claude-opus-4-7-20260101", + providerInstanceId: claudeInstance, + origin, + childThread: { + modelSelection: selection(claudeInstance, "claude-opus-4-7", [ + { id: "effort", value: "xhigh" }, + { id: "fastMode", value: true }, + ]), + }, + provider: claude(), + }); + expect(traits("app_owned")).toEqual({ effortLabel: "Extra High", speed: "fast" }); + expect(traits("provider_native")).toEqual({ effortLabel: null, speed: null }); + }); + + it("prefers the subagent's own selection over its child thread's", () => { + expect( + resolveSubagentModelTraits({ + model: "claude-opus-4-7", + providerInstanceId: claudeInstance, + origin: "app_owned", + modelSelection: selection(claudeInstance, "claude-opus-4-7", [ + { id: "effort", value: "high" }, + { id: "fastMode", value: false }, + ]), + childThread: { + modelSelection: selection(claudeInstance, "claude-opus-4-7", [ + { id: "effort", value: "xhigh" }, + { id: "fastMode", value: true }, + ]), + }, + provider: claude(), + }), + ).toEqual({ effortLabel: "High", speed: null }); + }); + + it("calls out a Codex fast tier and leaves the standard tier unmarked", () => { + const traits = (tier: string) => + resolveSubagentModelTraits({ + model: "gpt-6.1-sol", + providerInstanceId: codexInstance, + origin: "provider_native", + modelSelection: selection(codexInstance, "gpt-6.1-sol", [ + { id: "reasoningEffort", value: "high" }, + { id: "serviceTier", value: tier }, + ]), + provider: codex(codexTiers), + }); + expect(traits("priority")).toEqual({ effortLabel: "High", speed: "fast" }); + expect(traits("default")).toEqual({ effortLabel: "High", speed: null }); + }); + + it("finds a fast tier that shares its id with the standard choice", () => { + expect( + resolveSubagentModelTraits({ + model: "gpt-6.1-sol", + providerInstanceId: codexInstance, + origin: "provider_native", + modelSelection: selection(codexInstance, "gpt-6.1-sol", [ + { id: "serviceTier", value: "default" }, + ]), + provider: codex([ + { id: "default", label: "Standard" }, + { id: "default", label: "Fast" }, + ]), + }).speed, + ).toBe("fast"); + }); + + it("claims no speed the provider does not run", () => { + // Claude ignores service tiers, and a tier the catalog does not list is unknown. + expect( + resolveSubagentModelTraits({ + model: "claude-opus-4-7", + providerInstanceId: claudeInstance, + origin: "provider_native", + modelSelection: selection(claudeInstance, "claude-opus-4-7", [ + { id: "serviceTier", value: "priority" }, + ]), + provider: claude([ + { id: "serviceTier", label: "Speed", type: "select", options: codexTiers }, + ]), + }).speed, + ).toBeNull(); + expect( + resolveSubagentModelTraits({ + model: "gpt-6.1-sol", + providerInstanceId: codexInstance, + origin: "provider_native", + modelSelection: selection(codexInstance, "gpt-6.1-sol", [ + { id: "serviceTier", value: "Fast" }, + ]), + provider: codex(codexTiers), + }).speed, + ).toBeNull(); + }); + + it("ignores a selection for another model or provider instance", () => { + const options = [ + { id: "effort", value: "xhigh" }, + { id: "fastMode", value: true }, + ]; + expect( + resolveSubagentModelTraits({ + model: "claude-haiku-4-5", + providerInstanceId: claudeInstance, + origin: "provider_native", + modelSelection: selection(claudeInstance, "claude-opus-4-7", options), + provider: claude(), + }), + ).toEqual({ effortLabel: null, speed: null }); + expect( + resolveSubagentModelTraits({ + model: "claude-opus-4-7", + providerInstanceId: claudeInstance, + origin: "provider_native", + modelSelection: selection( + ProviderInstanceId.make("claude-work"), + "claude-opus-4-7", + options, + ), + provider: claude(), + }), + ).toEqual({ effortLabel: null, speed: null }); + }); +}); diff --git a/packages/client-runtime/src/state/subagentDisplay.ts b/packages/client-runtime/src/state/subagentDisplay.ts index 480cc8e0c7ec2..b76a8e1310994 100644 --- a/packages/client-runtime/src/state/subagentDisplay.ts +++ b/packages/client-runtime/src/state/subagentDisplay.ts @@ -1,10 +1,18 @@ import type { + ModelSelection, + OrchestrationV2Subagent, OrchestrationV2TurnItemStatus, OrchestrationV2ThreadShell, OrchestrationProjectShell, + ProviderInstanceId, ServerProvider, } from "@t3tools/contracts"; -import { formatModelSlugName, resolveSelectableModel } from "@t3tools/shared/model"; +import { + formatModelSlugName, + getModelSelectionBooleanOptionValue, + getModelSelectionStringOptionValue, + resolveSelectableModel, +} from "@t3tools/shared/model"; import { fileBasename } from "@t3tools/shared/path"; import { isTerminalSubagentStatus } from "./subagentRuntime.ts"; @@ -117,6 +125,73 @@ export function resolveSubagentMetadata(input: { return { modelLabel, workspace }; } +const EFFORT_OPTION_IDS = ["reasoningEffort", "effort", "reasoning", "variant"] as const; + +/** + * The reasoning effort and speed a subagent runs at, named as the composer + * names them. Only a selection for the model the subagent reported counts, + * since another model's options say nothing about this one. Normal speed is + * null: only a faster mode is worth calling out. + */ +export function resolveSubagentModelTraits(input: { + readonly model: string | null; + readonly providerInstanceId: ProviderInstanceId; + readonly origin: OrchestrationV2Subagent["origin"]; + readonly modelSelection?: ModelSelection | undefined; + /** + * Older app-owned records carry no selection; their child thread's stands + * in. A provider-native child's selection is inherited, not reported, so it + * says nothing about how the provider ran it. + */ + readonly childThread?: Pick | null | undefined; + readonly provider?: Pick | null | undefined; +}): { readonly effortLabel: string | null; readonly speed: "fast" | "ultrafast" | null } { + const { provider } = input; + const resolve = (model: string | undefined) => + (provider ? resolveSelectableModel(provider.driver, model, provider.models) : null) ?? + model?.trim(); + const selection = + input.modelSelection ?? + (input.origin === "app_owned" ? input.childThread?.modelSelection : undefined); + const modelSlug = resolve(input.model ?? undefined); + if ( + selection === undefined || + modelSlug === undefined || + selection.instanceId !== input.providerInstanceId || + resolve(selection.model) !== modelSlug + ) { + return { effortLabel: null, speed: null }; + } + const descriptors = + provider?.models.find((candidate) => candidate.slug === modelSlug)?.capabilities + ?.optionDescriptors ?? []; + const effortLabel = + EFFORT_OPTION_IDS.map((id) => { + const value = getModelSelectionStringOptionValue(selection, id); + if (value === undefined) return undefined; + const descriptor = descriptors.find((candidate) => candidate.id === id); + return descriptor?.type === "select" + ? (descriptor.options.find((option) => option.id === value)?.label ?? value) + : value; + }).find(Boolean) ?? null; + const fastMode = + descriptors.some(({ id, type }) => id === "fastMode" && type === "boolean") && + getModelSelectionBooleanOptionValue(selection, "fastMode") === true; + // Only Codex runs on service tiers. Match by id: a catalog can list a fast + // tier under the same id as its generic Standard choice. + const tierDescriptor = descriptors.find(({ id }) => id === "serviceTier"); + const tier = + provider?.driver === "codex" + ? getModelSelectionStringOptionValue(selection, "serviceTier") + : undefined; + const onTier = (label: string) => + tier !== undefined && + tierDescriptor?.type === "select" && + tierDescriptor.options.some((option) => option.id === tier && option.label === label); + const speed = onTier("Ultrafast") ? "ultrafast" : fastMode || onTier("Fast") ? "fast" : null; + return { effortLabel, speed }; +} + /** Live work leads with progress; settled work leads with its result. */ export function subagentDetailPreview(input: { readonly status: OrchestrationV2TurnItemStatus; From 1a3040a28bee37874eb168896e1bcd1a933aae34 Mon Sep 17 00:00:00 2001 From: Rakshith Bhat Date: Fri, 9 Oct 2026 12:27:47 +0530 Subject: [PATCH 2/6] refactor(web): drop the speed helper the subagent resolver replaced Co-authored-by: Claude Opus 5.5 --- apps/web/src/components/chat/TraitsSpeed.tsx | 37 +------------------- 1 file changed, 1 insertion(+), 36 deletions(-) diff --git a/apps/web/src/components/chat/TraitsSpeed.tsx b/apps/web/src/components/chat/TraitsSpeed.tsx index 6fdc71cc395cd..e91d9c36acb73 100644 --- a/apps/web/src/components/chat/TraitsSpeed.tsx +++ b/apps/web/src/components/chat/TraitsSpeed.tsx @@ -1,44 +1,9 @@ -import type { ProviderDriverKind, ProviderOptionDescriptor } from "@t3tools/contracts"; -import { getProviderOptionCurrentValue } from "@t3tools/shared/model"; +import type { ProviderDriverKind } from "@t3tools/contracts"; import { ZapIcon } from "lucide-react"; import { UltrafastIcon } from "../Icons"; import { cn } from "~/lib/utils"; import { ComposerControlIcon, type ComposerControlSize } from "./ComposerControl"; -export function getTraitsSpeedDisplay( - provider: ProviderDriverKind, - descriptor: ProviderOptionDescriptor, -): { label: string; speedIcon: "fast" | "ultrafast" | null } | null { - if (descriptor.id === "fastMode" && descriptor.type === "boolean") { - return { - label: descriptor.currentValue === true ? "Fast" : "Normal", - speedIcon: descriptor.currentValue === true ? "fast" : null, - }; - } - if (provider !== "codex" || descriptor.id !== "serviceTier" || descriptor.type !== "select") { - return null; - } - const currentValue = getProviderOptionCurrentValue(descriptor); - const fastTier = descriptor.options.find(({ label }) => label === "Fast"); - const ultrafastTier = descriptor.options.find(({ label }) => label === "Ultrafast"); - if ( - ((fastTier || ultrafastTier) && currentValue === "default") || - (fastTier && currentValue === fastTier.id) || - (ultrafastTier && currentValue === ultrafastTier.id) - ) { - return { - label: descriptor.options.find(({ id }) => id === currentValue)?.label ?? "Normal", - speedIcon: - ultrafastTier && currentValue === ultrafastTier.id - ? "ultrafast" - : fastTier && currentValue === fastTier.id - ? "fast" - : null, - }; - } - return null; -} - export function TraitsSpeedIcon({ provider, speedIcon, From 8248d9ec7b515af20c0e02b55337d82e7221bf66 Mon Sep 17 00:00:00 2001 From: maria-rcks Date: Sat, 31 Oct 2026 22:09:03 +0000 Subject: [PATCH 3/6] fix: omit uncertain subagent effort and speed --- .../src/state/subagentDisplay.ts | 31 ++++++++++--------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/packages/client-runtime/src/state/subagentDisplay.ts b/packages/client-runtime/src/state/subagentDisplay.ts index 86477f386a19e..1911abd3f8373 100644 --- a/packages/client-runtime/src/state/subagentDisplay.ts +++ b/packages/client-runtime/src/state/subagentDisplay.ts @@ -165,29 +165,32 @@ export function resolveSubagentModelTraits(input: { const descriptors = provider?.models.find((candidate) => candidate.slug === modelSlug)?.capabilities ?.optionDescriptors ?? []; + // Claude native selections can contain inherited effort instead of the agent definition's value. const effortLabel = - EFFORT_OPTION_IDS.map((id) => { - const value = getModelSelectionStringOptionValue(selection, id); - if (value === undefined) return undefined; - const descriptor = descriptors.find((candidate) => candidate.id === id); - return descriptor?.type === "select" - ? (descriptor.options.find((option) => option.id === value)?.label ?? value) - : value; - }).find(Boolean) ?? null; + input.origin === "provider_native" && provider?.driver === "claudeAgent" + ? null + : (EFFORT_OPTION_IDS.map((id) => { + const value = getModelSelectionStringOptionValue(selection, id); + if (value === undefined) return undefined; + const descriptor = descriptors.find((candidate) => candidate.id === id); + return descriptor?.type === "select" + ? (descriptor.options.find((option) => option.id === value)?.label ?? value) + : value; + }).find(Boolean) ?? null); const fastMode = descriptors.some(({ id, type }) => id === "fastMode" && type === "boolean") && getModelSelectionBooleanOptionValue(selection, "fastMode") === true; - // Only Codex runs on service tiers. Match by id: a catalog can list a fast - // tier under the same id as its generic Standard choice. + // A duplicated tier id does not identify which speed was selected. const tierDescriptor = descriptors.find(({ id }) => id === "serviceTier"); const tier = provider?.driver === "codex" ? getModelSelectionStringOptionValue(selection, "serviceTier") : undefined; - const onTier = (label: string) => - tier !== undefined && - tierDescriptor?.type === "select" && - tierDescriptor.options.some((option) => option.id === tier && option.label === label); + const tiers = + tierDescriptor?.type === "select" + ? tierDescriptor.options.filter((option) => option.id === tier) + : []; + const onTier = (label: string) => tiers.length === 1 && tiers[0]?.label === label; const speed = onTier("Ultrafast") ? "ultrafast" : fastMode || onTier("Fast") ? "fast" : null; return { effortLabel, speed }; } From e042871856713edc1150755c80e7cc8bda35deac Mon Sep 17 00:00:00 2001 From: maria-rcks Date: Sat, 31 Oct 2026 22:16:58 +0000 Subject: [PATCH 4/6] fix: preserve subagent traits across provider and environment changes --- .../src/orchestration-v2/Orchestrator.ts | 1 + .../RemoteDelegatedTask.test.ts | 1 + .../src/components/chat/V2LifecycleRow.tsx | 4 +++- .../src/state/subagentDisplay.test.ts | 21 +++++++++++++++---- .../src/state/subagentDisplay.ts | 13 +++--------- 5 files changed, 25 insertions(+), 15 deletions(-) diff --git a/apps/server/src/orchestration-v2/Orchestrator.ts b/apps/server/src/orchestration-v2/Orchestrator.ts index 9ebacc80a3c4e..b76b001403e3e 100644 --- a/apps/server/src/orchestration-v2/Orchestrator.ts +++ b/apps/server/src/orchestration-v2/Orchestrator.ts @@ -7999,6 +7999,7 @@ const makeOrchestrator = Effect.fn("orchestrationV2.Orchestrator.layer")(functio prompt: command.task, title: command.title ?? null, model: command.modelSelection.model, + modelSelection: command.modelSelection, ...(command.completionWake === undefined ? {} : { completionWake: command.completionWake }), status: "running", result: null, diff --git a/apps/server/src/orchestration-v2/RemoteDelegatedTask.test.ts b/apps/server/src/orchestration-v2/RemoteDelegatedTask.test.ts index 4bb8c103f6df7..aa98bf52dbdbc 100644 --- a/apps/server/src/orchestration-v2/RemoteDelegatedTask.test.ts +++ b/apps/server/src/orchestration-v2/RemoteDelegatedTask.test.ts @@ -215,6 +215,7 @@ it.layer(layerTest)("a delegated task in a linked environment", (it) => { const projection = yield* orchestrator.getThreadProjection(parent.threadId); assert.equal(projection.subagents.length, 1); const [task] = projection.subagents; + assert.deepEqual(task?.modelSelection, modelSelection); assert.equal(task?.childThreadId, null); assert.deepEqual(task?.remoteChild, remoteChild); assert.equal(task?.status, "running"); diff --git a/apps/web/src/components/chat/V2LifecycleRow.tsx b/apps/web/src/components/chat/V2LifecycleRow.tsx index 94cc8d99b49ba..da33046013f83 100644 --- a/apps/web/src/components/chat/V2LifecycleRow.tsx +++ b/apps/web/src/components/chat/V2LifecycleRow.tsx @@ -430,7 +430,9 @@ function SubagentTimelineLink(props: { const model = agent?.model ?? null; // Select only the derived labels so provider status pushes, which replace // the provider objects, do not re-render every row. - const providersAtom = serverEnvironment.providersValueAtom(props.parentRef.environmentId); + const providersAtom = serverEnvironment.providersValueAtom( + remote?.environmentId ?? props.parentRef.environmentId, + ); const findProvider = (providers: ReadonlyArray | null | undefined) => providers?.find((provider) => provider.instanceId === props.providerInstanceId); const modelTraits = (providers: ReadonlyArray | null | undefined) => diff --git a/packages/client-runtime/src/state/subagentDisplay.test.ts b/packages/client-runtime/src/state/subagentDisplay.test.ts index 732bc785e41bd..1cf549fedc70a 100644 --- a/packages/client-runtime/src/state/subagentDisplay.test.ts +++ b/packages/client-runtime/src/state/subagentDisplay.test.ts @@ -75,6 +75,16 @@ describe("resolveSubagentModelTraits", () => { }); expect(traits("app_owned")).toEqual({ effortLabel: "Extra High", speed: "fast" }); expect(traits("provider_native")).toEqual({ effortLabel: null, speed: null }); + expect( + resolveSubagentModelTraits({ + model: "claude-opus-4-7", + providerInstanceId: claudeInstance, + origin: "provider_native", + modelSelection: selection(claudeInstance, "claude-opus-4-7", [ + { id: "effort", value: "high" }, + ]), + }).effortLabel, + ).toBeNull(); expect( resolveSubagentModelTraits({ model: "claude-opus-4-7", @@ -109,7 +119,10 @@ describe("resolveSubagentModelTraits", () => { ).toEqual({ effortLabel: "High", speed: null }); }); - it("names Cursor's reasoning effort", () => { + it.each([ + ["cursor", "reasoning_effort"], + ["antigravity", "thinking_level"], + ] as const)("names %s reasoning effort", (driver, optionId) => { const cursorInstance = ProviderInstanceId.make("cursor"); expect( resolveSubagentModelTraits({ @@ -117,16 +130,16 @@ describe("resolveSubagentModelTraits", () => { providerInstanceId: cursorInstance, origin: "provider_native", modelSelection: selection(cursorInstance, "gpt-6.1-sol", [ - { id: "reasoning_effort", value: "xhigh" }, + { id: optionId, value: "xhigh" }, ]), provider: { - driver: ProviderDriverKind.make("cursor"), + driver: ProviderDriverKind.make(driver), models: [ { slug: "gpt-6.1-sol", name: "GPT-6.1-Sol", isCustom: false, - capabilities: { optionDescriptors: [{ ...effort, id: "reasoning_effort" }] }, + capabilities: { optionDescriptors: [{ ...effort, id: optionId }] }, }, ], }, diff --git a/packages/client-runtime/src/state/subagentDisplay.ts b/packages/client-runtime/src/state/subagentDisplay.ts index 1911abd3f8373..fbe413d80c938 100644 --- a/packages/client-runtime/src/state/subagentDisplay.ts +++ b/packages/client-runtime/src/state/subagentDisplay.ts @@ -15,6 +15,7 @@ import { } from "@t3tools/shared/model"; import { fileBasename } from "@t3tools/shared/path"; import { resolveModelShortName } from "../providerClients.ts"; +import { REASONING_EFFORT_OPTION_IDS } from "./threadExecution.ts"; import { isTerminalSubagentStatus } from "./subagentRuntime.ts"; export function subagentGroupSummary( @@ -119,14 +120,6 @@ export function resolveSubagentMetadata(input: { return { modelLabel, workspace }; } -const EFFORT_OPTION_IDS = [ - "reasoningEffort", - "effort", - "reasoning", - "variant", - "reasoning_effort", -] as const; - /** * The reasoning effort and speed a subagent runs at, named as the composer * names them. Only a selection for the model the subagent reported counts, @@ -167,9 +160,9 @@ export function resolveSubagentModelTraits(input: { ?.optionDescriptors ?? []; // Claude native selections can contain inherited effort instead of the agent definition's value. const effortLabel = - input.origin === "provider_native" && provider?.driver === "claudeAgent" + input.origin === "provider_native" && (provider == null || provider.driver === "claudeAgent") ? null - : (EFFORT_OPTION_IDS.map((id) => { + : (REASONING_EFFORT_OPTION_IDS.map((id) => { const value = getModelSelectionStringOptionValue(selection, id); if (value === undefined) return undefined; const descriptor = descriptors.find((candidate) => candidate.id === id); From 273261efdcc101a43bd864ea8ff1d24846c83102 Mon Sep 17 00:00:00 2001 From: maria-rcks Date: Sat, 31 Oct 2026 22:21:59 +0000 Subject: [PATCH 5/6] fix(web): use remote provider catalog in subagent tooltip --- apps/web/src/components/chat/V2LifecycleRow.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/apps/web/src/components/chat/V2LifecycleRow.tsx b/apps/web/src/components/chat/V2LifecycleRow.tsx index da33046013f83..ce94dae36ac77 100644 --- a/apps/web/src/components/chat/V2LifecycleRow.tsx +++ b/apps/web/src/components/chat/V2LifecycleRow.tsx @@ -572,7 +572,9 @@ function SubagentTimelineTooltip( const environmentId = props.parentRef.environmentId; // Read providers only in the open popup to avoid per-row subscriptions. const providers = - useAtomValue(serverEnvironment.providersValueAtom(environmentId)) ?? EMPTY_SERVER_PROVIDERS; + useAtomValue( + serverEnvironment.providersValueAtom(props.remote?.environmentId ?? environmentId), + ) ?? EMPTY_SERVER_PROVIDERS; const parent = useThreadShell(props.parentRef)?.source; const child = useThreadShell( props.threadId ? scopeThreadRef(environmentId, props.threadId) : null, From d2ccbead400f85bb8e4e647bd7f68acd1a2c5473 Mon Sep 17 00:00:00 2001 From: maria-rcks Date: Sun, 1 Nov 2026 01:35:45 +0000 Subject: [PATCH 6/6] fix: recover legacy subagent selections outside the shell --- .../src/features/threads/SubagentRow.tsx | 28 ++++++++++++++++--- .../src/components/chat/V2LifecycleRow.tsx | 28 +++++++++++++++---- 2 files changed, 46 insertions(+), 10 deletions(-) diff --git a/apps/mobile/src/features/threads/SubagentRow.tsx b/apps/mobile/src/features/threads/SubagentRow.tsx index d976b3586e6eb..dd197b8b03d95 100644 --- a/apps/mobile/src/features/threads/SubagentRow.tsx +++ b/apps/mobile/src/features/threads/SubagentRow.tsx @@ -13,6 +13,8 @@ import { SymbolView } from "../../components/ui/AppSymbol"; import { ProviderIcon } from "../../components/ProviderIcon"; import { cn } from "../../lib/cn"; import { useEnvironmentServerConfig, useProject, useThreadShell } from "../../state/entities"; +import { useEnvironmentQuery } from "../../state/query"; +import { threadFamilies } from "../../state/threads"; import { subagentCardDetail } from "./subagent-card-presentation"; import { SUBAGENT_TONE_TEXT_CLASS, SubagentStatusDot } from "./SubagentStatusDot"; import { resolveSubagentRowPresentation } from "./threadAgentsPresentation"; @@ -111,9 +113,27 @@ function SubagentMetadata(props: { (candidate) => candidate.instanceId === subagent.providerInstanceId, ); const parent = useThreadShell(scopeThreadRef(environmentId, subagent.threadId))?.source; - const child = useThreadShell( - subagent.childThreadId === null ? null : scopeThreadRef(environmentId, subagent.childThreadId), - )?.source; + const childRef = + subagent.childThreadId !== null + ? scopeThreadRef(environmentId, subagent.childThreadId) + : subagent.remoteChild + ? scopeThreadRef(subagent.remoteChild.environmentId, subagent.remoteChild.threadId) + : null; + const childShell = useThreadShell(childRef)?.source; + const family = useEnvironmentQuery( + childRef !== null && + childShell === undefined && + subagent.origin === "app_owned" && + subagent.modelSelection === undefined + ? threadFamilies.familyAtom( + childRef.environmentId, + subagent.childThreadId !== null ? subagent.threadId : childRef.threadId, + ) + : null, + ); + const selectionChild = + childShell ?? family.data?.threads.find((row) => row.id === childRef?.threadId); + const child = subagent.childThreadId === null ? undefined : childShell; const parentProject = useProject( parent ? scopeProjectRef(environmentId, parent.projectId) : null, ); @@ -131,7 +151,7 @@ function SubagentMetadata(props: { providerInstanceId: subagent.providerInstanceId, origin: subagent.origin, modelSelection: subagent.modelSelection, - childThread: child, + childThread: selectionChild, provider, }); const traits = [ diff --git a/apps/web/src/components/chat/V2LifecycleRow.tsx b/apps/web/src/components/chat/V2LifecycleRow.tsx index ce94dae36ac77..1aa26bc05ee72 100644 --- a/apps/web/src/components/chat/V2LifecycleRow.tsx +++ b/apps/web/src/components/chat/V2LifecycleRow.tsx @@ -9,7 +9,8 @@ import { useNavigate } from "@tanstack/react-router"; import { SubagentTooltipContent } from "./SubagentTooltipContent"; import { useAtomValue } from "@effect/atom-react"; import { scopeThreadRef, scopeProjectRef } from "@t3tools/client-runtime/environment"; -import { environmentThreadDetails } from "../../state/threads"; +import { environmentThreadDetails, threadFamilies } from "../../state/threads"; +import { useEnvironmentQuery } from "../../state/query"; import { EMPTY_SERVER_PROVIDERS, serverEnvironment } from "../../state/server"; import { MiddleTruncate } from "../ui/middle-truncate"; import * as DateTime from "effect/DateTime"; @@ -422,11 +423,26 @@ function SubagentTimelineLink(props: { completedAt: isoOrNull(agent?.completedAt ?? props.completedAt), }; // Older app-owned records keep their selection only on the child thread. - const childModelSelection = useThreadShell( - props.origin === "app_owned" && threadId !== null && agent?.modelSelection === undefined - ? scopeThreadRef(props.parentRef.environmentId, threadId) + const fallbackChild = + props.origin === "app_owned" && agent?.modelSelection === undefined + ? threadId !== null + ? scopeThreadRef(props.parentRef.environmentId, threadId) + : remote + ? scopeThreadRef(remote.environmentId, remote.threadId) + : null + : null; + const childShell = useThreadShell(fallbackChild)?.source; + const family = useEnvironmentQuery( + fallbackChild !== null && childShell === undefined + ? threadFamilies.familyAtom( + fallbackChild.environmentId, + threadId !== null ? props.parentRef.threadId : fallbackChild.threadId, + ) : null, - )?.source.modelSelection; + ); + const childModelSelection = ( + childShell ?? family.data?.threads.find((child) => child.id === fallbackChild?.threadId) + )?.modelSelection; const model = agent?.model ?? null; // Select only the derived labels so provider status pushes, which replace // the provider objects, do not re-render every row. @@ -552,7 +568,7 @@ function SubagentTimelineLink(props: { {...props} elapsed={agent ? : null} model={agent?.model ?? null} - modelSelection={agent?.modelSelection} + modelSelection={agent?.modelSelection ?? childModelSelection} status={liveStatus} result={agent?.result ?? props.result} progress={agent?.progress ?? props.progress}