diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 341928737..4688f3d3e 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -24,6 +24,7 @@ ### 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. +- Stopped the task tool from using the spawn handle as the HUD description, which blocked tiny-model label generation and produced `Name: Name`. ## [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 02e08c6f7..23c995523 100644 --- a/packages/coding-agent/src/modes/interactive-mode.ts +++ b/packages/coding-agent/src/modes/interactive-mode.ts @@ -117,6 +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 { labelEchoesHandle } from "../task/label"; import { agentTypeBadge, formatTaskId } from "../task/render"; import type { ConfiguredThinkingLevel } from "../thinking"; import { tinyTitleClient } from "../tiny/title-client"; @@ -450,13 +451,6 @@ const MODEL_CYCLE_TRACK_CLEAR_MS = 4000; const SUBAGENT_HUD_VISIBLE_LIMIT = 8; const SUBAGENT_OBSERVER_UI_COALESCE_MS = 100; -/** True when `label` is the spawn handle, including collision suffixes (`Name-2`). */ -function hudLabelEchoesId(id: string, label: string): boolean { - if (label.localeCompare(id, undefined, { sensitivity: "accent" }) === 0) return true; - const suffix = id.startsWith(`${label}-`) ? id.slice(label.length + 1) : ""; - return /^\d+$/.test(suffix); -} - /** * Build the anchored subagent HUD block: a bold accent "Subagents" header plus * a bounded set of running-agent rows in the same `Id ⟨role⟩: description` shape @@ -487,7 +481,7 @@ export function renderSubagentHudLines(sessions: ObservableSession[], columns: n const badge = agentTypeBadge(role, theme); let line = `${dot} ${theme.fg("accent", theme.bold(displayId))}${badge}`; const description = session.description?.trim() || session.progress?.description?.trim(); - const distinctDescription = description && !hudLabelEchoesId(session.id, description) ? description : undefined; + const distinctDescription = description && !labelEchoesHandle(session.id, description) ? description : undefined; if (distinctDescription) { const budget = Math.max( TRUNCATE_LENGTHS.SHORT, @@ -498,7 +492,7 @@ export function renderSubagentHudLines(sessions: ObservableSession[], columns: n // 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 && !hudLabelEchoesId(session.id, taskPreview)) { + if (taskPreview && !labelEchoesHandle(session.id, taskPreview)) { line += ` ${theme.fg("muted", truncateToWidth(replaceTabs(taskPreview), TRUNCATE_LENGTHS.SHORT))}`; } } diff --git a/packages/coding-agent/src/task/index.ts b/packages/coding-agent/src/task/index.ts index a2fe5ca8b..7b59e6cf0 100644 --- a/packages/coding-agent/src/task/index.ts +++ b/packages/coding-agent/src/task/index.ts @@ -1424,6 +1424,8 @@ export class TaskTool implements AgentTool { expect(requestSignal).toBe(controller.signal); expect(await label).toBeNull(); }); + + it("rejects a generated label that only echoes the spawn handle", async () => { + const model = getModelOrThrow("claude-sonnet-4-5"); + vi.spyOn(ai, "completeSimple").mockResolvedValue({ + stopReason: "stop", + content: [{ type: "text", text: "AuthLoader" }], + } as never); + + const echoed = await generateTaskLabel( + "Sleep forty seconds then reply done", + createRegistry(model), + createSettings(model), + "AuthLoader", + ); + expect(echoed).toBeNull(); + + vi.spyOn(ai, "completeSimple").mockResolvedValue({ + stopReason: "stop", + content: [{ type: "text", text: "Sleep then reply done" }], + } as never); + const labeled = await generateTaskLabel( + "Sleep forty seconds then reply done", + createRegistry(model), + createSettings(model), + "AuthLoader", + ); + expect(labeled).toBe("Sleep then reply done"); + }); + + }); diff --git a/packages/coding-agent/test/task/structured-subagent.test.ts b/packages/coding-agent/test/task/structured-subagent.test.ts index a24facde7..9f6b0d009 100644 --- a/packages/coding-agent/test/task/structured-subagent.test.ts +++ b/packages/coding-agent/test/task/structured-subagent.test.ts @@ -186,6 +186,34 @@ describe("structured subagent primitive", () => { expect(settled.result.modelRole).toBe("reviewer"); await fs.rm(settled.artifactsDir, { recursive: true, force: true }); }); + it("does not treat a spawn handle as the HUD description", async () => { + mockDiscovery(); + const dispatched: executorModule.ExecutorOptions[] = []; + vi.spyOn(executorModule, "runSubprocess").mockImplementation(async options => { + dispatched.push(options); + return result(); + }); + + const handleOnly = await runStructuredSubagent( + request({ identity: { id: "AuthLoader", label: "AuthLoader" }, retainArtifacts: true }), + ); + expect(dispatched[0]?.description).toBeUndefined(); + expect(dispatched[0]?.id).toBe("AuthLoader"); + await fs.rm(handleOnly.artifactsDir, { recursive: true, force: true }); + + dispatched.length = 0; + const evalLabeled = await runStructuredSubagent( + request({ + invocationKind: "eval", + identity: { label: "Refactor the auth flow" }, + retainArtifacts: true, + }), + ); + expect(dispatched[0]?.description).toBe("Refactor the auth flow"); + await fs.rm(evalLabeled.artifactsDir, { recursive: true, force: true }); + }); + + it("derives modelRole from the raw selector source in request, override, definition order", async () => { const customAgent = { ...AGENT, model: ["@definition"] }; mockDiscovery(customAgent);