| 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.tsx b/apps/web/src/components/chat/ThreadRelationshipsControl.tsx
index 827a7697294a..ad8923941054 100644
--- a/apps/web/src/components/chat/ThreadRelationshipsControl.tsx
+++ b/apps/web/src/components/chat/ThreadRelationshipsControl.tsx
@@ -389,6 +389,7 @@ export function ThreadRelationshipsPanel(props: {
Date: Fri, 18 Sep 2026 20:01:15 +0200
Subject: [PATCH 2/4] feat(web): show running subagent count in Lineage
---
.../chat/ThreadRelationshipsControl.test.tsx | 52 ++++++++++++++++++-
.../chat/ThreadRelationshipsControl.tsx | 3 +-
2 files changed, 52 insertions(+), 3 deletions(-)
diff --git a/apps/web/src/components/chat/ThreadRelationshipsControl.test.tsx b/apps/web/src/components/chat/ThreadRelationshipsControl.test.tsx
index 2d4ea8cc1f56..bad96fe51ed8 100644
--- a/apps/web/src/components/chat/ThreadRelationshipsControl.test.tsx
+++ b/apps/web/src/components/chat/ThreadRelationshipsControl.test.tsx
@@ -1,7 +1,26 @@
-import { describe, expect, it } from "vite-plus/test";
+import { describe, expect, it, vi } from "vite-plus/test";
import { renderToStaticMarkup } from "react-dom/server";
+import { ThreadId } from "@t3tools/contracts";
-import { resolveThreadLineageWindow, ThreadLineageRowList } from "./ThreadRelationshipsControl";
+import { makeThreadFixture } from "../../test-fixtures";
+import {
+ resolveThreadLineageWindow,
+ ThreadLineageRowList,
+ ThreadRelationshipsPanel,
+} from "./ThreadRelationshipsControl";
+
+const state = vi.hoisted(() => ({ threads: [] as ReturnType[] }));
+vi.mock("../../state/entities", () => ({
+ useThreadProjection: () => null,
+ useThreadShells: () => state.threads,
+ useProjects: () => [],
+ useServerConfigs: () => new Map(),
+}));
+vi.mock("../../lib/archivedThreadsState", () => ({
+ useArchivedThreadSnapshots: () => ({ snapshots: [] }),
+}));
+vi.mock("@tanstack/react-router", () => ({ useNavigate: () => vi.fn() }));
+vi.mock("../../state/use-atom-command", () => ({ useAtomCommand: () => vi.fn() }));
const rows = Array.from({ length: 20 }, (_, index) => `row-${index}`);
@@ -17,6 +36,35 @@ function renderRowList(visibleCount: number) {
}
describe("thread lineage row list", () => {
+ it.each([0, 1, 8])("counts %i running children across all pages", (runningCount) => {
+ const parent = makeThreadFixture();
+ state.threads = [
+ parent,
+ ...Array.from({ length: runningCount + 2 }, (_, index) => {
+ const child = makeThreadFixture({
+ id: ThreadId.make(`child-${index}`),
+ lineage: {
+ rootThreadId: parent.id,
+ parentThreadId: parent.id,
+ relationshipToParent: index === runningCount + 1 ? "fork" : "subagent",
+ },
+ });
+ return {
+ ...child,
+ source: {
+ ...child.source,
+ status: index === runningCount ? ("idle" as const) : ("running" as const),
+ },
+ };
+ }),
+ ];
+ const markup = renderToStaticMarkup(
+ ,
+ );
+ const heading = /]*>(.*?)<\/h3>/.exec(markup)?.[1];
+ expect(heading).toBe(runningCount ? `Lineage · ${runningCount} running` : "Lineage");
+ });
+
it("shows six rows before the first expansion", () => {
const { visibleRows, hiddenCount } = resolveThreadLineageWindow(rows, 6);
diff --git a/apps/web/src/components/chat/ThreadRelationshipsControl.tsx b/apps/web/src/components/chat/ThreadRelationshipsControl.tsx
index ad8923941054..9b40ce1ea36e 100644
--- a/apps/web/src/components/chat/ThreadRelationshipsControl.tsx
+++ b/apps/web/src/components/chat/ThreadRelationshipsControl.tsx
@@ -240,6 +240,7 @@ export function ThreadRelationshipsPanel(props: {
{ id: "active", label: null, rows: active, expanded: true },
{ id: "previous", label: "Previous agents", rows: previous, expanded: false },
];
+ const runningCount = active.filter(({ edge }) => edge.status === "running").length;
if (relationshipRows.length === 0) {
return null;
@@ -285,7 +286,7 @@ export function ThreadRelationshipsPanel(props: {
return (
0 ? `Lineage · ${runningCount} running` : "Lineage"}
data-thread-relationships-panel
actions={
canDetach ? (
From 93fb0c95abf39ca560c4ba6ed923bbd1d4fb2a6c Mon Sep 17 00:00:00 2001
From: Bil0000 <62337003+Bil0000@users.noreply.github.com>
Date: Fri, 18 Sep 2026 20:09:42 +0200
Subject: [PATCH 3/4] test(web): cover subagent models and counts in existing
panel tests
---
.../chat/SubagentTooltipContent.test.tsx | 63 -------------------
...ThreadRelationshipsControl.agents.test.tsx | 37 ++++++++++-
.../chat/ThreadRelationshipsControl.test.tsx | 52 +--------------
.../chat/ThreadRelationshipsControl.tsx | 4 +-
4 files changed, 40 insertions(+), 116 deletions(-)
delete mode 100644 apps/web/src/components/chat/SubagentTooltipContent.test.tsx
diff --git a/apps/web/src/components/chat/SubagentTooltipContent.test.tsx b/apps/web/src/components/chat/SubagentTooltipContent.test.tsx
deleted file mode 100644
index 1b86e670defc..000000000000
--- a/apps/web/src/components/chat/SubagentTooltipContent.test.tsx
+++ /dev/null
@@ -1,63 +0,0 @@
-import { ProviderDriverKind, ProviderInstanceId, type ServerProvider } from "@t3tools/contracts";
-import { act } from "react";
-import { create, type ReactTestRenderer } from "react-test-renderer";
-import { afterEach, expect, it, vi } from "vite-plus/test";
-
-import { makeThreadProjectionFixture } from "../../test-fixtures";
-import { SubagentTooltipContent } from "./SubagentTooltipContent";
-
-const provider: ServerProvider = {
- instanceId: ProviderInstanceId.make("codex"),
- driver: ProviderDriverKind.make("codex"),
- enabled: true,
- installed: true,
- version: null,
- status: "ready",
- auth: { status: "authenticated" },
- checkedAt: "2026-09-18T00:00:00.000Z",
- models: [
- {
- slug: "gpt-5.4",
- name: "OpenAI GPT-5.4",
- shortName: "OpenAI GPT-5.4",
- subProvider: "OpenAI",
- aliases: ["default-model"],
- isCustom: false,
- capabilities: {},
- },
- ],
- slashCommands: [],
- skills: [],
-};
-
-afterEach(() => vi.unstubAllGlobals());
-
-it.each([
- [null, true, "GPT-5.4"],
- ["", true, "GPT-5.4"],
- [" ", true, "GPT-5.4"],
- ["default-model", false, "GPT-5.4"],
- ["gpt-5.4", false, "GPT-5.4"],
- ["gpt-5.5", true, "GPT-5.5"],
- ["custom/model-v1", true, "custom/model-v1"],
- [null, false, "Not reported"],
-] as const)("displays model %s with child=%s as %s", async (model, hasChild, expected) => {
- vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true);
- let renderer!: ReactTestRenderer;
- await act(async () => {
- renderer = create(
- ,
- );
- });
- try {
- expect(renderer.root.findAllByType("dd")[0]?.children.join("")).toBe(expected);
- } finally {
- await act(async () => renderer.unmount());
- }
-});
diff --git a/apps/web/src/components/chat/ThreadRelationshipsControl.agents.test.tsx b/apps/web/src/components/chat/ThreadRelationshipsControl.agents.test.tsx
index 8549f452ef4e..a0ccd67e66ed 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) => ({
@@ -176,6 +189,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 +208,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 +258,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 +350,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.test.tsx b/apps/web/src/components/chat/ThreadRelationshipsControl.test.tsx
index bad96fe51ed8..2d4ea8cc1f56 100644
--- a/apps/web/src/components/chat/ThreadRelationshipsControl.test.tsx
+++ b/apps/web/src/components/chat/ThreadRelationshipsControl.test.tsx
@@ -1,26 +1,7 @@
-import { describe, expect, it, vi } from "vite-plus/test";
+import { describe, expect, it } from "vite-plus/test";
import { renderToStaticMarkup } from "react-dom/server";
-import { ThreadId } from "@t3tools/contracts";
-import { makeThreadFixture } from "../../test-fixtures";
-import {
- resolveThreadLineageWindow,
- ThreadLineageRowList,
- ThreadRelationshipsPanel,
-} from "./ThreadRelationshipsControl";
-
-const state = vi.hoisted(() => ({ threads: [] as ReturnType[] }));
-vi.mock("../../state/entities", () => ({
- useThreadProjection: () => null,
- useThreadShells: () => state.threads,
- useProjects: () => [],
- useServerConfigs: () => new Map(),
-}));
-vi.mock("../../lib/archivedThreadsState", () => ({
- useArchivedThreadSnapshots: () => ({ snapshots: [] }),
-}));
-vi.mock("@tanstack/react-router", () => ({ useNavigate: () => vi.fn() }));
-vi.mock("../../state/use-atom-command", () => ({ useAtomCommand: () => vi.fn() }));
+import { resolveThreadLineageWindow, ThreadLineageRowList } from "./ThreadRelationshipsControl";
const rows = Array.from({ length: 20 }, (_, index) => `row-${index}`);
@@ -36,35 +17,6 @@ function renderRowList(visibleCount: number) {
}
describe("thread lineage row list", () => {
- it.each([0, 1, 8])("counts %i running children across all pages", (runningCount) => {
- const parent = makeThreadFixture();
- state.threads = [
- parent,
- ...Array.from({ length: runningCount + 2 }, (_, index) => {
- const child = makeThreadFixture({
- id: ThreadId.make(`child-${index}`),
- lineage: {
- rootThreadId: parent.id,
- parentThreadId: parent.id,
- relationshipToParent: index === runningCount + 1 ? "fork" : "subagent",
- },
- });
- return {
- ...child,
- source: {
- ...child.source,
- status: index === runningCount ? ("idle" as const) : ("running" as const),
- },
- };
- }),
- ];
- const markup = renderToStaticMarkup(
- ,
- );
- const heading = /]*>(.*?)<\/h3>/.exec(markup)?.[1];
- expect(heading).toBe(runningCount ? `Lineage · ${runningCount} running` : "Lineage");
- });
-
it("shows six rows before the first expansion", () => {
const { visibleRows, hiddenCount } = resolveThreadLineageWindow(rows, 6);
diff --git a/apps/web/src/components/chat/ThreadRelationshipsControl.tsx b/apps/web/src/components/chat/ThreadRelationshipsControl.tsx
index 9b40ce1ea36e..6d91c8fdc3a9 100644
--- a/apps/web/src/components/chat/ThreadRelationshipsControl.tsx
+++ b/apps/web/src/components/chat/ThreadRelationshipsControl.tsx
@@ -240,7 +240,9 @@ export function ThreadRelationshipsPanel(props: {
{ id: "active", label: null, rows: active, expanded: true },
{ id: "previous", label: "Previous agents", rows: previous, expanded: false },
];
- const runningCount = active.filter(({ edge }) => edge.status === "running").length;
+ const runningCount =
+ projection?.subagents.filter((agent) => agent.status === "running").length ??
+ active.filter(({ edge }) => edge.status === "running").length;
if (relationshipRows.length === 0) {
return null;
From ebefc29b268b079df74b11c54684717b81a8a72e Mon Sep 17 00:00:00 2001
From: Bil0000 <62337003+Bil0000@users.noreply.github.com>
Date: Fri, 18 Sep 2026 20:11:28 +0200
Subject: [PATCH 4/4] fix(web): keep running count visible before child threads
arrive
---
.../chat/ThreadRelationshipsControl.agents.test.tsx | 7 +++++++
.../web/src/components/chat/ThreadRelationshipsControl.tsx | 2 +-
2 files changed, 8 insertions(+), 1 deletion(-)
diff --git a/apps/web/src/components/chat/ThreadRelationshipsControl.agents.test.tsx b/apps/web/src/components/chat/ThreadRelationshipsControl.agents.test.tsx
index a0ccd67e66ed..1e7dd8fc0436 100644
--- a/apps/web/src/components/chat/ThreadRelationshipsControl.agents.test.tsx
+++ b/apps/web/src/components/chat/ThreadRelationshipsControl.agents.test.tsx
@@ -171,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 () => {
diff --git a/apps/web/src/components/chat/ThreadRelationshipsControl.tsx b/apps/web/src/components/chat/ThreadRelationshipsControl.tsx
index 6d91c8fdc3a9..b07013bea588 100644
--- a/apps/web/src/components/chat/ThreadRelationshipsControl.tsx
+++ b/apps/web/src/components/chat/ThreadRelationshipsControl.tsx
@@ -244,7 +244,7 @@ export function ThreadRelationshipsPanel(props: {
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;
}