diff --git a/ui/src/ui/chat/slash-command-executor.node.test.ts b/ui/src/ui/chat/slash-command-executor.node.test.ts index ad934e8446c..cbe6e926773 100644 --- a/ui/src/ui/chat/slash-command-executor.node.test.ts +++ b/ui/src/ui/chat/slash-command-executor.node.test.ts @@ -764,7 +764,7 @@ describe("executeSlashCommand /steer (soft inject)", () => { ); }); - it("ignores ended subagent sessions when resolving target", async () => { + it("keeps ended subagent targets so steer does not fall back to the current session", async () => { const request = vi.fn(async (method: string, _payload?: unknown) => { if (method === "sessions.list") { return { @@ -790,16 +790,9 @@ describe("executeSlashCommand /steer (soft inject)", () => { "researcher try again", ); - // "researcher" is ended, so the full string is sent to current session - expect(result.content).toBe("Steered."); - expect(request).toHaveBeenCalledWith( - "chat.send", - expect.objectContaining({ - sessionKey: "agent:main:main", - message: "researcher try again", - deliver: false, - }), - ); + expect(result.content).toBe("No active run matched `researcher`. Use `/redirect` instead."); + expect(request).toHaveBeenCalledWith("sessions.list", {}); + expect(request).not.toHaveBeenCalledWith("chat.send", expect.anything()); }); it("returns a no-op summary when the current session has no active run", async () => { @@ -915,6 +908,40 @@ describe("executeSlashCommand /redirect (hard kill-and-restart)", () => { }); }); + it("redirects an ended subagent instead of falling back to the current session", async () => { + const request = vi.fn(async (method: string, _payload?: unknown) => { + if (method === "sessions.list") { + return { + sessions: [ + row("agent:main:main"), + row("agent:main:subagent:researcher", { + spawnedBy: "agent:main:main", + endedAt: Date.now() - 60_000, + }), + ], + }; + } + if (method === "sessions.steer") { + return { status: "started", runId: "run-3", messageSeq: 1 }; + } + throw new Error(`unexpected method: ${method}`); + }); + + const result = await executeSlashCommand( + { request } as unknown as GatewayBrowserClient, + "agent:main:main", + "redirect", + "researcher start over completely", + ); + + expect(result.content).toBe("Redirected `researcher`."); + expect(result.trackRunId).toBeUndefined(); + expect(request).toHaveBeenCalledWith("sessions.steer", { + key: "agent:main:subagent:researcher", + message: "start over completely", + }); + }); + it("returns usage when no message is provided", async () => { const request = vi.fn(); diff --git a/ui/src/ui/chat/slash-command-executor.ts b/ui/src/ui/chat/slash-command-executor.ts index 8ed9ee676cb..a426a527258 100644 --- a/ui/src/ui/chat/slash-command-executor.ts +++ b/ui/src/ui/chat/slash-command-executor.ts @@ -642,10 +642,6 @@ function resolveSteerSubagent( if (!key || !isSubagentSessionKey(key)) { continue; } - // P1: skip ended sessions so stale subagents are not targeted - if (session.endedAt) { - continue; - } const normalizedKey = key.toLowerCase(); const parsed = parseAgentSessionKey(normalizedKey); const belongsToCurrentSession = isWithinCurrentSessionSubtree( @@ -675,6 +671,10 @@ function resolveSteerSubagent( * Resolve an optional subagent target from the first word of args. * Returns the resolved session key and the remaining message, or * falls back to the current session key with the full args as message. + * + * Ended subagents are still resolved here so explicit `/steer ...` + * can surface the correct "No active run matched" message and `/redirect ...` + * can restart that specific session instead of silently steering the current one. */ async function resolveSteerTarget( client: GatewayBrowserClient,