FN-6965: stack list-pane chat agent headers
Stack List View task-chat agent headers above their output in the split detail pane while keeping desktop chat layouts unchanged. - Add a split-pane-specific CSS override that stacks task chat groups in List View only. - Keep compact header sizing safe without affecting modal, board, expanded, or popped-out chat layouts. - Cover List View populated, loading, empty, and CSS host-scope behavior in TaskChatTab tests. - Add a patch changeset for the published Fusion package. Files changed: .changeset/fn-6965-list-pane-chat-layout.md | 5 ++ packages/dashboard/app/components/TaskChatTab.css | 12 +++ .../app/components/__tests__/TaskChatTab.test.tsx | 90 ++++++++++++++++++++++ 3 files changed, 107 insertions(+) Fusion-Task-Id: FN-6965 Fusion-Task-Lineage: 8107ae8c-c25e-497e-8506-4c77e3e083d3
This commit is contained in:
5
.changeset/fn-6965-list-pane-chat-layout.md
Normal file
5
.changeset/fn-6965-list-pane-chat-layout.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@runfusion/fusion": patch
|
||||
---
|
||||
|
||||
Stack task-detail Chat agent headers above output blocks in the List View split-pane detail pane while preserving full-width desktop chat layout.
|
||||
@@ -166,6 +166,18 @@ FN-6425 requires the chat expand control to stay inside the chat view as an icon
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/*
|
||||
FNXC:TaskDetailChat 2026-06-23-21:38:
|
||||
List View split-pane detail can be narrow while the global viewport is desktop-sized. Scope the compact/mobile agent-output group contract to that host so agent/provider headers stack above output blocks there without changing full-width modal, board main-panel, popped-out, or expanded chat desktop layouts.
|
||||
*/
|
||||
.list-split-detail-content .task-chat-group {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.list-split-detail-content .task-chat-group-header {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.task-chat-group-bubbles {
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
|
||||
@@ -167,6 +167,22 @@ function expectTranscriptTextOrder(...texts: string[]) {
|
||||
}
|
||||
}
|
||||
|
||||
function expectAgentHeaderBeforeBubbles(group: HTMLElement) {
|
||||
const header = group.querySelector(".task-chat-group-header");
|
||||
const bubbles = group.querySelector(".task-chat-group-bubbles");
|
||||
expect(header).not.toBeNull();
|
||||
expect(bubbles).not.toBeNull();
|
||||
expect(header?.compareDocumentPosition(bubbles as Element) ?? 0).toBe(Node.DOCUMENT_POSITION_FOLLOWING);
|
||||
}
|
||||
|
||||
function renderListSplitTaskChat(task: Task = makeTask()) {
|
||||
return render(
|
||||
<div className="list-split-detail-content" data-testid="list-split-detail-content">
|
||||
<TaskChatTab task={task} active addToast={vi.fn()} />
|
||||
</div>,
|
||||
);
|
||||
}
|
||||
|
||||
function expectIdleSessionHint() {
|
||||
// FNXC:TaskDetailChat 2026-06-22-21:20: The idle "No agent is working…" banner was removed per user request — idle chats stay sendable with no hint shown.
|
||||
expect(screen.queryByTestId("task-chat-idle-hint")).not.toBeInTheDocument();
|
||||
@@ -486,6 +502,55 @@ describe("TaskChatTab", () => {
|
||||
expect(screen.queryByLabelText("Executor: model provider unknown")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("keeps List View split chat agent headers before text tool thinking and user output", () => {
|
||||
mockLogs([
|
||||
makeEntry({ agent: "executor", text: "executor compact text", timestamp: "2026-06-12T00:00:00.000Z" }),
|
||||
makeEntry({ agent: "executor", type: "tool", text: "bash", detail: "pnpm test", timestamp: "2026-06-12T00:00:01.000Z" }),
|
||||
makeEntry({ agent: "executor", type: "tool_result", text: "bash", detail: "ok", timestamp: "2026-06-12T00:00:02.000Z" }),
|
||||
makeEntry({ agent: "executor", type: "thinking", text: "checking compact layout", timestamp: "2026-06-12T00:00:03.000Z" }),
|
||||
makeEntry({ text: "fallback compact text", timestamp: "2026-06-12T00:00:05.000Z" }),
|
||||
]);
|
||||
|
||||
renderListSplitTaskChat(
|
||||
makeTask({
|
||||
modelProvider: "openai",
|
||||
modelId: "gpt-4o",
|
||||
steeringComments: [makeSteeringComment({ id: "compact-user", text: "compact user guidance", createdAt: "2026-06-12T00:00:04.000Z" })],
|
||||
}),
|
||||
);
|
||||
|
||||
const host = screen.getByTestId("list-split-detail-content");
|
||||
const executorGroup = within(host).getByLabelText("Executor messages");
|
||||
const fallbackGroup = within(host).getByLabelText("Agent messages");
|
||||
|
||||
expectAgentHeaderBeforeBubbles(executorGroup);
|
||||
expectAgentHeaderBeforeBubbles(fallbackGroup);
|
||||
expect(within(executorGroup).getAllByText("Executor")).toHaveLength(1);
|
||||
expect(within(fallbackGroup).getAllByText("Agent")).toHaveLength(1);
|
||||
expect(within(executorGroup).getByText("executor compact text")).toBeVisible();
|
||||
expect(within(executorGroup).getByTestId("task-chat-tool-group")).toBeInTheDocument();
|
||||
expect(within(executorGroup).getByTestId("task-chat-thinking")).toBeInTheDocument();
|
||||
expect(within(host).getByText("compact user guidance").closest(".task-chat-user-group")).not.toBeNull();
|
||||
expect(within(host).getByText("fallback compact text")).toBeVisible();
|
||||
expect(document.querySelector(".task-chat-provider-icon [data-provider='openai']")).toBeTruthy();
|
||||
expect(within(fallbackGroup).getByLabelText("Agent: model provider unknown")).toBeVisible();
|
||||
});
|
||||
|
||||
it("keeps List View split chat empty and loading states free of header shells", () => {
|
||||
mockLogs([], true);
|
||||
const loading = renderListSplitTaskChat();
|
||||
expect(screen.getByText(/Loading agent output/)).toBeVisible();
|
||||
expect(document.querySelector(".task-chat-group-header")).not.toBeInTheDocument();
|
||||
expect(document.querySelector(".task-chat-group-bubbles")).not.toBeInTheDocument();
|
||||
|
||||
loading.unmount();
|
||||
mockLogs([]);
|
||||
renderListSplitTaskChat();
|
||||
expect(screen.getByText(/No agent output yet/)).toBeVisible();
|
||||
expect(document.querySelector(".task-chat-group-header")).not.toBeInTheDocument();
|
||||
expect(document.querySelector(".task-chat-group-bubbles")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("groups consecutive entries by agent role", () => {
|
||||
mockLogs([
|
||||
makeEntry({ agent: "executor", text: "first" }),
|
||||
@@ -2413,6 +2478,31 @@ describe("TaskChatTab", () => {
|
||||
expect(source).toContain("expanded={chatExpanded}");
|
||||
});
|
||||
|
||||
it("stacks agent headers only inside the List View split-detail chat host", () => {
|
||||
const css = readFileSync(resolve(__dirname, "../TaskChatTab.css"), "utf8");
|
||||
const desktopGroupRule = getCssRuleBlock(css, ".task-chat-group");
|
||||
const listSplitGroupRule = getCssRuleBlock(css, ".list-split-detail-content .task-chat-group");
|
||||
const listSplitHeaderRule = getCssRuleBlock(css, ".list-split-detail-content .task-chat-group-header");
|
||||
const mobileCss = getCssAfter(css, "@media (max-width: 768px)");
|
||||
const mobileGroupRule = getCssRuleBlock(mobileCss, ".task-chat-group");
|
||||
|
||||
expect(desktopGroupRule).toContain("grid-template-columns: auto minmax(0, 1fr)");
|
||||
expect(listSplitGroupRule).toContain("grid-template-columns: 1fr");
|
||||
expect(listSplitHeaderRule).toContain("min-width: 0");
|
||||
expect(mobileGroupRule).toContain("grid-template-columns: 1fr");
|
||||
});
|
||||
|
||||
it("keeps List View as the only split-pane host for compact task chat", () => {
|
||||
const listSource = readFileSync(resolve(__dirname, "../ListView.tsx"), "utf8");
|
||||
const appSource = readFileSync(resolve(__dirname, "../../App.tsx"), "utf8");
|
||||
|
||||
expect(listSource).toContain('className="list-split-detail-content"');
|
||||
expect(listSource).toContain("<TaskDetailContent");
|
||||
expect(listSource).toContain("embedded");
|
||||
expect(appSource).toContain('className="task-detail-main-panel-body"');
|
||||
expect(appSource).toContain("<TaskDetailContent");
|
||||
});
|
||||
|
||||
it("keeps task chat timestamp styling tokenized and mobile-safe", () => {
|
||||
const css = readFileSync(resolve(__dirname, "../TaskChatTab.css"), "utf8");
|
||||
const groupMetaRule = getCssRuleBlock(css, ".task-chat-group-meta");
|
||||
|
||||
Reference in New Issue
Block a user