Skip to content
Open
Show file tree
Hide file tree
Changes from 9 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
47 changes: 42 additions & 5 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 @@ -10,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";
Expand All @@ -19,6 +24,8 @@ type SubagentRowSubagent = Pick<
| "threadId"
| "childThreadId"
| "model"
| "modelSelection"
| "origin"
| "driver"
| "providerInstanceId"
| "title"
Expand Down Expand Up @@ -106,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,
);
Expand All @@ -121,6 +146,18 @@ function SubagentMetadata(props: {
parentProject,
childProject,
});
const { effortLabel, speed } = resolveSubagentModelTraits({
model: subagent.model,
providerInstanceId: subagent.providerInstanceId,
origin: subagent.origin,
modelSelection: subagent.modelSelection,
childThread: selectionChild,
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 +167,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
3 changes: 3 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 Expand Up @@ -7997,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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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");
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
Loading
Loading