FN-6369: make task chat send control icon-only
Refine the task-detail chat composer so the send action stays narrow and inline with the input. - Replace the composer placeholder with the steering-focused copy. - Convert the send button to an accessible icon-only control with loading state labels. - Keep the send control inline at mobile breakpoints and cover the behavior in tests and docs. Files changed: docs/dashboard-guide.md | 2 +- packages/dashboard/app/components/TaskChatTab.css | 14 +++++++---- packages/dashboard/app/components/TaskChatTab.tsx | 11 ++++++--- .../app/components/__tests__/TaskChatTab.test.tsx | 27 ++++++++++++++++++++-- 4 files changed, 44 insertions(+), 10 deletions(-) Fusion-Task-Id: FN-6369 Fusion-Task-Lineage: b7e0717f-4cba-4380-a08a-e01fc12985e3
This commit is contained in:
@@ -734,7 +734,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. 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 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 active, assigned, non-paused 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; this includes regular engine agents working in a task worktree as well as live CLI sessions. When no active session is available, the composer is disabled with an explanatory hint. The task-detail Chat tab keeps the composer pinned and visible on mobile and desktop while the transcript scrolls internally.
|
||||
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. 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 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 active, assigned, non-paused 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; this includes regular engine agents working in a task worktree as well as live CLI sessions. When no active session is available, the composer is disabled with an explanatory hint. 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” and the send affordance is an inline, icon-only button to the right of the input at every breakpoint.
|
||||
|
||||
The **Logs** tab includes an **Agent Log** subview designed for debugging long-running and tool-heavy sessions:
|
||||
|
||||
|
||||
@@ -312,7 +312,12 @@
|
||||
flex: 0 0 auto;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--space-xs);
|
||||
justify-content: center;
|
||||
inline-size: calc(var(--space-2xl) + var(--space-sm));
|
||||
min-inline-size: calc(var(--space-2xl) + var(--space-sm));
|
||||
block-size: calc(var(--space-2xl) + var(--space-sm));
|
||||
min-block-size: calc(var(--space-2xl) + var(--space-sm));
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
@@ -378,11 +383,12 @@
|
||||
}
|
||||
|
||||
.task-chat-composer-row {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
align-items: flex-end;
|
||||
gap: var(--space-xs);
|
||||
}
|
||||
|
||||
.task-chat-send {
|
||||
justify-content: center;
|
||||
inline-size: calc(var(--space-2xl) + var(--space-sm));
|
||||
min-inline-size: calc(var(--space-2xl) + var(--space-sm));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -671,16 +671,21 @@ export function TaskChatTab({ task, projectId, active, addToast, sessionLive, on
|
||||
ref={textareaRef}
|
||||
className="input task-chat-input"
|
||||
value={draft}
|
||||
placeholder={activeSession ? "Message the active agent session…" : "Message the agent…"}
|
||||
placeholder="Steer the currently executing agent"
|
||||
onChange={(event) => setDraft(event.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
disabled={sending}
|
||||
aria-label="Message active agent session"
|
||||
rows={1}
|
||||
/>
|
||||
<button type="submit" className="btn btn-primary task-chat-send" disabled={!canSend}>
|
||||
<button
|
||||
type="submit"
|
||||
className="btn btn-primary btn-icon task-chat-send"
|
||||
disabled={!canSend}
|
||||
aria-label={sending ? "Sending" : "Send"}
|
||||
title={sending ? "Sending" : "Send"}
|
||||
>
|
||||
{sending ? <Loader2 className="animate-spin" aria-hidden="true" /> : <Send aria-hidden="true" />}
|
||||
<span>{sending ? "Sending" : "Send"}</span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -120,7 +120,7 @@ function expectComposerSendableAfterDraft(message = "Please continue") {
|
||||
function expectNoInactiveSessionHint() {
|
||||
expect(screen.queryByText(/picked up by the next session/i)).not.toBeInTheDocument();
|
||||
expect(document.querySelector(".task-chat-session-hint")).not.toBeInTheDocument();
|
||||
expect(screen.getByPlaceholderText("Message the agent…")).toBeInTheDocument();
|
||||
expect(screen.getByPlaceholderText("Steer the currently executing agent")).toBeInTheDocument();
|
||||
}
|
||||
|
||||
function expectActiveSessionCopy() {
|
||||
@@ -779,6 +779,15 @@ describe("TaskChatTab", () => {
|
||||
expect(screen.getByRole("button", { name: "Jump to latest message" })).toHaveClass("task-chat-jump-to-bottom");
|
||||
});
|
||||
|
||||
it("renders an icon-only send button with preserved accessible name and new placeholder", () => {
|
||||
render(<TaskChatTab task={makeTask()} active addToast={vi.fn()} />);
|
||||
|
||||
expect(screen.getByPlaceholderText("Steer the currently executing agent")).toBeInTheDocument();
|
||||
const sendButton = screen.getByRole("button", { name: "Send" });
|
||||
expect(sendButton).toHaveClass("task-chat-send");
|
||||
expect(sendButton).toHaveTextContent("");
|
||||
});
|
||||
|
||||
it("posts composer text through addSteeringComment and clears on success", async () => {
|
||||
const user = userEvent.setup();
|
||||
mockedAddSteeringComment.mockResolvedValue(makeTask());
|
||||
@@ -1205,7 +1214,9 @@ describe("TaskChatTab", () => {
|
||||
expect(sendButton).not.toBeDisabled();
|
||||
await user.click(sendButton);
|
||||
|
||||
expect(screen.getByRole("button", { name: "Sending" })).toBeDisabled();
|
||||
const sendingButton = screen.getByRole("button", { name: "Sending" });
|
||||
expect(sendingButton).toBeDisabled();
|
||||
expect(sendingButton).toHaveTextContent("");
|
||||
expect(input).toBeDisabled();
|
||||
|
||||
await act(async () => {
|
||||
@@ -1306,10 +1317,22 @@ describe("TaskChatTab", () => {
|
||||
|
||||
it("keeps mobile breakpoint scaffolding for the transcript, composer, and collapsible groups", () => {
|
||||
const css = readFileSync(resolve(__dirname, "../TaskChatTab.css"), "utf8");
|
||||
const sendRule = getCssRuleBlock(css, ".task-chat-send");
|
||||
const mobileCss = getCssAfter(css, "@media (max-width: 768px)");
|
||||
const mobileComposerRule = getCssRuleBlock(mobileCss, ".task-chat-composer-row");
|
||||
const mobileSendRule = getCssRuleBlock(mobileCss, ".task-chat-send");
|
||||
|
||||
expect(css).toContain("@media (max-width: 768px)");
|
||||
expect(css).toContain(".task-chat-transcript");
|
||||
expect(css).toContain(".task-chat-jump-to-bottom");
|
||||
expect(css).toContain(".task-chat-composer-row");
|
||||
expect(sendRule).toContain("inline-size: calc(var(--space-2xl) + var(--space-sm))");
|
||||
expect(sendRule).toContain("block-size: calc(var(--space-2xl) + var(--space-sm))");
|
||||
expect(sendRule).not.toContain("gap");
|
||||
expect(mobileComposerRule).toContain("align-items: flex-end");
|
||||
expect(mobileComposerRule).not.toContain("flex-direction: column");
|
||||
expect(mobileComposerRule).not.toContain("align-items: stretch");
|
||||
expect(mobileSendRule).toContain("inline-size: calc(var(--space-2xl) + var(--space-sm))");
|
||||
expect(css).toContain(".task-chat-tool-group-summary");
|
||||
expect(css).toContain(".task-chat-tool-group-names");
|
||||
expect(css).toContain(".task-chat-tool-group-error-count");
|
||||
|
||||
Reference in New Issue
Block a user