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:
5
.changeset/fn-3602-compact-chat-tool-call-mobile.md
Normal file
5
.changeset/fn-3602-compact-chat-tool-call-mobile.md
Normal 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.
|
||||
@@ -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.
|
||||
|
||||

|
||||
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
Reference in New Issue
Block a user