diff --git a/packages/tui/src/routes/session/subagent-footer.tsx b/packages/tui/src/routes/session/subagent-footer.tsx index 0aadb4f43c96..d39c2ae35462 100644 --- a/packages/tui/src/routes/session/subagent-footer.tsx +++ b/packages/tui/src/routes/session/subagent-footer.tsx @@ -3,11 +3,29 @@ import { useRouteData } from "../../context/route" import { useSync } from "../../context/sync" import { useTheme } from "../../context/theme" import { SplitBorder } from "../../ui/border" -import type { AssistantMessage } from "@opencode-ai/sdk/v2" +import type { Agent, AssistantMessage, Provider, Session } from "@opencode-ai/sdk/v2" import { Locale } from "../../util/locale" import { useTerminalDimensions } from "@opentui/solid" import { useCommandShortcut, useOpencodeKeymap } from "../../keymap" +type AgentVariantConfig = Pick +type ProviderModelVariants = Pick + +export function resolveSubagentVariant( + sessionModel: Session["model"], + agent: AgentVariantConfig | undefined, + model: ProviderModelVariants | undefined, +) { + const persisted = sessionModel?.variant + if (persisted && persisted !== "default") return persisted + if (!sessionModel || !agent?.model || !model?.variants) return undefined + if (agent.model.providerID !== sessionModel.providerID || agent.model.modelID !== sessionModel.id) return undefined + if (agent.variant && agent.variant in model.variants) return agent.variant + const effort = agent.options.reasoningEffort + if (typeof effort === "string" && effort in model.variants) return effort + return undefined +} + export function SubagentFooter() { const route = useRouteData("session") const sync = useSync() @@ -30,6 +48,20 @@ export function SubagentFooter() { return { label, index: index + 1, total: siblings.length } }) + const modelInfo = createMemo(() => { + const current = session() + if (!current?.model) return undefined + const sessionModel = current.model + const provider = sync.data.provider.find((item) => item.id === sessionModel.providerID) + const model = provider?.models[sessionModel.id] + const agent = sync.data.agent.find((item) => item.name === current.agent) + return { + model: model?.name ?? sessionModel.id, + provider: provider?.name ?? sessionModel.providerID, + variant: resolveSubagentVariant(sessionModel, agent, model), + } + }) + const usage = createMemo(() => { const msg = messages() const last = msg.findLast((item): item is AssistantMessage => item.role === "assistant" && item.tokens.output > 0) @@ -75,54 +107,80 @@ export function SubagentFooter() { flexShrink={0} backgroundColor={theme.backgroundPanel} > - - - - {subagentInfo().label} - - 0}> - - ({subagentInfo().index} of {subagentInfo().total}) - - - - {(item) => ( + + + {(item) => ( + + + {item().model} + - {[item().context, item().cost].filter(Boolean).join(" · ")} + {item().provider} - )} - - - - setHover("parent")} - onMouseOut={() => setHover(null)} - onMouseUp={() => keymap.dispatchCommand("session.parent")} - backgroundColor={hover() === "parent" ? theme.backgroundElement : theme.backgroundPanel} - > + + {(variant) => ( + <> + + · + + + {variant()} + + + )} + + + )} + + + - Parent {parentShortcut()} - - - setHover("prev")} - onMouseOut={() => setHover(null)} - onMouseUp={() => keymap.dispatchCommand("session.child.previous")} - backgroundColor={hover() === "prev" ? theme.backgroundElement : theme.backgroundPanel} - > - - Prev {previousShortcut()} + {subagentInfo().label} + 0}> + + ({subagentInfo().index} of {subagentInfo().total}) + + + + {(item) => ( + + {[item().context, item().cost].filter(Boolean).join(" · ")} + + )} + - setHover("next")} - onMouseOut={() => setHover(null)} - onMouseUp={() => keymap.dispatchCommand("session.child.next")} - backgroundColor={hover() === "next" ? theme.backgroundElement : theme.backgroundPanel} - > - - Next {nextShortcut()} - + + setHover("parent")} + onMouseOut={() => setHover(null)} + onMouseUp={() => keymap.dispatchCommand("session.parent")} + backgroundColor={hover() === "parent" ? theme.backgroundElement : theme.backgroundPanel} + > + + Parent {parentShortcut()} + + + setHover("prev")} + onMouseOut={() => setHover(null)} + onMouseUp={() => keymap.dispatchCommand("session.child.previous")} + backgroundColor={hover() === "prev" ? theme.backgroundElement : theme.backgroundPanel} + > + + Prev {previousShortcut()} + + + setHover("next")} + onMouseOut={() => setHover(null)} + onMouseUp={() => keymap.dispatchCommand("session.child.next")} + backgroundColor={hover() === "next" ? theme.backgroundElement : theme.backgroundPanel} + > + + Next {nextShortcut()} + + diff --git a/packages/tui/test/cli/tui/subagent-footer.test.ts b/packages/tui/test/cli/tui/subagent-footer.test.ts new file mode 100644 index 000000000000..720f14558c22 --- /dev/null +++ b/packages/tui/test/cli/tui/subagent-footer.test.ts @@ -0,0 +1,93 @@ +import { describe, expect, test } from "bun:test" +import { resolveSubagentVariant } from "../../../src/routes/session/subagent-footer" + +const sessionModel = { + id: "gpt-5.6-sol", + providerID: "openai", + variant: "default", +} + +const model = { + variants: { + medium: { reasoningEffort: "medium" }, + high: { reasoningEffort: "high" }, + xhigh: { reasoningEffort: "xhigh" }, + }, +} + +describe("subagent footer variant", () => { + test("prefers the persisted non-default variant", () => { + const result = resolveSubagentVariant({ ...sessionModel, variant: "xhigh" }, undefined, undefined) + + expect(result).toBe("xhigh") + }) + + test("uses the matching agent variant for a legacy session", () => { + const result = resolveSubagentVariant( + sessionModel, + { + model: { providerID: "openai", modelID: "gpt-5.6-sol" }, + variant: "high", + options: {}, + }, + model, + ) + + expect(result).toBe("high") + }) + + test("falls back to a supported reasoning effort", () => { + const result = resolveSubagentVariant( + sessionModel, + { + model: { providerID: "openai", modelID: "gpt-5.6-sol" }, + options: { reasoningEffort: "medium" }, + }, + model, + ) + + expect(result).toBe("medium") + }) + + test("omits configuration for a different model", () => { + const result = resolveSubagentVariant( + sessionModel, + { + model: { providerID: "openai", modelID: "gpt-5.6-terra" }, + variant: "high", + options: { reasoningEffort: "medium" }, + }, + model, + ) + + expect(result).toBeUndefined() + }) + + test("omits unsupported configured levels", () => { + const result = resolveSubagentVariant( + sessionModel, + { + model: { providerID: "openai", modelID: "gpt-5.6-sol" }, + variant: "max", + options: { reasoningEffort: "minimal" }, + }, + model, + ) + + expect(result).toBeUndefined() + }) + + test("falls through an unsupported agent variant to reasoning effort", () => { + const result = resolveSubagentVariant( + sessionModel, + { + model: { providerID: "openai", modelID: "gpt-5.6-sol" }, + variant: "max", + options: { reasoningEffort: "high" }, + }, + model, + ) + + expect(result).toBe("high") + }) +})