diff --git a/docs/dashboard-guide.md b/docs/dashboard-guide.md
index 26b31190b8..60c01f7dad 100644
--- a/docs/dashboard-guide.md
+++ b/docs/dashboard-guide.md
@@ -844,7 +844,7 @@ Recommended workflow: ordinary chains stay as `Blocks N` so noise stays low, hig
### Logs → Agent Log view
-The **Chat** tab sits between Definition and Logs and presents a live, chat-styled transcript of task agent output. Consecutive entries are grouped by role and labeled as Planner, Executor, Reviewer, or Merger; legacy log rows without an agent role use the neutral Agent fallback. Agent group headers and user message headers show a small muted relative timestamp (for example, “just now”, “1m ago”, or “2h ago”) based on the transcript timestamp, while agent group metadata still includes the entry count. Consecutive text/message chunks inside a role group render as one continuous markdown bubble, while consecutive tool/tool-result/tool-error rows collapse into one expandable, compact tool-call summary that stays collapsed by default; the summary counts tool invocations, lists deduped tool names with overflow, and shows an error count when failures are present, while the expanded body pairs each call with its result or error in dense entry cards. Thinking entries render in a collapsible block that starts expanded. The transcript opens at the latest output whenever the tab loads or becomes active, then follows new live output when you are already near the bottom while preserving your scroll position when you review older messages. When older task-agent history exists, scrolling to the top or selecting **Load previous messages** prepends earlier transcript entries without moving the message you were reading. When you scroll away from the bottom of a populated transcript, a sticky **Latest** button appears inside the transcript so you can jump back to the newest message and resume live follow. For non-`done` tasks, the composer sends guidance through the same steering path used by comments, including active assigned `in-progress`/`in-review` sessions and messages queued when no session is currently live. When no active or steerable agent session will reply immediately, the composer shows an idle warning hint that no agent is currently working on the task and that the sent message is saved for the next run; the input and Send button remain usable. On a `done` task, sending a Chat message starts a refinement task using the typed text as feedback and shows a success toast with the new task ID; the current task detail modal remains on the completed task. The task-detail Chat tab keeps the composer pinned and visible on mobile and desktop while the transcript scrolls internally; its textarea placeholder reads “Steer the currently executing agent” for steering mode and switches to refinement copy for completed tasks, with the same inline, icon-only send affordance to the right of the input at every breakpoint. In the composer, plain **Enter** sends, **Shift+Enter** inserts a newline, and **Cmd/Ctrl+Enter** remains a supported send shortcut.
+The **Chat** tab sits between Definition and Logs and presents a live, chat-styled transcript of task agent output. Consecutive entries are grouped by role and labeled as Planner, Executor, Reviewer, or Merger; legacy log rows without an agent role use the neutral Agent fallback. Agent group headers and user message headers show a small muted relative timestamp (for example, “just now”, “1m ago”, or “2h ago”) based on the transcript timestamp, while agent group metadata still includes the entry count. Consecutive text/message chunks inside a role group render as one continuous markdown bubble, while consecutive tool/tool-result/tool-error rows collapse into one expandable, compact tool-call summary that stays collapsed by default; the summary counts tool invocations, lists deduped tool names with overflow, and shows an error count when failures are present, while the expanded body pairs each call with its result or error in dense entry cards. Thinking entries render in a collapsible block that starts expanded. The transcript opens at the latest output whenever the tab loads or becomes active, then follows new live output when you are already near the bottom while preserving your scroll position when you review older messages. When older task-agent history exists, scrolling to the top or selecting **Load previous messages** prepends earlier transcript entries without moving the message you were reading. When you scroll away from the bottom of a populated transcript, a sticky **Latest** button appears inside the transcript so you can jump back to the newest message and resume live follow. For non-`done` tasks, the composer sends guidance through the same steering path used by comments, including active planning/triage, `in-progress`, and `in-review` sessions, plus live CLI-agent sessions reported by the session bridge; messages are still saved as queued guidance when no session is currently live. When no active or steerable agent session will reply immediately, the composer shows an idle warning hint that no agent is currently working on the task and that the sent message is saved for the next run; the input and Send button remain usable. On a `done` task, sending a Chat message starts a refinement task using the typed text as feedback and shows a success toast with the new task ID; the current task detail modal remains on the completed task. The task-detail Chat tab keeps the composer pinned and visible on mobile and desktop while the transcript scrolls internally; its textarea placeholder reads “Steer the currently executing agent” for steering mode and switches to refinement copy for completed tasks, with the same inline, icon-only send affordance to the right of the input at every breakpoint. In the composer, plain **Enter** sends, **Shift+Enter** inserts a newline, and **Cmd/Ctrl+Enter** remains a supported send shortcut.
The **Logs** tab includes an **Agent Log** subview designed for debugging long-running and tool-heavy sessions:
diff --git a/packages/dashboard/app/components/TaskChatTab.tsx b/packages/dashboard/app/components/TaskChatTab.tsx
index 667659b7ee..d002170b9b 100644
--- a/packages/dashboard/app/components/TaskChatTab.tsx
+++ b/packages/dashboard/app/components/TaskChatTab.tsx
@@ -49,6 +49,8 @@ const STEERING_BLOCKED_STATUSES = new Set([
"awaiting-user-input",
"awaiting-cli-approval",
"awaiting-user-review",
+ "awaiting-approval",
+ "awaiting-integration",
"failed",
"needs-replan",
]);
@@ -171,11 +173,16 @@ function isActiveAgentSession(task: Task | TaskDetail, opts: { sessionLive?: boo
if (task.paused || task.userPaused) return false;
if (opts.sessionLive) return true;
+ if (task.status === SCHEDULER_WAITING_STATUS) return false;
+
const hasAssignedAgent = Boolean(task.assignedAgentId || task.checkedOutBy);
const statusBlocksProgressSteering = task.status ? STEERING_BLOCKED_STATUSES.has(task.status) : false;
+ if (statusBlocksProgressSteering) return false;
+
const statusAllowsProgressSteering = !statusBlocksProgressSteering;
const statusAllowsReviewSteering = !task.status || REVIEW_STEERABLE_STATUSES.has(task.status);
- const columnAllowsSteering = (task.column === "in-progress" && statusAllowsProgressSteering)
+ const columnAllowsSteering = (task.column === "triage" && statusAllowsProgressSteering)
+ || (task.column === "in-progress" && statusAllowsProgressSteering)
|| (task.column === "in-review" && statusAllowsReviewSteering);
// FNXC:TaskDetailChat 2026-06-20-20:10:
// In the default ephemeral-agents mode the scheduler never writes
@@ -184,14 +191,21 @@ function isActiveAgentSession(task: Task | TaskDetail, opts: { sessionLive?: boo
// 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-progress with a non-blocked, non-`queued` status is an executing run;
// - 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.
+ // FNXC:TaskDetailChat 2026-06-21-13:03:
+ // Planning/triage is an execution surface too: triage.ts writes
+ // `status: "planning"` only after a planner slot is acquired, while active
+ // default-mode planner sessions still omit assignment fields. Treat non-waiting,
+ // non-blocked triage rows as active so steering copy does not falsely say no
+ // agent is working during spec generation. Keep `queued` and awaiting/failed
+ // statuses idle before assignment checks because they are waiting states, not
+ // live agent work.
const executionImpliesActiveAgent =
- (task.column === "in-progress" && statusAllowsProgressSteering && task.status !== SCHEDULER_WAITING_STATUS)
+ (task.column === "triage" && statusAllowsProgressSteering)
+ || (task.column === "in-progress" && statusAllowsProgressSteering)
|| (task.column === "in-review" && task.status != null && REVIEW_STEERABLE_STATUSES.has(task.status));
return columnAllowsSteering
&& (hasAssignedAgent || executionImpliesActiveAgent);
diff --git a/packages/dashboard/app/components/__tests__/TaskChatTab.test.tsx b/packages/dashboard/app/components/__tests__/TaskChatTab.test.tsx
index 3f058f591b..874e8e1c16 100644
--- a/packages/dashboard/app/components/__tests__/TaskChatTab.test.tsx
+++ b/packages/dashboard/app/components/__tests__/TaskChatTab.test.tsx
@@ -548,6 +548,43 @@ describe("TaskChatTab", () => {
expectIdleSessionHint();
});
+ it.each([
+ ["inline planning", false, "planning"],
+ ["expanded planning", true, "planning"],
+ ["inline cleared status", false, null],
+ ["expanded cleared status", true, null],
+ ] as const)("renders active planning guidance in the %s task chat surface", (_label, expanded, status) => {
+ render(
+ ,
+ );
+
+ expectActiveSessionCopy();
+ });
+
+ it.each([
+ ["empty", [], false, makeTask({ column: "triage", status: "planning", assignedAgentId: undefined, checkedOutBy: undefined })],
+ ["populated", [makeEntry({ agent: "triage", text: "Planner is drafting the spec" })], false, makeTask({
+ column: "triage",
+ status: "planning",
+ assignedAgentId: undefined,
+ checkedOutBy: undefined,
+ steeringComments: [makeSteeringComment({ id: "planning-populated-user", text: "Earlier planning guidance" })],
+ })],
+ ["loading", [], true, makeTask({ column: "triage", status: null, assignedAgentId: undefined, checkedOutBy: undefined })],
+ ] as const)("keeps planning-session guidance active with an %s transcript", (_label, entries, loading, task) => {
+ mockLogs([...entries], loading);
+ render();
+
+ expectActiveSessionCopy();
+ });
+
it.each([
["empty", [], makeTask({ column: "todo", assignedAgentId: undefined, checkedOutBy: undefined, status: undefined })],
["populated", [makeEntry({ agent: "executor", text: "Earlier agent output" })], makeTask({
@@ -1953,10 +1990,11 @@ describe("TaskChatTab", () => {
});
it.each([
- ["in-progress task", makeTask({ column: "in-progress", assignedAgentId: "agent-1", status: "queued" }), true],
+ ["in-progress task", makeTask({ column: "in-progress", assignedAgentId: "agent-1", status: undefined }), true],
+ ["queued in-progress task", makeTask({ column: "in-progress", assignedAgentId: "agent-1", status: "queued" }), false],
["in-review task", makeTask({ column: "in-review", assignedAgentId: "agent-1", status: "reviewing" }), true],
["todo task", makeTask({ column: "todo", assignedAgentId: "agent-1", status: undefined }), false],
- ["triage task", makeTask({ column: "triage", assignedAgentId: "agent-1", status: undefined }), false],
+ ["triage task", makeTask({ column: "triage", assignedAgentId: "agent-1", status: undefined }), true],
["done task", makeTask({ column: "done", assignedAgentId: "agent-1", status: undefined }), false],
["archived task", makeTask({ column: "archived", assignedAgentId: "agent-1", status: undefined }), false],
])("keeps the composer sendable for %s column", (_label, task, showsActiveCopy) => {
@@ -1974,6 +2012,9 @@ describe("TaskChatTab", () => {
it.each([
["in-progress task without an assigned or checked-out agent", makeTask({ column: "in-progress", status: "queued", assignedAgentId: undefined, checkedOutBy: undefined })],
+ ["triage task waiting in the queue", makeTask({ column: "triage", status: "queued", assignedAgentId: undefined, checkedOutBy: undefined })],
+ ["paused triage task", makeTask({ column: "triage", status: "planning", paused: true, assignedAgentId: undefined, checkedOutBy: undefined })],
+ ["user-paused triage task", makeTask({ column: "triage", status: "planning", userPaused: true, 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 })],
// Paused early-return must win over the ephemeral executionImpliesActiveAgent path:
@@ -2033,10 +2074,10 @@ describe("TaskChatTab", () => {
expectComposerSendableAfterDraft();
});
- it.each(["paused", "awaiting-user-input", "awaiting-cli-approval", "awaiting-user-review", "failed", "needs-replan"])(
- "keeps in-progress steering sendable with idle guidance for %s status",
+ it.each(["paused", "awaiting-user-input", "awaiting-cli-approval", "awaiting-user-review", "awaiting-approval", "awaiting-integration", "failed", "needs-replan"])(
+ "keeps active-column steering sendable with idle guidance for %s status",
(status) => {
- render();
+ render();
expectIdleSessionHint();
expectComposerSendableAfterDraft();