diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 2f7b500ff..341928737 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -21,6 +21,10 @@ ### Fixed - Fixed `omp stats` and `/stats` dashboards being unreachable from container hosts by accepting an explicit `--host` bind address while preserving the `127.0.0.1` default. +### Fixed + +- Fixed the anchored Subagents HUD omitting the spawned agent role and repeating the spawn id as `Name: Name` when the generated label echoed the handle. + ## [17.3.5] - 2026-08-16 diff --git a/packages/coding-agent/src/modes/interactive-mode.ts b/packages/coding-agent/src/modes/interactive-mode.ts index ec6eb9c6b..2cb0b2487 100644 --- a/packages/coding-agent/src/modes/interactive-mode.ts +++ b/packages/coding-agent/src/modes/interactive-mode.ts @@ -117,7 +117,7 @@ import { BUILTIN_SLASH_COMMAND_RESERVED_NAMES, buildTuiBuiltinSlashCommands } fr import { formatDuration } from "../slash-commands/helpers/format"; import { STTController, type SttState } from "../stt"; import { discoverTitleSystemPromptFile, resolvePromptInput } from "../system-prompt"; -import { formatTaskId } from "../task/render"; +import { agentTypeBadge, formatTaskId } from "../task/render"; import type { ConfiguredThinkingLevel } from "../thinking"; import { tinyTitleClient } from "../tiny/title-client"; import type { LspStartupServerInfo } from "../tools"; @@ -452,8 +452,8 @@ const SUBAGENT_OBSERVER_UI_COALESCE_MS = 100; /** * Build the anchored subagent HUD block: a bold accent "Subagents" header plus - * a bounded set of running-agent rows in the same `Id: description` shape the - * inline task rows use (muted task preview when no description was given). + * a bounded set of running-agent rows in the same `Id ⟨role⟩: description` shape + * the inline task rows use (muted task preview when no description was given). * Layout mirrors the Todos HUD exactly: unindented header, then * `renderTreeList` rows (dim connectors) shifted right by one space. * Only detached background spawns are listed: a sync task call blocks the @@ -476,16 +476,25 @@ export function renderSubagentHudLines(sessions: ObservableSession[], columns: n expanded: true, renderItem: session => { const displayId = formatTaskId(session.id); - let line = `${dot} ${theme.fg("accent", theme.bold(displayId))}`; + const role = session.agent ?? session.progress?.agent; + const badge = agentTypeBadge(role, theme); + let line = `${dot} ${theme.fg("accent", theme.bold(displayId))}${badge}`; const description = session.description?.trim() || session.progress?.description?.trim(); - if (description) { - const budget = Math.max(TRUNCATE_LENGTHS.SHORT, columns - visibleWidth(displayId) - 10); - line += `${theme.fg("accent", ":")} ${theme.fg("accent", truncateToWidth(replaceTabs(description), budget))}`; + const distinctDescription = + description && description.localeCompare(session.id, undefined, { sensitivity: "accent" }) !== 0 + ? description + : undefined; + if (distinctDescription) { + const budget = Math.max( + TRUNCATE_LENGTHS.SHORT, + columns - visibleWidth(displayId) - visibleWidth(Bun.stripANSI(badge)) - 10, + ); + line += `${theme.fg("accent", ":")} ${theme.fg("accent", truncateToWidth(replaceTabs(distinctDescription), budget))}`; } else { // No spawn description: fall back to a muted task preview, same as // the inline task rows when a row has no label. const taskPreview = session.progress?.task?.trim(); - if (taskPreview) { + if (taskPreview && taskPreview.localeCompare(session.id, undefined, { sensitivity: "accent" }) !== 0) { line += ` ${theme.fg("muted", truncateToWidth(replaceTabs(taskPreview), TRUNCATE_LENGTHS.SHORT))}`; } } diff --git a/packages/coding-agent/src/task/render.ts b/packages/coding-agent/src/task/render.ts index 585e4e740..52ca6fe43 100644 --- a/packages/coding-agent/src/task/render.ts +++ b/packages/coding-agent/src/task/render.ts @@ -710,7 +710,7 @@ function formatAgentHeaderLabel(args: Partial | undefined): string | } /** Dim `⟨agent⟩` badge for a non-default agent type; empty for the generic worker. */ -function agentTypeBadge(agent: string | undefined, theme: Theme): string { +export function agentTypeBadge(agent: string | undefined, theme: Theme): string { const trimmed = agent?.trim(); if (!trimmed || trimmed === "task") return ""; return ` ${theme.fg("dim", `${theme.format.bracketLeft}${trimmed}${theme.format.bracketRight}`)}`; diff --git a/packages/coding-agent/test/subagent-hud-render.test.ts b/packages/coding-agent/test/subagent-hud-render.test.ts index 4f1929b30..2157b5717 100644 --- a/packages/coding-agent/test/subagent-hud-render.test.ts +++ b/packages/coding-agent/test/subagent-hud-render.test.ts @@ -107,6 +107,34 @@ describe("subagent HUD lines", () => { expect(out).toContain("SchemaMigrator: Migrating the users table"); }); + it("shows a non-default role badge and hides descriptions that only echo the id", () => { + const withRole = render([ + makeSession({ + id: "HindsightMcpFunnel", + agent: "scout", + description: "Audit MCP funnel wiring", + }), + ]); + expect(withRole).toContain("HindsightMcpFunnel"); + expect(withRole).toMatch(/HindsightMcpFunnel.*scout/); + expect(withRole).toContain("Audit MCP funnel wiring"); + + const echoed = render([ + makeSession({ + id: "HindsightMcpFunnel", + agent: "scout", + description: "HindsightMcpFunnel", + }), + ]); + expect(echoed).toContain("HindsightMcpFunnel"); + expect(echoed).toMatch(/HindsightMcpFunnel.*scout/); + expect(echoed).not.toContain("HindsightMcpFunnel: HindsightMcpFunnel"); + + const defaultWorker = render([makeSession({ id: "AuthLoader", agent: "task", description: "Refactor auth" })]); + expect(defaultWorker).toContain("AuthLoader: Refactor auth"); + expect(defaultWorker).not.toMatch(/AuthLoader.*task/); + }); + it("only shows active subagents and clears once everything finished", () => { const finishedStates = ["completed", "failed", "aborted"] as const; const sessions: ObservableSession[] = [