FN-6316: collapse task chat tool and thinking entries
Make task-detail agent chat easier to scan during tool-heavy sessions. - Group consecutive tool, result, and error entries into collapsed summaries with status counts. - Render thinking output in expanded-by-default collapsible blocks. - Add responsive styling, docs, and regression coverage for grouped transcript segments. Files changed: docs/dashboard-guide.md | 2 +- packages/dashboard/app/components/TaskChatTab.css | 120 +++++++++++++-- packages/dashboard/app/components/TaskChatTab.tsx | 161 ++++++++++++++++++--- .../app/components/__tests__/TaskChatTab.test.tsx | 100 +++++++++++-- 4 files changed, 337 insertions(+), 46 deletions(-) Fusion-Task-Id: FN-6316 Fusion-Task-Lineage: 84c83ea1-fa9a-4e30-9794-ae95e29f989e
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 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. 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; 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. Consecutive tool/tool-result/tool-error rows inside a role group collapse into one expandable tool-call summary that stays collapsed by default, while 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. 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; 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:
|
||||
|
||||
|
||||
@@ -75,18 +75,103 @@
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.task-chat-entry--thinking {
|
||||
.task-chat-tool-group,
|
||||
.task-chat-thinking {
|
||||
min-width: 0;
|
||||
border: var(--btn-border-width) solid var(--border);
|
||||
border-radius: var(--radius-lg);
|
||||
background: var(--surface);
|
||||
color: var(--text);
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.task-chat-tool-group {
|
||||
background: var(--bg-tertiary);
|
||||
}
|
||||
|
||||
.task-chat-tool-group-summary,
|
||||
.task-chat-thinking-summary {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-sm);
|
||||
padding: var(--space-sm) var(--space-md);
|
||||
cursor: pointer;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.task-chat-tool-group-summary::-webkit-details-marker,
|
||||
.task-chat-thinking-summary::-webkit-details-marker {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.task-chat-tool-group-summary::marker,
|
||||
.task-chat-thinking-summary::marker {
|
||||
content: "";
|
||||
}
|
||||
|
||||
.task-chat-tool-group-summary:focus-visible,
|
||||
.task-chat-thinking-summary:focus-visible {
|
||||
outline: none;
|
||||
box-shadow: var(--focus-ring-strong);
|
||||
}
|
||||
|
||||
.task-chat-tool-group-count {
|
||||
flex: 0 0 auto;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.task-chat-tool-group-status {
|
||||
display: inline-flex;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--space-xs);
|
||||
color: var(--text-muted);
|
||||
font-size: var(--space-md);
|
||||
}
|
||||
|
||||
.task-chat-tool-group-status-part--call {
|
||||
color: var(--color-warning);
|
||||
}
|
||||
|
||||
.task-chat-tool-group-status-part--error {
|
||||
color: var(--color-error);
|
||||
}
|
||||
|
||||
.task-chat-tool-group-entries,
|
||||
.task-chat-thinking-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-sm);
|
||||
padding: 0 var(--space-md) var(--space-md);
|
||||
}
|
||||
|
||||
.task-chat-tool-entry {
|
||||
min-width: 0;
|
||||
padding: var(--space-sm) var(--space-md);
|
||||
border: var(--btn-border-width) solid var(--border);
|
||||
border-radius: var(--radius-md);
|
||||
background: var(--surface);
|
||||
color: var(--text);
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.task-chat-tool-entry--tool-error {
|
||||
border-color: color-mix(in srgb, var(--color-error) 45%, var(--border));
|
||||
background: color-mix(in srgb, var(--color-error) 8%, var(--surface));
|
||||
}
|
||||
|
||||
.task-chat-thinking {
|
||||
border-color: color-mix(in srgb, var(--color-warning) 35%, var(--border));
|
||||
background: color-mix(in srgb, var(--color-warning) 8%, var(--surface));
|
||||
}
|
||||
|
||||
.task-chat-entry--tool {
|
||||
background: var(--bg-tertiary);
|
||||
.task-chat-thinking-summary {
|
||||
color: var(--color-warning);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.task-chat-entry--tool-error {
|
||||
border-color: color-mix(in srgb, var(--color-error) 45%, var(--border));
|
||||
background: color-mix(in srgb, var(--color-error) 8%, var(--surface));
|
||||
.task-chat-thinking-markdown + .task-chat-thinking-markdown {
|
||||
padding-top: var(--space-sm);
|
||||
border-top: var(--btn-border-width) solid color-mix(in srgb, var(--color-warning) 24%, var(--border));
|
||||
}
|
||||
|
||||
.task-chat-entry-kicker {
|
||||
@@ -98,11 +183,7 @@
|
||||
letter-spacing: 0.08em;
|
||||
}
|
||||
|
||||
.task-chat-entry--thinking .task-chat-entry-kicker {
|
||||
color: var(--color-warning);
|
||||
}
|
||||
|
||||
.task-chat-entry--tool-error .task-chat-entry-kicker {
|
||||
.task-chat-tool-entry--tool-error .task-chat-entry-kicker {
|
||||
color: var(--color-error);
|
||||
}
|
||||
|
||||
@@ -178,6 +259,25 @@
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.task-chat-tool-group-summary,
|
||||
.task-chat-thinking-summary {
|
||||
align-items: flex-start;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.task-chat-tool-group-status {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.task-chat-tool-group-entries,
|
||||
.task-chat-thinking-body {
|
||||
padding-inline: var(--space-sm);
|
||||
}
|
||||
|
||||
.task-chat-tool-entry {
|
||||
padding: var(--space-sm);
|
||||
}
|
||||
|
||||
.task-chat-composer {
|
||||
padding: var(--space-sm);
|
||||
}
|
||||
|
||||
@@ -28,6 +28,11 @@ interface AgentLogGroup {
|
||||
entries: AgentLogEntry[];
|
||||
}
|
||||
|
||||
type TaskChatSegment =
|
||||
| { kind: "tool"; entries: AgentLogEntry[]; startIndex: number }
|
||||
| { kind: "thinking"; entries: AgentLogEntry[]; startIndex: number }
|
||||
| { kind: "text"; entry: AgentLogEntry; index: number };
|
||||
|
||||
const STEERING_BLOCKED_STATUSES = new Set([
|
||||
"paused",
|
||||
"awaiting-user-input",
|
||||
@@ -99,6 +104,10 @@ function isActiveAgentSession(task: Task | TaskDetail): boolean {
|
||||
&& !task.userPaused;
|
||||
}
|
||||
|
||||
function isToolLikeEntry(entry: AgentLogEntry): boolean {
|
||||
return entry.type === "tool" || entry.type === "tool_result" || entry.type === "tool_error";
|
||||
}
|
||||
|
||||
function formatEntryLabel(entry: AgentLogEntry): string {
|
||||
switch (entry.type) {
|
||||
case "tool":
|
||||
@@ -114,27 +123,51 @@ function formatEntryLabel(entry: AgentLogEntry): string {
|
||||
}
|
||||
}
|
||||
|
||||
function TaskChatEntry({ entry }: { entry: AgentLogEntry }) {
|
||||
const isToolEntry = entry.type === "tool" || entry.type === "tool_result" || entry.type === "tool_error";
|
||||
const className = [
|
||||
"task-chat-entry",
|
||||
`task-chat-entry--${entry.type.replace("_", "-")}`,
|
||||
isToolEntry ? "task-chat-entry--tool" : "",
|
||||
].filter(Boolean).join(" ");
|
||||
function formatToolCallCount(count: number): string {
|
||||
return count === 1 ? "1 tool call" : `${count} tool calls`;
|
||||
}
|
||||
|
||||
if (isToolEntry) {
|
||||
return (
|
||||
<article className={className} data-testid={`task-chat-entry-${entry.type}`}>
|
||||
<div className="task-chat-entry-kicker">{formatEntryLabel(entry)}</div>
|
||||
<div className="task-chat-entry-text">{entry.text}</div>
|
||||
{entry.detail ? <pre className="task-chat-tool-detail">{linkifyFilePaths(entry.detail)}</pre> : null}
|
||||
</article>
|
||||
);
|
||||
function segmentGroupEntries(entries: AgentLogEntry[]): TaskChatSegment[] {
|
||||
const segments: TaskChatSegment[] = [];
|
||||
let index = 0;
|
||||
|
||||
while (index < entries.length) {
|
||||
const entry = entries[index];
|
||||
if (isToolLikeEntry(entry)) {
|
||||
const startIndex = index;
|
||||
const toolEntries: AgentLogEntry[] = [];
|
||||
while (index < entries.length && isToolLikeEntry(entries[index])) {
|
||||
toolEntries.push(entries[index]);
|
||||
index += 1;
|
||||
}
|
||||
segments.push({ kind: "tool", entries: toolEntries, startIndex });
|
||||
continue;
|
||||
}
|
||||
|
||||
if (entry.type === "thinking") {
|
||||
const startIndex = index;
|
||||
const thinkingEntries: AgentLogEntry[] = [];
|
||||
while (index < entries.length && entries[index].type === "thinking") {
|
||||
thinkingEntries.push(entries[index]);
|
||||
index += 1;
|
||||
}
|
||||
segments.push({ kind: "thinking", entries: thinkingEntries, startIndex });
|
||||
continue;
|
||||
}
|
||||
|
||||
segments.push({ kind: "text", entry, index });
|
||||
index += 1;
|
||||
}
|
||||
|
||||
return segments;
|
||||
}
|
||||
|
||||
function TaskChatTextEntry({ entry }: { entry: AgentLogEntry }) {
|
||||
return (
|
||||
<article className={className} data-testid={`task-chat-entry-${entry.type}`}>
|
||||
{entry.type === "thinking" ? <div className="task-chat-entry-kicker">{formatEntryLabel(entry)}</div> : null}
|
||||
<article
|
||||
className={`task-chat-entry task-chat-entry--${entry.type.replace("_", "-")}`}
|
||||
data-testid={`task-chat-entry-${entry.type}`}
|
||||
>
|
||||
<div className="markdown-body task-chat-markdown">
|
||||
<ReactMarkdown remarkPlugins={[remarkGfm]} components={markdownComponents}>
|
||||
{entry.text}
|
||||
@@ -144,6 +177,88 @@ function TaskChatEntry({ entry }: { entry: AgentLogEntry }) {
|
||||
);
|
||||
}
|
||||
|
||||
function TaskChatToolEntry({ entry }: { entry: AgentLogEntry }) {
|
||||
return (
|
||||
<article
|
||||
className={`task-chat-tool-entry task-chat-tool-entry--${entry.type.replace("_", "-")}`}
|
||||
data-testid={`task-chat-entry-${entry.type}`}
|
||||
>
|
||||
<div className="task-chat-entry-kicker">{formatEntryLabel(entry)}</div>
|
||||
<div className="task-chat-entry-text">{entry.text}</div>
|
||||
{entry.detail ? <pre className="task-chat-tool-detail">{linkifyFilePaths(entry.detail)}</pre> : null}
|
||||
</article>
|
||||
);
|
||||
}
|
||||
|
||||
function TaskChatToolGroup({ entries }: { entries: AgentLogEntry[] }) {
|
||||
const callCount = entries.filter((entry) => entry.type === "tool").length;
|
||||
const resultCount = entries.filter((entry) => entry.type === "tool_result").length;
|
||||
const errorCount = entries.filter((entry) => entry.type === "tool_error").length;
|
||||
|
||||
return (
|
||||
<details className="task-chat-tool-group" data-testid="task-chat-tool-group">
|
||||
<summary className="task-chat-tool-group-summary">
|
||||
<span className="task-chat-tool-group-count">{formatToolCallCount(entries.length)}</span>
|
||||
<span className="task-chat-tool-group-status" aria-label="Tool status breakdown">
|
||||
{callCount > 0 ? (
|
||||
<span className="task-chat-tool-group-status-part task-chat-tool-group-status-part--call">
|
||||
{callCount === 1 ? "1 call" : `${callCount} calls`}
|
||||
</span>
|
||||
) : null}
|
||||
{resultCount > 0 ? (
|
||||
<span className="task-chat-tool-group-status-part">
|
||||
{resultCount === 1 ? "1 result" : `${resultCount} results`}
|
||||
</span>
|
||||
) : null}
|
||||
{errorCount > 0 ? (
|
||||
<span className="task-chat-tool-group-status-part task-chat-tool-group-status-part--error">
|
||||
{errorCount === 1 ? "1 error" : `${errorCount} errors`}
|
||||
</span>
|
||||
) : null}
|
||||
</span>
|
||||
</summary>
|
||||
<div className="task-chat-tool-group-entries">
|
||||
{entries.map((entry, entryIndex) => (
|
||||
<TaskChatToolEntry key={getEntryKey(entry, entryIndex)} entry={entry} />
|
||||
))}
|
||||
</div>
|
||||
</details>
|
||||
);
|
||||
}
|
||||
|
||||
function TaskChatThinking({ entries }: { entries: AgentLogEntry[] }) {
|
||||
return (
|
||||
<details className="task-chat-thinking" data-testid="task-chat-thinking" open>
|
||||
<summary className="task-chat-thinking-summary">
|
||||
{entries.length === 1 ? "Thinking" : `${entries.length} thinking entries`}
|
||||
</summary>
|
||||
<div className="task-chat-thinking-body">
|
||||
{entries.map((entry, entryIndex) => (
|
||||
<div
|
||||
className="markdown-body task-chat-markdown task-chat-thinking-markdown"
|
||||
data-testid="task-chat-entry-thinking"
|
||||
key={getEntryKey(entry, entryIndex)}
|
||||
>
|
||||
<ReactMarkdown remarkPlugins={[remarkGfm]} components={markdownComponents}>
|
||||
{entry.text}
|
||||
</ReactMarkdown>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</details>
|
||||
);
|
||||
}
|
||||
|
||||
function TaskChatSegmentView({ segment }: { segment: TaskChatSegment }) {
|
||||
if (segment.kind === "tool") {
|
||||
return <TaskChatToolGroup entries={segment.entries} />;
|
||||
}
|
||||
if (segment.kind === "thinking") {
|
||||
return <TaskChatThinking entries={segment.entries} />;
|
||||
}
|
||||
return <TaskChatTextEntry entry={segment.entry} />;
|
||||
}
|
||||
|
||||
export function TaskChatTab({ task, projectId, active, addToast }: TaskChatTabProps) {
|
||||
const { entries, loading } = useAgentLogs(task.id, active, projectId);
|
||||
const [draft, setDraft] = useState("");
|
||||
@@ -261,6 +376,7 @@ export function TaskChatTab({ task, projectId, active, addToast }: TaskChatTabPr
|
||||
name: group.label,
|
||||
icon: getRoleIcon(group.role),
|
||||
};
|
||||
const segments = segmentGroupEntries(group.entries);
|
||||
return (
|
||||
<section className="task-chat-group" key={`${group.role ?? "agent"}-${groupIndex}`} aria-label={`${group.label} messages`}>
|
||||
<header className="task-chat-group-header">
|
||||
@@ -271,9 +387,12 @@ export function TaskChatTab({ task, projectId, active, addToast }: TaskChatTabPr
|
||||
</div>
|
||||
</header>
|
||||
<div className="task-chat-group-bubbles">
|
||||
{group.entries.map((entry, entryIndex) => (
|
||||
<TaskChatEntry key={getEntryKey(entry, entryIndex)} entry={entry} />
|
||||
))}
|
||||
{segments.map((segment) => {
|
||||
const segmentKey = segment.kind === "text"
|
||||
? `text-${getEntryKey(segment.entry, segment.index)}`
|
||||
: `${segment.kind}-${segment.startIndex}-${segment.entries.length}`;
|
||||
return <TaskChatSegmentView key={segmentKey} segment={segment} />;
|
||||
})}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
||||
import { render, screen, fireEvent, waitFor } from "@testing-library/react";
|
||||
import { render, screen, fireEvent, waitFor, within } from "@testing-library/react";
|
||||
import { userEvent } from "@testing-library/user-event";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { resolve } from "node:path";
|
||||
@@ -189,9 +189,9 @@ describe("TaskChatTab", () => {
|
||||
expect(screen.getByLabelText("Reviewer messages")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("renders thinking and tool entries legibly", () => {
|
||||
it("collapses consecutive tool entries into one expandable summary", async () => {
|
||||
const user = userEvent.setup();
|
||||
mockLogs([
|
||||
makeEntry({ agent: "triage", type: "thinking", text: "I am considering options" }),
|
||||
makeEntry({ agent: "executor", type: "tool", text: "bash", detail: "pnpm test" }),
|
||||
makeEntry({ agent: "executor", type: "tool_result", text: "done", detail: "ok" }),
|
||||
makeEntry({ agent: "executor", type: "tool_error", text: "failed", detail: "stderr" }),
|
||||
@@ -199,24 +199,94 @@ describe("TaskChatTab", () => {
|
||||
|
||||
render(<TaskChatTab task={makeTask()} active addToast={vi.fn()} />);
|
||||
|
||||
expect(screen.getByText("Thinking")).toBeTruthy();
|
||||
expect(screen.getByText("Tool call")).toBeTruthy();
|
||||
expect(screen.getByText("Tool result")).toBeTruthy();
|
||||
expect(screen.getByText("Tool error")).toBeTruthy();
|
||||
expect(screen.getByText("stderr")).toBeTruthy();
|
||||
const toolGroup = screen.getByTestId("task-chat-tool-group");
|
||||
expect(toolGroup).not.toHaveAttribute("open");
|
||||
expect(screen.getByText("3 tool calls")).toBeVisible();
|
||||
expect(screen.getByText("1 call")).toBeVisible();
|
||||
expect(screen.getByText("1 result")).toBeVisible();
|
||||
expect(screen.getByText("1 error")).toBeVisible();
|
||||
expect(screen.getByText("stderr")).not.toBeVisible();
|
||||
|
||||
await user.click(within(toolGroup).getByText("3 tool calls"));
|
||||
|
||||
expect(toolGroup).toHaveAttribute("open");
|
||||
expect(screen.getByText("Tool call")).toBeVisible();
|
||||
expect(screen.getByText("Tool result")).toBeVisible();
|
||||
expect(screen.getByText("Tool error")).toBeVisible();
|
||||
expect(screen.getByText("stderr")).toBeVisible();
|
||||
});
|
||||
|
||||
it("appends newly streamed entries from the hook", () => {
|
||||
it("renders a single tool entry as one collapsed group and tolerates missing detail", () => {
|
||||
mockLogs([
|
||||
makeEntry({ agent: "executor", type: "tool", text: "bash", detail: undefined }),
|
||||
]);
|
||||
|
||||
render(<TaskChatTab task={makeTask()} active addToast={vi.fn()} />);
|
||||
|
||||
const toolGroup = screen.getByTestId("task-chat-tool-group");
|
||||
expect(toolGroup).not.toHaveAttribute("open");
|
||||
expect(screen.getByText("1 tool call")).toBeVisible();
|
||||
expect(screen.getByText("bash")).not.toBeVisible();
|
||||
});
|
||||
|
||||
it("renders thinking in an expanded-by-default collapsible block", async () => {
|
||||
const user = userEvent.setup();
|
||||
mockLogs([
|
||||
makeEntry({ agent: "triage", type: "thinking", text: "I am considering options" }),
|
||||
]);
|
||||
|
||||
render(<TaskChatTab task={makeTask()} active addToast={vi.fn()} />);
|
||||
|
||||
const thinking = screen.getByTestId("task-chat-thinking");
|
||||
expect(thinking).toHaveAttribute("open");
|
||||
expect(screen.getByText("Thinking")).toBeVisible();
|
||||
expect(screen.getByText("I am considering options")).toBeVisible();
|
||||
|
||||
await user.click(within(thinking).getByText("Thinking"));
|
||||
|
||||
expect(thinking).not.toHaveAttribute("open");
|
||||
expect(screen.getByText("I am considering options")).not.toBeVisible();
|
||||
});
|
||||
|
||||
it("creates distinct tool segments when text or thinking entries are interleaved", () => {
|
||||
mockLogs([
|
||||
makeEntry({ agent: "executor", type: "tool", text: "first tool", detail: "first detail" }),
|
||||
makeEntry({ agent: "executor", text: "plain response" }),
|
||||
makeEntry({ agent: "executor", type: "thinking", text: "thinking between tools" }),
|
||||
makeEntry({ agent: "executor", type: "tool_result", text: "second tool", detail: "second detail" }),
|
||||
]);
|
||||
|
||||
render(<TaskChatTab task={makeTask()} active addToast={vi.fn()} />);
|
||||
|
||||
const toolGroups = screen.getAllByTestId("task-chat-tool-group");
|
||||
expect(toolGroups).toHaveLength(2);
|
||||
expect(toolGroups[0]).not.toHaveAttribute("open");
|
||||
expect(toolGroups[1]).not.toHaveAttribute("open");
|
||||
expect(screen.getAllByText("1 tool call")).toHaveLength(2);
|
||||
expect(screen.getByText("plain response")).toBeVisible();
|
||||
expect(screen.getByText("thinking between tools")).toBeVisible();
|
||||
});
|
||||
|
||||
it("appends newly streamed entries from the hook without auto-opening tool groups", () => {
|
||||
const firstEntries = [makeEntry({ agent: "executor", text: "first live chunk" })];
|
||||
const secondEntries = [...firstEntries, makeEntry({ agent: "executor", text: "second live chunk", timestamp: "2026-06-12T00:00:01.000Z" })];
|
||||
const secondEntries = [
|
||||
...firstEntries,
|
||||
makeEntry({ agent: "executor", type: "tool", text: "streamed tool", detail: "streamed detail", timestamp: "2026-06-12T00:00:01.000Z" }),
|
||||
makeEntry({ agent: "executor", text: "second live chunk", timestamp: "2026-06-12T00:00:02.000Z" }),
|
||||
];
|
||||
mockedUseAgentLogs.mockReturnValueOnce({ entries: firstEntries, loading: false, clear: vi.fn(), loadMore: vi.fn(), hasMore: false, total: 1, loadingMore: false });
|
||||
mockedUseAgentLogs.mockReturnValueOnce({ entries: secondEntries, loading: false, clear: vi.fn(), loadMore: vi.fn(), hasMore: false, total: 2, loadingMore: false });
|
||||
mockedUseAgentLogs.mockReturnValueOnce({ entries: secondEntries, loading: false, clear: vi.fn(), loadMore: vi.fn(), hasMore: false, total: 3, loadingMore: false });
|
||||
|
||||
const { rerender } = render(<TaskChatTab task={makeTask()} active addToast={vi.fn()} />);
|
||||
expect(screen.getByText("first live chunk")).toBeTruthy();
|
||||
expect(screen.getByText("first live chunk")).toBeVisible();
|
||||
|
||||
rerender(<TaskChatTab task={makeTask()} active addToast={vi.fn()} />);
|
||||
expect(screen.getByText("second live chunk")).toBeTruthy();
|
||||
|
||||
const toolGroup = screen.getByTestId("task-chat-tool-group");
|
||||
expect(toolGroup).not.toHaveAttribute("open");
|
||||
expect(screen.getByText("1 tool call")).toBeVisible();
|
||||
expect(screen.getByText("streamed detail")).not.toBeVisible();
|
||||
expect(screen.getByText("second live chunk")).toBeVisible();
|
||||
});
|
||||
|
||||
it.each([
|
||||
@@ -478,10 +548,12 @@ describe("TaskChatTab", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps mobile breakpoint scaffolding for the transcript and composer", () => {
|
||||
it("keeps mobile breakpoint scaffolding for the transcript, composer, and collapsible groups", () => {
|
||||
const css = readFileSync(resolve(__dirname, "../TaskChatTab.css"), "utf8");
|
||||
expect(css).toContain("@media (max-width: 768px)");
|
||||
expect(css).toContain(".task-chat-transcript");
|
||||
expect(css).toContain(".task-chat-composer-row");
|
||||
expect(css).toContain(".task-chat-tool-group-summary");
|
||||
expect(css).toContain(".task-chat-thinking-summary");
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user