From 72e3316f4f5dfe775c77dbf998968c92e2de9019 Mon Sep 17 00:00:00 2001 From: "paseo-bot[bot]" <266920839+paseo-bot[bot]@users.noreply.github.com> Date: Sat, 24 Oct 2026 13:58:06 +0000 Subject: [PATCH] fix(opencode): show model details on v2 subagent rows (#7799) The v2 child descriptor omitted the session's presentation facts, leaving completed children with title-only rows. Reuse the existing subtitle formatter for both live updates and reconciliation. Co-authored-by: paseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com> Co-authored-by: Davin <128177533+Davnn1@users.noreply.github.com> --- .../providers/opencode/v2/children.test.ts | 80 +++++++++++++++++++ .../agent/providers/opencode/v2/children.ts | 13 +++ 2 files changed, 93 insertions(+) create mode 100644 packages/server/src/server/agent/providers/opencode/v2/children.test.ts diff --git a/packages/server/src/server/agent/providers/opencode/v2/children.test.ts b/packages/server/src/server/agent/providers/opencode/v2/children.test.ts new file mode 100644 index 0000000000..21e2520f80 --- /dev/null +++ b/packages/server/src/server/agent/providers/opencode/v2/children.test.ts @@ -0,0 +1,80 @@ +import type { SessionInfo } from "@opencode/client"; +import { describe, expect, test } from "vitest"; + +import type { AgentStreamEvent } from "../../../agent-sdk-types.js"; +import { V2Harness } from "../test-utils/v2-harness.js"; +import { SessionChildren } from "./children.js"; + +function setup() { + const harness = new V2Harness(); + const child: SessionInfo = { + ...harness.info, + id: "child", + parentID: "session", + title: "Check the worker model", + agent: "worker", + model: { providerID: "test", id: "worker-model", variant: "xhigh" }, + tokens: { input: 10000, output: 3000, reasoning: 2000, cache: { read: 3000, write: 800 } }, + }; + harness.api.session.list = async (input) => ({ + data: input?.parentID === "session" ? [child] : [], + cursor: {}, + }); + harness.api.session.get = async () => child; + const events: AgentStreamEvent[] = []; + const children = new SessionChildren({ + client: () => harness.api, + id: "session", + emit: (event) => events.push(event), + reconcilePermissions: async () => {}, + }); + const upserts = () => + events.flatMap((event) => + event.type === "provider_subagent" && event.event.type === "upsert" ? [event.event] : [], + ); + return { children, child, upserts }; +} + +describe("OpenCode v2 child presentation", () => { + test("restores agent, model, variant and all token categories in the child subtitle", async () => { + const { children, upserts } = setup(); + await children.reconcile("session"); + expect(upserts()).toEqual([ + expect.objectContaining({ + id: "child", + title: "Check the worker model", + parentSubagentId: null, + status: "completed", + subtitle: "worker · worker-model · Extra High · 18.8k tokens", + }), + ]); + await children.reconcile("session"); + expect(upserts()).toHaveLength(1); + }); + + test("publishes changed child facts on live events even when its timestamp is unchanged", async () => { + const { children, child, upserts } = setup(); + await children.observe({ id: "created", created: 1, type: "session.created", data: child }); + child.model = { providerID: "test", id: "next-model", variant: "low" }; + child.tokens.output += 1200; + await children.observe({ + id: "finished", + created: 2, + type: "session.execution.succeeded", + durable: { aggregateID: "child", seq: 1, version: 1 }, + data: { sessionID: "child" }, + }); + expect(upserts().map((event) => event.subtitle)).toEqual([ + "worker · worker-model · Extra High · 18.8k tokens", + "worker · next-model · Low · 20k tokens", + ]); + }); + + test("keeps available agent information before a child has a model or token usage", async () => { + const { children, child, upserts } = setup(); + delete child.model; + child.tokens = { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } }; + await children.reconcile("session"); + expect(upserts()[0]).toMatchObject({ title: "Check the worker model", subtitle: "worker" }); + }); +}); diff --git a/packages/server/src/server/agent/providers/opencode/v2/children.ts b/packages/server/src/server/agent/providers/opencode/v2/children.ts index 95853b71c7..8c2e0ec312 100644 --- a/packages/server/src/server/agent/providers/opencode/v2/children.ts +++ b/packages/server/src/server/agent/providers/opencode/v2/children.ts @@ -7,6 +7,7 @@ import type { AgentStreamEvent } from "../../../agent-sdk-types.js"; import type { ProviderSubagentInputEvent } from "../../../provider-subagents/store.js"; import { messages } from "./history.js"; +import { buildOpenCodeSubagentSubtitle } from "../subagent-presentation.js"; import type { V2Api } from "./api.js"; interface ChildrenOptions { @@ -65,11 +66,23 @@ export class SessionChildren { if (active[info.id]) status = "running"; else if (info.outcome === "failed") status = "failed"; else if (info.outcome === "interrupted") status = "canceled"; + const subtitle = buildOpenCodeSubagentSubtitle({ + agentName: info.agent, + modelId: info.model?.id, + variant: info.model?.variant, + totalTokens: + info.tokens.input + + info.tokens.output + + info.tokens.reasoning + + info.tokens.cache.read + + info.tokens.cache.write, + }); const presentation: ProviderSubagentInputEvent = { type: "upsert", id: info.id, parentSubagentId: info.parentID === this.options.id ? null : info.parentID, title: info.title ?? null, + ...(subtitle ? { subtitle } : {}), status, cwd: info.location.directory, timestamp: new Date(info.time.updated).toISOString(),