Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
73 changes: 41 additions & 32 deletions apps/mobile/src/features/threads/SubagentRow.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
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 {
formatSubagentUsage,
resolveSubagentContextUsage,
resolveSubagentMetadata,
} 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 @@ -26,6 +30,7 @@ type SubagentRowSubagent = Pick<
| "status"
| "progress"
| "result"
| "usage"
>;

export function SubagentRow(props: {
Expand Down Expand Up @@ -112,38 +117,42 @@ function SubagentMetadata(props: {
parentProject,
childProject,
});
const usage = formatSubagentUsage(resolveSubagentContextUsage(subagent, child));
return (
<View className="min-w-0 flex-row items-center gap-1.5">
<ProviderIcon
provider={provider?.driver ?? subagent.driver}
iconUrl={provider?.iconUrl}
size={12}
/>
<Text className="min-w-0 shrink text-xs text-foreground-muted" numberOfLines={1}>
{provider ? `${resolveProviderInstanceDisplayName(provider)} · ` : ""}
{modelLabel}
</Text>
{workspace.map(({ label, value }) => (
// The row reads this label in place of the icon. collapsable keeps the
// view (and label) from being flattened away without making it a
// separate accessibility stop.
<View
key={label}
collapsable={false}
accessibilityLabel={`${label}: ${value}`}
className="min-w-0 shrink flex-row items-center gap-1.5"
>
<Text className="text-xs text-foreground-muted">·</Text>
<SymbolView
name={label === "Branch" ? "arrow.triangle.branch" : "folder"}
size={11}
tintColorClassName="accent-icon-muted"
/>
<Text className="min-w-0 shrink text-xs text-foreground-muted" numberOfLines={1}>
{value}
</Text>
</View>
))}
<View className="gap-1">
<View className="min-w-0 flex-row items-center gap-1.5">
<ProviderIcon
provider={provider?.driver ?? subagent.driver}
iconUrl={provider?.iconUrl}
size={12}
/>
<Text className="min-w-0 shrink text-xs text-foreground-muted" numberOfLines={1}>
{provider ? `${resolveProviderInstanceDisplayName(provider)} · ` : ""}
{modelLabel}
</Text>
{workspace.map(({ label, value }) => (
// The row reads this label in place of the icon. collapsable keeps the
// view (and label) from being flattened away without making it a
// separate accessibility stop.
<View
key={label}
collapsable={false}
accessibilityLabel={`${label}: ${value}`}
className="min-w-0 shrink flex-row items-center gap-1.5"
>
<Text className="text-xs text-foreground-muted">·</Text>
<SymbolView
name={label === "Branch" ? "arrow.triangle.branch" : "folder"}
size={11}
tintColorClassName="accent-icon-muted"
/>
<Text className="min-w-0 shrink text-xs text-foreground-muted" numberOfLines={1}>
{value}
</Text>
</View>
))}
</View>
{usage ? <Text className="text-xs tabular-nums text-foreground-muted">{usage}</Text> : null}
</View>
);
}
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 @@ -89,6 +89,7 @@ export function ThreadSubagentGroup(props: {
result: live?.result ?? item.result,
progress: live?.progress ?? item.progress,
model: live?.model ?? null,
usage: live?.usage,
};
});
const grouped = agents.length > 1;
Expand Down
10 changes: 10 additions & 0 deletions apps/web/src/components/chat/SubagentTooltipContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import type {
OrchestrationV2Subagent,
} from "@t3tools/contracts";
import {
formatSubagentUsage,
resolveSubagentMetadata,
subagentDetailPreview,
} from "@t3tools/client-runtime/state/subagent-display";
Expand All @@ -20,6 +21,7 @@ import {
CircleDashedIcon,
CircleXIcon,
FolderIcon,
GaugeIcon,
GitBranchIcon,
TerminalIcon,
} from "lucide-react";
Expand Down Expand Up @@ -49,9 +51,11 @@ export function SubagentTooltipContent(props: {
status: OrchestrationV2TurnItemStatus;
result?: string | null | undefined;
progress?: string | null | undefined;
usage?: OrchestrationV2Subagent["usage"] | null;
}) {
const { modelLabel, workspace: metadata } = resolveSubagentMetadata(props);
const preview = subagentDetailPreview(props);
const usage = formatSubagentUsage(props.usage ?? null);
const model = props.model?.trim();
const modelSlug = props.provider
? resolveSelectableModel(props.provider.driver, model, props.provider.models)
Expand Down Expand Up @@ -178,6 +182,12 @@ export function SubagentTooltipContent(props: {
</div>
);
})}
{usage ? (
<div className="flex min-w-0 items-center gap-2">
<GaugeIcon aria-hidden className="size-3 shrink-0" />
<span className="tabular-nums">{usage}</span>
</div>
) : null}
{preview ? (
<div className="flex min-w-0 items-center gap-2">
<TerminalIcon aria-hidden className="size-3 shrink-0" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -832,6 +832,76 @@ it("shows readable models and only differing workspace details in agent tooltips
expect(text()).not.toContain("Worktree");
expect(text()).not.toContain("Workspace");

expect(text()).not.toContain("context");
expect(text()).not.toContain("tool uses");
const contextAgent = {
...projection.subagents[0],
origin: "provider_native",
driver: "claudeAgent",
providerInstanceId: "claude",
status: "running",
usage: { contextTokens: 41_100, toolUses: 2 },
};
state.projection = { ...projection, subagents: [contextAgent] };
await update();
expect(text()).toContain("41.1K context · 2 tool uses");

state.projection = {
...projection,
subagents: [{ ...contextAgent, status: "completed" }],
};
state.shells = [{ environmentId: "test", source: { ...child, activityRunStatus: "running" } }];
await update();
expect(text()).not.toContain("context");
expect(text()).not.toContain("tool uses");

state.projection = { ...projection, subagents: [contextAgent] };
await update();
expect(text()).toContain("41.1K context · 2 tool uses");
state.projection = {
...projection,
subagents: [{ ...contextAgent, usage: { contextTokens: 12_000, toolUses: 3 } }],
};
await update();
expect(text()).toContain("12K context · 3 tool uses");
expect(text()).not.toContain("41.1K context");

state.projection = {
...projection,
subagents: [
{
...contextAgent,
status: "completed",
completedAt: DateTime.makeUnsafe("2026-09-16T12:01:00Z"),
},
],
};
const completedChild = {
...child,
status: "completed",
activityRunStatus: null,
latestRunRequestedAt: DateTime.makeUnsafe("2026-09-16T12:00:00Z"),
latestRunCompletedAt: DateTime.makeUnsafe("2026-09-16T12:01:00Z"),
};
await rerender(completedChild);
await click(sectionToggle(false));
expect(text()).toContain("41.1K context · 2 tool uses");
const newerCompletedChild = {
...completedChild,
latestRunRequestedAt: DateTime.makeUnsafe("2026-09-16T12:02:00Z"),
latestRunCompletedAt: DateTime.makeUnsafe("2026-09-16T12:03:00Z"),
};
await rerender(newerCompletedChild);
expect(text()).toContain("Worker");
expect(text()).not.toContain("context");
expect(text()).not.toContain("tool uses");
await click(sectionToggle(true));

state.projection = projection;
await rerender();
expect(text()).not.toContain("context");
expect(text()).not.toContain("tool uses");

for (const [model, expected] of [
[null, "Not reported"],
["", "Not reported"],
Expand Down
10 changes: 9 additions & 1 deletion apps/web/src/components/chat/ThreadRelationshipsControl.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,10 @@ import {
projectedSubagentsToRuntime,
type RuntimeSubagent,
} from "@t3tools/client-runtime/state/subagentRuntime";
import { formatSubagentDisplayTitle } from "@t3tools/client-runtime/state/subagent-display";
import {
formatSubagentDisplayTitle,
resolveSubagentContextUsage,
} from "@t3tools/client-runtime/state/subagent-display";
import {
deriveThreadRelationshipGraph,
immediateThreadRelationships,
Expand Down Expand Up @@ -198,6 +201,7 @@ function currentSubagent<Agent extends RuntimeSubagent>(
: status,
startedAt: startedAt ? DateTime.formatIso(startedAt) : null,
completedAt: completedAt ? DateTime.formatIso(completedAt) : null,
contextUsage: newerRun ? null : agent.contextUsage,
progress: null,
result: null,
error: null,
Expand Down Expand Up @@ -487,6 +491,10 @@ export function ThreadRelationshipsPanel(props: {
status={agent.status}
result={agent.result}
progress={agent.progress}
usage={resolveSubagentContextUsage(
{ status: agent.nativeStatus, usage: agent.contextUsage },
node?.thread,
)}
parentThread={currentThread ?? undefined}
childThread={node?.thread ?? undefined}
parentProject={currentProject}
Expand Down
10 changes: 9 additions & 1 deletion apps/web/src/components/chat/V2LifecycleRow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,10 @@ import {
type HandoffEndpoints,
type HandoffTimelineRun,
} from "@t3tools/client-runtime/handoff";
import { formatSubagentDisplayTitle } from "@t3tools/client-runtime/state/subagent-display";
import {
formatSubagentDisplayTitle,
resolveSubagentContextUsage,
} from "@t3tools/client-runtime/state/subagent-display";
import {
ProviderDriverKind,
type OrchestrationV2Notification,
Expand Down Expand Up @@ -517,6 +520,8 @@ function SubagentTimelineLink(props: {
status={liveStatus}
result={agent?.result ?? props.result}
progress={agent?.progress ?? props.progress}
usage={agent?.usage}
taskStatus={agent?.status ?? props.status}
/>
</ThreadHoverCardPopup>
</Tooltip>
Expand All @@ -526,6 +531,8 @@ function SubagentTimelineLink(props: {
function SubagentTimelineTooltip(
props: Parameters<typeof SubagentTimelineLink>[0] & {
model: string | null;
usage: OrchestrationV2Subagent["usage"];
taskStatus: OrchestrationV2Subagent["status"];
modelSelection: OrchestrationV2Subagent["modelSelection"];
elapsed: ReactNode;
},
Expand Down Expand Up @@ -557,6 +564,7 @@ function SubagentTimelineTooltip(
status={props.status}
result={props.result}
progress={props.progress}
usage={resolveSubagentContextUsage({ status: props.taskStatus, usage: props.usage }, child)}
parentThread={parent}
childThread={child}
parentProject={parentProject ?? undefined}
Expand Down
25 changes: 25 additions & 0 deletions packages/client-runtime/src/state/subagentDisplay.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import type {
OrchestrationV2Subagent,
OrchestrationV2TurnItemStatus,
OrchestrationV2ThreadShell,
OrchestrationProjectShell,
ServerProvider,
} from "@t3tools/contracts";
import { formatModelSlugName, resolveSelectableModel } from "@t3tools/shared/model";
import { formatTokens } from "@t3tools/shared/usageFormat";
import { fileBasename } from "@t3tools/shared/path";
import { isTerminalSubagentStatus } from "./subagentRuntime.ts";

Expand Down Expand Up @@ -122,3 +124,26 @@ export function subagentDetailPreview(input: {
const compact = detail.replace(/\s+/gu, " ");
return compact.length > 280 ? `${compact.slice(0, 280).trimEnd()}…` : compact || null;
}

export function resolveSubagentContextUsage(
subagent: {
readonly status: OrchestrationV2Subagent["status"];
readonly usage?: OrchestrationV2Subagent["usage"] | null;
},
childThread?: Pick<OrchestrationV2ThreadShell, "activityRunStatus" | "status"> | null,
): OrchestrationV2Subagent["usage"] | null {
// A follow-up runs in the child thread without updating the settled task.
if (
isTerminalSubagentStatus(subagent.status) &&
(childThread?.activityRunStatus || childThread?.status === "queued")
) {
return null;
}
return subagent.usage ?? null;
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
}

export function formatSubagentUsage(usage: OrchestrationV2Subagent["usage"] | null): string | null {
if (!usage) return null;
const tools = `${usage.toolUses} ${usage.toolUses === 1 ? "tool use" : "tool uses"}`;
return `${formatTokens(usage.contextTokens)} context · ${tools}`;
}
Loading
Loading