feat(FN-3602): compact mobile chat tool-call density and layout

Merged FN-3602: Compact tool-call summaries in the ChatView with responsive mobile layout improvements, including new regression tests and documentation in the dashboard guide.

Fusion-Task-Id: FN-3602
This commit is contained in:
Fusion
2026-05-06 13:51:44 -07:00
committed by gsxdsm
parent d7efe67fe5
commit 85381df153
6 changed files with 91 additions and 17 deletions

View File

@@ -0,0 +1,5 @@
---
"@runfusion/fusion": patch
---
Improve full Chat mobile tool-call cards by keeping collapsed summaries on a single row and tightening spacing for denser scanning without changing expand/collapse behavior.

View File

@@ -49,6 +49,7 @@ Chat view provides project-scoped conversations with agents.
- Full Chat and Quick Chat both consume the same streamed `/api/chat/sessions/:id/messages` response contract, and both now prefer the authoritative assistant `message` snapshot on `done` while still accumulating `text` chunks when present (so providers without incremental text streaming still render output immediately)
- Chat message lists now track near-bottom scroll state: while you are reading older messages, live streaming/new replies do not force-scroll; a **Latest** jump control appears until you return to the tail.
- On mobile (`max-width: 768px`), chat bubbles are slightly wider in full Chat for improved readability while preserving header/composer gutters.
- Full Chat tool-call summaries now use a denser mobile layout: grouped and single-call collapsed rows keep icon + label + status on one line (Quick Chat-style scanability) while expanded details remain unchanged.
![Chat view](./screenshots/chat-view.png)

View File

@@ -0,0 +1,43 @@
import { describe, expect, it } from "vitest";
import { loadAllAppCss } from "../test/cssFixture";
function extractMobileMediaBlocks(content: string): string {
const blocks: string[] = [];
const regex = /@media\s*\(\s*max-width:\s*768px\s*\)\s*\{/g;
let match: RegExpExecArray | null;
while ((match = regex.exec(content)) !== null) {
const startIdx = match.index + match[0].length;
let braceCount = 1;
let endIdx = startIdx;
while (braceCount > 0 && endIdx < content.length) {
if (content[endIdx] === "{") braceCount += 1;
if (content[endIdx] === "}") braceCount -= 1;
endIdx += 1;
}
if (braceCount === 0) {
blocks.push(content.slice(startIdx, endIdx - 1));
}
}
return blocks.join("\n");
}
describe("chat tool-call mobile layout css", () => {
const css = loadAllAppCss();
const mobileCss = extractMobileMediaBlocks(css);
it("keeps full-chat grouped and single tool-call summaries on one row in mobile media blocks", () => {
const groupedSummaryRule = mobileCss.match(/\.chat-tool-calls-group-summary,\s*\n\s*\.chat-tool-call summary\s*\{[^}]*\}/m)?.[0] ?? "";
expect(groupedSummaryRule).toMatch(/flex-wrap:\s*nowrap/);
expect(groupedSummaryRule).toMatch(/flex-direction:\s*row/);
expect(groupedSummaryRule).toMatch(/align-items:\s*center/);
const nowrapRule = mobileCss.match(/\.chat-tool-calls-names,\s*\n\s*\.chat-tool-call-name,\s*\n\s*\.chat-tool-call-status-text,\s*\n\s*\.chat-tool-calls-group-status,\s*\n\s*\.chat-tool-calls-count\s*\{[^}]*\}/m)?.[0] ?? "";
expect(nowrapRule).toMatch(/white-space:\s*nowrap/);
const allMobileSummaryRules = [...mobileCss.matchAll(/\.chat-tool-calls-group-summary\s*\{[^}]*\}/g)].map((m) => m[0]);
expect(allMobileSummaryRules.length).toBeGreaterThan(0);
expect(allMobileSummaryRules.every((rule) => !/flex-direction:\s*column/.test(rule))).toBe(true);
});
});

View File

@@ -501,10 +501,10 @@
.chat-tool-calls-group-summary {
display: flex;
align-items: center;
gap: var(--space-sm);
gap: var(--space-xs);
list-style: none;
cursor: pointer;
padding: var(--space-xs) var(--space-sm);
padding: var(--space-xs);
color: var(--text-muted);
border-radius: var(--radius-sm);
font-size: var(--space-md);
@@ -524,13 +524,18 @@
box-shadow: var(--focus-ring-strong);
}
.chat-tool-calls-count {
white-space: nowrap;
flex-shrink: 0;
}
.chat-tool-calls-names {
color: var(--text-dim);
font-family: var(--font-mono, monospace);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
flex: 1;
flex: 1 1 auto;
min-width: 0;
font-size: 0.6875rem;
}
@@ -543,11 +548,11 @@
}
.chat-tool-calls-group > .chat-tool-call {
margin: 0 var(--space-sm) var(--space-xs) var(--space-sm);
margin: 0 var(--space-xs) var(--space-xs);
}
.chat-tool-calls-group > .chat-tool-call:last-child {
margin-bottom: var(--space-sm);
margin-bottom: var(--space-xs);
}
.chat-tool-call {
@@ -562,7 +567,7 @@
gap: var(--space-xs);
list-style: none;
cursor: pointer;
padding: var(--space-xs) var(--space-sm);
padding: var(--space-xs);
color: var(--text-muted);
border-radius: var(--radius-sm);
font-size: var(--space-md);
@@ -592,7 +597,11 @@
.chat-tool-call-name {
font-family: var(--font-mono, monospace);
flex-shrink: 0;
flex: 0 1 auto;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.chat-tool-call-preview {
@@ -612,7 +621,7 @@
}
.chat-tool-call-content {
margin: var(--space-xs) var(--space-sm) var(--space-sm);
margin: var(--space-xs);
padding: var(--space-sm);
border-radius: var(--radius-sm);
background: var(--bg);
@@ -1106,13 +1115,24 @@
opacity: 1;
}
.chat-tool-calls-group-summary {
flex-direction: column;
align-items: flex-start;
.chat-tool-calls-group-summary,
.chat-tool-call summary {
flex-wrap: nowrap;
flex-direction: row;
align-items: center;
}
.chat-tool-calls-group-status {
justify-content: flex-start;
.chat-tool-calls-names,
.chat-tool-call-name,
.chat-tool-call-status-text,
.chat-tool-calls-group-status,
.chat-tool-calls-count {
white-space: nowrap;
}
.chat-tool-calls-group-status,
.chat-tool-call-status-text {
margin-left: auto;
}
.chat-attach-btn,

View File

@@ -184,7 +184,7 @@ function renderToolCalls(toolCalls?: ToolCallInfo[]): ReactNode {
>
<summary>
<span className="chat-tool-call-status-dot" aria-hidden="true" />
<span className="chat-tool-call-name">{toolCall.toolName}</span>
<span className="chat-tool-call-name" title={toolCall.toolName}>{toolCall.toolName}</span>
{summaryPreview && (
<span className="chat-tool-call-preview" title={summaryPreview}>
{summaryPreview}
@@ -243,7 +243,7 @@ function renderToolCalls(toolCalls?: ToolCallInfo[]): ReactNode {
<details className="chat-tool-calls-group" data-testid="chat-tool-calls-group" open={hasRunning}>
<summary className="chat-tool-calls-group-summary">
<Wrench size={12} aria-hidden="true" />
<span>{toolCalls.length} tool calls</span>
<span className="chat-tool-calls-count">{toolCalls.length} tool calls</span>
<span className="chat-tool-calls-names" title={namesSummary}>{namesSummary}</span>
{statusSummary && <span className="chat-tool-calls-group-status">{statusSummary}</span>}
</summary>

View File

@@ -593,8 +593,11 @@ describe("ChatView", () => {
const group = screen.getByTestId("chat-tool-calls-group") as HTMLDetailsElement;
expect(group).toBeInTheDocument();
expect(group.open).toBe(false);
expect(screen.getByText("2 tool calls")).toBeInTheDocument();
expect(screen.getByText("read, grep")).toBeInTheDocument();
const summary = group.querySelector(".chat-tool-calls-group-summary") as HTMLElement;
expect(summary).toBeInTheDocument();
expect(summary.querySelector(".chat-tool-calls-count")).toHaveTextContent("2 tool calls");
expect(summary.querySelector(".chat-tool-calls-names")).toHaveTextContent("read, grep");
});
it("auto-opens grouped tool calls when any tool call is running", () => {
@@ -771,6 +774,8 @@ describe("ChatView", () => {
const details = document.querySelector(".chat-tool-call") as HTMLDetailsElement | null;
expect(details).toBeInTheDocument();
expect(details?.open).toBe(false);
expect(details?.querySelector(".chat-tool-call-name")).toHaveTextContent("read");
expect(details?.querySelector(".chat-tool-call-status-text")).toHaveTextContent("completed");
});
it("truncates tool names when more than 5 unique", () => {