From b9821eebd732a4cb88f9f5f2dafe5a7ded4a4333 Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Tue, 23 Jun 2026 21:57:53 -0700 Subject: [PATCH] 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 --- .changeset/fn-6965-list-pane-chat-layout.md | 5 ++ .../dashboard/app/components/TaskChatTab.css | 12 +++ .../components/__tests__/TaskChatTab.test.tsx | 90 +++++++++++++++++++ 3 files changed, 107 insertions(+) create mode 100644 .changeset/fn-6965-list-pane-chat-layout.md diff --git a/.changeset/fn-6965-list-pane-chat-layout.md b/.changeset/fn-6965-list-pane-chat-layout.md new file mode 100644 index 0000000000..6e4e1995fa --- /dev/null +++ b/.changeset/fn-6965-list-pane-chat-layout.md @@ -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. diff --git a/packages/dashboard/app/components/TaskChatTab.css b/packages/dashboard/app/components/TaskChatTab.css index 746a4a844c..a49c30f140 100644 --- a/packages/dashboard/app/components/TaskChatTab.css +++ b/packages/dashboard/app/components/TaskChatTab.css @@ -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; diff --git a/packages/dashboard/app/components/__tests__/TaskChatTab.test.tsx b/packages/dashboard/app/components/__tests__/TaskChatTab.test.tsx index 9d54fe43f4..3c098d1ff5 100644 --- a/packages/dashboard/app/components/__tests__/TaskChatTab.test.tsx +++ b/packages/dashboard/app/components/__tests__/TaskChatTab.test.tsx @@ -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( +
+ +
, + ); +} + 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(" { const css = readFileSync(resolve(__dirname, "../TaskChatTab.css"), "utf8"); const groupMetaRule = getCssRuleBlock(css, ".task-chat-group-meta");