diff --git a/packages/dashboard/app/components/TaskChatTab.tsx b/packages/dashboard/app/components/TaskChatTab.tsx index 3d8421e85f..cdd039702a 100644 --- a/packages/dashboard/app/components/TaskChatTab.tsx +++ b/packages/dashboard/app/components/TaskChatTab.tsx @@ -175,17 +175,20 @@ function isActiveAgentSession(task: Task | TaskDetail, opts: { sessionLive?: boo // In the default ephemeral-agents mode the scheduler never writes // `assignedAgentId`/`checkedOutBy` — those are only set when // `ephemeralAgentsEnabled === false` (scheduler.ts). An actively-executing - // in-progress task therefore has no assignment field yet IS being worked, so - // requiring `hasAssignedAgent` made the chat always show "no agent is working" - // for default-mode tasks. Treat assignment as sufficient-but-not-necessary: - // a non-blocked, non-`queued` in-progress task is a live session on its own. - // `queued` is the documented waiting/blocked marker (self-healing.ts), so it - // stays assignment-gated; in-review keeps requiring an assignment. - const inProgressExecuting = task.column === "in-progress" - && statusAllowsProgressSteering - && task.status !== "queued"; + // task therefore has no assignment field yet IS being worked, so requiring + // `hasAssignedAgent` made the chat always show "no agent is working" for + // default-mode tasks. Treat assignment as sufficient-but-not-necessary: + // - in-progress with a non-blocked, non-`queued` status is an executing run + // (`queued` is the documented waiting marker, self-healing.ts — it stays + // assignment-gated); + // - in-review with an active review/merge status (REVIEW_STEERABLE_STATUSES) + // has a reviewer/merger running. A null-status in-review row is awaiting + // human review, not actively worked, so it stays assignment-gated and idle. + const executionImpliesActiveAgent = + (task.column === "in-progress" && statusAllowsProgressSteering && task.status !== "queued") + || (task.column === "in-review" && task.status != null && REVIEW_STEERABLE_STATUSES.has(task.status)); return columnAllowsSteering - && (hasAssignedAgent || inProgressExecuting); + && (hasAssignedAgent || executionImpliesActiveAgent); } function isToolLikeEntry(entry: AgentLogEntry): boolean { diff --git a/packages/dashboard/app/components/__tests__/TaskChatTab.test.tsx b/packages/dashboard/app/components/__tests__/TaskChatTab.test.tsx index ea046c0cba..92e246addd 100644 --- a/packages/dashboard/app/components/__tests__/TaskChatTab.test.tsx +++ b/packages/dashboard/app/components/__tests__/TaskChatTab.test.tsx @@ -1976,7 +1976,6 @@ describe("TaskChatTab", () => { ["in-progress task without an assigned or checked-out agent", makeTask({ column: "in-progress", status: "queued", assignedAgentId: undefined, checkedOutBy: undefined })], ["paused in-progress task", makeTask({ column: "in-progress", status: "queued", paused: true })], ["user-paused in-progress task", makeTask({ column: "in-progress", status: "queued", userPaused: true })], - ["in-review task without an assigned or checked-out agent", makeTask({ column: "in-review", status: "reviewing", assignedAgentId: undefined, checkedOutBy: undefined })], ["paused in-review task", makeTask({ column: "in-review", status: "reviewing", paused: true })], ["user-paused in-review task", makeTask({ column: "in-review", status: "reviewing", userPaused: true })], ])("keeps the composer sendable with idle guidance for %s", (_label, task) => { @@ -1986,6 +1985,39 @@ describe("TaskChatTab", () => { expectComposerSendableAfterDraft(); }); + it.each(["reviewing", "merging", "merging-fix", "fixing"])( + "treats an actively-reviewing in-review task as an active session even without an assignment (ephemeral mode): %s status", + (status) => { + // A reviewer/merger runs ephemerally with no assignedAgentId/checkedOutBy, + // so an in-review task in an active review/merge status must NOT show the + // idle "no agent is working" hint. + render( + , + ); + + expectActiveSessionCopy(); + expect(screen.getByLabelText("Message active agent session")).not.toBeDisabled(); + }, + ); + + it("keeps a null-status in-review task (awaiting human review) idle without an assignment", () => { + render( + , + ); + + expectIdleSessionHint(); + }); + it.each([ ["paused in-progress task with a live session", makeTask({ column: "in-progress", status: "queued", paused: true })], ["user-paused in-progress task with a live session", makeTask({ column: "in-progress", status: "queued", userPaused: true })],