diff --git a/apps/web/src/components/chat/SubagentTooltipContent.tsx b/apps/web/src/components/chat/SubagentTooltipContent.tsx index 83f9026b6906..8900f0f715d1 100644 --- a/apps/web/src/components/chat/SubagentTooltipContent.tsx +++ b/apps/web/src/components/chat/SubagentTooltipContent.tsx @@ -4,7 +4,7 @@ import type { ServerProvider, } from "@t3tools/contracts"; import { fileBasename } from "@t3tools/client-runtime/markdown-links"; -import { formatModelSlugName } from "@t3tools/shared/model"; +import { formatModelSlugName, resolveSelectableModel } from "@t3tools/shared/model"; import { getTriggerDisplayModelName } from "./providerIconUtils"; import { Fragment } from "react"; @@ -14,19 +14,25 @@ export function SubagentTooltipContent(props: { model: string | null; provider?: ServerProvider | undefined; parentThread?: Pick | undefined; - childThread?: Pick | undefined; + childThread?: + | Pick + | undefined; parentProject?: Pick | undefined; childProject?: Pick | undefined; status: string; result?: string | null | undefined; progress?: string | null | undefined; }) { - const providerModel = props.provider?.models.find((candidate) => candidate.slug === props.model); + const model = props.model?.trim() || props.childThread?.modelSelection.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 modelLabel = providerModel ? getTriggerDisplayModelName(providerModel) - : props.model - ? formatModelSlugName(props.model) - : "Unknown"; + : model + ? formatModelSlugName(model) + : "Not reported"; const currentWorkspace = props.parentThread?.worktreePath ?? props.parentProject?.workspaceRoot; const childWorkspace = props.childThread?.worktreePath ?? props.childProject?.workspaceRoot; const metadata = [ diff --git a/apps/web/src/components/chat/ThreadRelationshipsControl.agents.test.tsx b/apps/web/src/components/chat/ThreadRelationshipsControl.agents.test.tsx index 8549f452ef4e..1e7dd8fc0436 100644 --- a/apps/web/src/components/chat/ThreadRelationshipsControl.agents.test.tsx +++ b/apps/web/src/components/chat/ThreadRelationshipsControl.agents.test.tsx @@ -94,6 +94,7 @@ it("shows the matching child agent details and refreshes them when the agent set .join(" ") .replace(/\s+/g, " "); expect(text()).toContain("Checker"); + expect(text()).toContain("Lineage · 3 running"); expect(text()).toContain("running"); expect(text()).not.toContain("gpt-5.4"); expect(text()).not.toContain("gpt-5.3"); @@ -115,6 +116,7 @@ it("shows the matching child agent details and refreshes them when the agent set ], }; await act(async () => renderer.update(cloneElement(panel))); + expect(renderer.root.findByType("h3").children).toEqual(["Lineage"]); expect(text()).toContain("Previous agents (1)"); expect(text()).not.toContain("Checker"); await act(async () => @@ -136,6 +138,17 @@ it("shows the matching child agent details and refreshes them when the agent set ); expect(text()).toContain("Checker"); + state.projection = { + ...projection, + subagents: Array.from({ length: 8 }, (_, index) => ({ + ...agent, + id: `running-agent-${index}`, + childThreadId: `running-child-${index}`, + })), + }; + await act(async () => renderer.update(cloneElement(panel))); + expect(text()).toContain("Lineage · 8 running"); + state.projection = { ...projection, subagents: Array.from({ length: 8 }, (_, index) => ({ @@ -158,6 +171,13 @@ it("shows the matching child agent details and refreshes them when the agent set .props.onClick(), ); expect(text()).toContain("Old agent 7"); + + state.projection = { + ...projection, + subagents: [{ ...agent, childThreadId: null }], + }; + await act(async () => renderer.update(cloneElement(panel))); + expect(text()).toContain("Lineage · 1 running"); }); it("shows readable models and only differing workspace details in agent tooltips", async () => { @@ -176,6 +196,7 @@ it("shows readable models and only differing workspace details in agent tooltips worktreePath: null as string | null, branch: null as string | null, title: "Worker", + modelSelection: { instanceId: "codex", model: "gpt-5.4" }, lineage: { parentThreadId: "parent", relationshipToParent: "subagent" }, }; state.projects = [ @@ -194,7 +215,9 @@ it("shows readable models and only differing workspace details in agent tooltips { instanceId: "codex", driver: "codex", - models: [{ slug: "gpt-5.4", name: "My GPT model", shortName: "My GPT" }], + models: [ + { slug: "gpt-5.4", name: "My GPT model", shortName: "My GPT", aliases: ["model-alias"] }, + ], }, ], }); @@ -242,6 +265,23 @@ it("shows readable models and only differing workspace details in agent tooltips expect(text()).not.toContain("Worktree"); expect(text()).not.toContain("Workspace"); + for (const [model, expected] of [ + [null, "My GPT"], + ["", "My GPT"], + [" ", "My GPT"], + ["model-alias", "My GPT"], + ["gpt-5.5", "GPT-5.5"], + ["custom/model-v1", "custom/model-v1"], + ] as const) { + state.projection = { + ...projection, + subagents: [{ ...projection.subagents[0], model }], + }; + await act(async () => renderer.update(cloneElement(panel))); + expect(text()).toContain(expected); + expect(text()).not.toContain("Unknown"); + } + state.projection = { ...projection, subagents: [ @@ -317,7 +357,7 @@ it("shows readable models and only differing workspace details in agent tooltips ["grok", "grok-4-fast", "Grok 4 Fast"], ["antigravity", "gemini-3.8-flash-high", "Gemini 3.8 Flash High"], ["opencode", "anthropic/claude-sonnet-4-6", "anthropic/Claude Sonnet 4.6"], - ["codex", null, "Unknown"], + ["codex", null, "Not reported"], ] as const) { state.projection = { ...projection, diff --git a/apps/web/src/components/chat/ThreadRelationshipsControl.tsx b/apps/web/src/components/chat/ThreadRelationshipsControl.tsx index 827a7697294a..b07013bea588 100644 --- a/apps/web/src/components/chat/ThreadRelationshipsControl.tsx +++ b/apps/web/src/components/chat/ThreadRelationshipsControl.tsx @@ -240,8 +240,11 @@ export function ThreadRelationshipsPanel(props: { { id: "active", label: null, rows: active, expanded: true }, { id: "previous", label: "Previous agents", rows: previous, expanded: false }, ]; + const runningCount = + projection?.subagents.filter((agent) => agent.status === "running").length ?? + active.filter(({ edge }) => edge.status === "running").length; - if (relationshipRows.length === 0) { + if (relationshipRows.length === 0 && runningCount === 0) { return null; } @@ -285,7 +288,7 @@ export function ThreadRelationshipsPanel(props: { return ( 0 ? `Lineage · ${runningCount} running` : "Lineage"} data-thread-relationships-panel actions={ canDetach ? ( @@ -389,6 +392,7 @@ export function ThreadRelationshipsPanel(props: {