FN-6314: allow chat steering during review sessions
Task Chat can now steer active assigned sessions while tasks are being reviewed or merged. - Permit composer steering for in-review reviewing, merging, merging-fix, and fixing statuses when an agent is assigned or checked out. - Keep paused, user-paused, unassigned, todo, and done tasks disabled with clearer active-session messaging. - Cover in-review steering and disabled-session states in TaskChatTab tests and update dashboard documentation. Files changed: docs/dashboard-guide.md | 2 +- packages/dashboard/app/components/TaskChatTab.tsx | 13 +++-- .../app/components/__tests__/TaskChatTab.test.tsx | 61 ++++++++++++++++++++-- 3 files changed, 67 insertions(+), 9 deletions(-) Fusion-Task-Id: FN-6314 Fusion-Task-Lineage: 42c0f4f7-58f5-4e0c-837f-9f4b7c497fa3
This commit is contained in:
@@ -728,7 +728,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. The transcript follows new live output when you are already near the bottom, but it preserves your scroll position when you review older messages. For active `in-progress` tasks with an assigned agent session, the composer sends guidance to the running agent through the same steering path used by comments; when no active session is available, the composer is disabled with an explanatory hint.
|
||||
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. The transcript follows new live output when you are already near the bottom, but it preserves your scroll position when you review older messages. For active assigned agent sessions in `in-progress` or `in-review` (reviewing/merging/fixing) tasks, the composer sends guidance to the running agent through the same steering path used by comments; when no active session is available, the composer is disabled with an explanatory hint.
|
||||
|
||||
The **Logs** tab includes an **Agent Log** subview designed for debugging long-running and tool-heavy sessions:
|
||||
|
||||
|
||||
@@ -29,6 +29,7 @@ interface AgentLogGroup {
|
||||
}
|
||||
|
||||
const ACTIVE_STATUSES = new Set(["planning", "researching", "executing", "finalizing", "merging", "merging-fix"]);
|
||||
const REVIEW_STEERABLE_STATUSES = new Set(["reviewing", "merging", "merging-fix", "fixing"]);
|
||||
const BOTTOM_FOLLOW_THRESHOLD = 48;
|
||||
|
||||
function getRoleLabel(role: AgentLogRole): string {
|
||||
@@ -80,10 +81,12 @@ function groupEntriesByAgent(entries: AgentLogEntry[]): AgentLogGroup[] {
|
||||
|
||||
function isActiveAgentSession(task: Task | TaskDetail): boolean {
|
||||
const hasAssignedAgent = Boolean(task.assignedAgentId || task.checkedOutBy);
|
||||
const statusAllowsSteering = !task.status || ACTIVE_STATUSES.has(task.status);
|
||||
return task.column === "in-progress"
|
||||
const statusAllowsProgressSteering = !task.status || ACTIVE_STATUSES.has(task.status);
|
||||
const statusAllowsReviewSteering = !task.status || REVIEW_STEERABLE_STATUSES.has(task.status);
|
||||
const columnAllowsSteering = (task.column === "in-progress" && statusAllowsProgressSteering)
|
||||
|| (task.column === "in-review" && statusAllowsReviewSteering);
|
||||
return columnAllowsSteering
|
||||
&& hasAssignedAgent
|
||||
&& statusAllowsSteering
|
||||
&& !task.paused
|
||||
&& !task.userPaused;
|
||||
}
|
||||
@@ -250,7 +253,7 @@ export function TaskChatTab({ task, projectId, active, addToast }: TaskChatTabPr
|
||||
<form className="task-chat-composer card" onSubmit={handleSubmit}>
|
||||
{!activeSession ? (
|
||||
<div className="task-chat-session-hint" role="status">
|
||||
No active assigned agent session is available. Move the task into progress with an assigned agent to send guidance.
|
||||
No active assigned agent session is available. An active, assigned agent session is required to send guidance.
|
||||
</div>
|
||||
) : null}
|
||||
<div className="task-chat-composer-row">
|
||||
@@ -258,7 +261,7 @@ export function TaskChatTab({ task, projectId, active, addToast }: TaskChatTabPr
|
||||
ref={textareaRef}
|
||||
className="input task-chat-input"
|
||||
value={draft}
|
||||
placeholder={activeSession ? "Message the active agent session…" : "No active session available"}
|
||||
placeholder={activeSession ? "Message the active agent session…" : "Active assigned agent session required"}
|
||||
onChange={(event) => setDraft(event.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
disabled={!activeSession || sending}
|
||||
|
||||
@@ -141,8 +141,11 @@ describe("TaskChatTab", () => {
|
||||
render(<TaskChatTab task={makeTask()} projectId="project-1" active addToast={vi.fn()} />);
|
||||
|
||||
const input = screen.getByLabelText("Message active agent session");
|
||||
expect(input).not.toBeDisabled();
|
||||
await user.type(input, "Please inspect the failing test");
|
||||
await user.click(screen.getByRole("button", { name: "Send" }));
|
||||
const sendButton = screen.getByRole("button", { name: "Send" });
|
||||
expect(sendButton).not.toBeDisabled();
|
||||
await user.click(sendButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockedAddSteeringComment).toHaveBeenCalledWith("FN-001", "Please inspect the failing test", "project-1");
|
||||
@@ -150,11 +153,63 @@ describe("TaskChatTab", () => {
|
||||
expect(input).toHaveValue("");
|
||||
});
|
||||
|
||||
it("disables the composer and shows a hint when no active assigned session exists", () => {
|
||||
render(<TaskChatTab task={makeTask({ column: "todo", assignedAgentId: undefined, status: undefined })} active addToast={vi.fn()} />);
|
||||
it.each(["reviewing", "merging", "merging-fix", "fixing"])(
|
||||
"enables in-review steering while %s with an assigned agent",
|
||||
async (status) => {
|
||||
const user = userEvent.setup();
|
||||
mockedAddSteeringComment.mockResolvedValue(makeTask({ column: "in-review", status }));
|
||||
render(<TaskChatTab task={makeTask({ column: "in-review", status })} projectId="project-1" active addToast={vi.fn()} />);
|
||||
|
||||
const input = screen.getByLabelText("Message active agent session");
|
||||
expect(input).not.toBeDisabled();
|
||||
await user.type(input, `Please continue ${status}`);
|
||||
const sendButton = screen.getByRole("button", { name: "Send" });
|
||||
expect(sendButton).not.toBeDisabled();
|
||||
await user.click(sendButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockedAddSteeringComment).toHaveBeenCalledWith("FN-001", `Please continue ${status}`, "project-1");
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
it("enables in-review steering with checkedOutBy when no assignedAgentId exists", async () => {
|
||||
const user = userEvent.setup();
|
||||
mockedAddSteeringComment.mockResolvedValue(makeTask({ column: "in-review", status: "reviewing" }));
|
||||
render(
|
||||
<TaskChatTab
|
||||
task={makeTask({ column: "in-review", status: "reviewing", assignedAgentId: undefined, checkedOutBy: "agent-1" })}
|
||||
projectId="project-1"
|
||||
active
|
||||
addToast={vi.fn()}
|
||||
/>,
|
||||
);
|
||||
|
||||
const input = screen.getByLabelText("Message active agent session");
|
||||
expect(input).not.toBeDisabled();
|
||||
await user.type(input, "Please review this follow-up");
|
||||
const sendButton = screen.getByRole("button", { name: "Send" });
|
||||
expect(sendButton).not.toBeDisabled();
|
||||
await user.click(sendButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockedAddSteeringComment).toHaveBeenCalledWith("FN-001", "Please review this follow-up", "project-1");
|
||||
});
|
||||
});
|
||||
|
||||
it.each([
|
||||
["todo task", makeTask({ column: "todo", assignedAgentId: undefined, status: undefined })],
|
||||
["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 })],
|
||||
["done task", makeTask({ column: "done", status: undefined })],
|
||||
])("disables the composer and shows a hint for %s", (_label, task) => {
|
||||
render(<TaskChatTab task={task} active addToast={vi.fn()} />);
|
||||
|
||||
expect(screen.getByText(/No active assigned agent session/)).toBeTruthy();
|
||||
expect(screen.getByText(/active, assigned agent session is required/i)).toBeTruthy();
|
||||
expect(screen.getByLabelText("Message active agent session")).toBeDisabled();
|
||||
expect(screen.getByPlaceholderText("Active assigned agent session required")).toBeTruthy();
|
||||
expect(screen.getByRole("button", { name: "Send" })).toBeDisabled();
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user