Skip to content
Open
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 19 additions & 2 deletions apps/mobile/src/features/threads/SubagentRow.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -19,6 +22,8 @@ type SubagentRowSubagent = Pick<
| "threadId"
| "childThreadId"
| "model"
| "modelSelection"
| "origin"
| "driver"
| "providerInstanceId"
| "title"
Expand Down Expand Up @@ -121,6 +126,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 (
<View className="min-w-0 flex-row items-center gap-1.5">
<ProviderIcon
Expand All @@ -130,7 +147,7 @@ function SubagentMetadata(props: {
/>
<Text className="min-w-0 shrink text-xs text-foreground-muted" numberOfLines={1}>
{provider ? `${resolveProviderInstanceDisplayName(provider)} · ` : ""}
{modelLabel}
{[modelLabel, ...traits].join(" · ")}
</Text>
{workspace.map(({ label, value }) => (
// Preserve the label against flattening without making it a separate accessibility stop.
Expand Down
1 change: 1 addition & 0 deletions apps/mobile/src/features/threads/thread-subagent-group.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,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;
Expand Down
2 changes: 2 additions & 0 deletions apps/mobile/src/features/threads/thread-workflow-opening.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,8 @@ function memberRowSubagent(
threadId: coordinatorThreadId,
childThreadId: member.childThreadId as ThreadId | null,
model: member.model,
// The provider's own workflow tool launches its members.
origin: "provider_native" as const,
driver: opening.driver!,
providerInstanceId: opening.providerInstanceId!,
title: member.title,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -994,6 +994,63 @@ it.effect("keeps delegated child pull-request links independent of the parent",
}).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)),
);

it.effect("wakes the parent once when its delegated task asks a question", () =>
Effect.gen(function* () {
const orchestrator = yield* Orchestrator.OrchestratorV2;
Expand Down
2 changes: 2 additions & 0 deletions apps/server/src/orchestration-v2/Orchestrator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7583,6 +7583,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,
Expand Down
64 changes: 6 additions & 58 deletions apps/web/src/components/chat/SubagentTooltipContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,10 @@ import type {
} from "@t3tools/contracts";
import {
resolveSubagentMetadata,
resolveSubagentModelTraits,
subagentDetailPreview,
} from "@t3tools/client-runtime/state/subagent-display";
import { REASONING_EFFORT_OPTION_IDS } from "@t3tools/client-runtime/state/thread-execution";
import { getModelSelectionStringOptionValue, resolveSelectableModel } from "@t3tools/shared/model";
import { getTraitsSpeedDisplay, TraitsSpeedIcon } from "./TraitsSpeed";
import { TraitsSpeedIcon } from "./TraitsSpeed";
import type { ReactNode } from "react";
import {
BotIcon,
Expand Down Expand Up @@ -52,59 +51,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 = REASONING_EFFORT_OPTION_IDS.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);
Expand Down Expand Up @@ -139,11 +87,11 @@ export function SubagentTooltipContent(props: {
<span className="min-w-0 truncate">
{showInstanceBadge ? `${modelLabel} · ${entry.displayName}` : modelLabel}
</span>
{effort || speed?.speedIcon ? (
{effort || speed ? (
<span className="inline-flex shrink-0 items-center gap-1">
{effort ? " · " : null}
{speed?.speedIcon && provider ? (
<TraitsSpeedIcon provider={provider.driver} speedIcon={speed.speedIcon} size="xs" />
{speed && provider ? (
<TraitsSpeedIcon provider={provider.driver} speedIcon={speed} size="xs" />
) : null}
{effort}
</span>
Expand Down
37 changes: 1 addition & 36 deletions apps/web/src/components/chat/TraitsSpeed.tsx
Original file line number Diff line number Diff line change
@@ -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,
Expand Down
52 changes: 51 additions & 1 deletion apps/web/src/components/chat/V2LifecycleRow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,11 @@ import {
type HandoffEndpoints,
type HandoffTimelineRun,
} from "@t3tools/client-runtime/handoff";
import { formatSubagentDisplayTitle } from "@t3tools/client-runtime/state/subagent-display";
import {
formatSubagentDisplayTitle,
resolveSubagentMetadata,
resolveSubagentModelTraits,
} from "@t3tools/client-runtime/state/subagent-display";
import {
ProviderDriverKind,
type OrchestrationV2Notification,
Expand Down Expand Up @@ -54,6 +58,7 @@ import {
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 { subagentResumedSince } from "./V2LifecycleRow.logic";
Expand Down Expand Up @@ -416,6 +421,37 @@ 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;
// 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);
Comment thread
macroscopeapp[bot] marked this conversation as resolved.
Resolved
const findProvider = (providers: ReadonlyArray<ServerProvider> | null | undefined) =>
providers?.find((provider) => provider.instanceId === props.providerInstanceId);
const modelTraits = (providers: ReadonlyArray<ServerProvider> | null | undefined) =>
resolveSubagentModelTraits({
model,
providerInstanceId: props.providerInstanceId,
origin: props.origin,
modelSelection: agent?.modelSelection,
childThread: childModelSelection ? { modelSelection: childModelSelection } : null,
provider: findProvider(providers),
});
const modelLabel = useAtomValue(providersAtom, (providers) =>
model === null
? null
: resolveSubagentMetadata({ model, provider: findProvider(providers) }).modelLabel,
);
const effortLabel = useAtomValue(
providersAtom,
(providers) => modelTraits(providers).effortLabel,
);
const speed = useAtomValue(providersAtom, (providers) => modelTraits(providers).speed);
const content = (
<>
<EnvironmentSubagentAvatar
Expand Down Expand Up @@ -449,6 +485,20 @@ function SubagentTimelineLink(props: {
)}
</span>
</span>
{modelLabel === null ? null : (
<span className="inline-flex max-w-[40%] min-w-0 items-center gap-1 text-3xs text-muted-foreground">
<span className="min-w-0 truncate">{modelLabel}</span>
{effortLabel !== null || speed !== null ? (
<span className="inline-flex shrink-0 items-center gap-1">
{effortLabel !== null ? " · " : null}
{speed !== null ? (
<TraitsSpeedIcon provider={props.driver} speedIcon={speed} size="xs" />
) : null}
{effortLabel}
</span>
) : null}
</span>
)}
{detail !== null && (props.event !== undefined || status !== "completed") ? (
<span
className={cn("shrink-0 text-xs", failed ? "text-destructive" : "text-muted-foreground")}
Expand Down
Loading
Loading