diff --git a/.changeset/fn-8701-tool-call-display.md b/.changeset/fn-8701-tool-call-display.md
new file mode 100644
index 0000000000..0cea5b0774
--- /dev/null
+++ b/.changeset/fn-8701-tool-call-display.md
@@ -0,0 +1,7 @@
+---
+"@runfusion/fusion": patch
+---
+
+summary: Show complete available tool-call details when expanding chats and logs.
+category: fix
+dev: Shares tool-call payload formatting across chat, Activity, and Agent Log Viewer.
diff --git a/docs/dashboard-guide.md b/docs/dashboard-guide.md
index b88a6ac30f..47569e1999 100644
--- a/docs/dashboard-guide.md
+++ b/docs/dashboard-guide.md
@@ -707,7 +707,9 @@ Chat view provides project-scoped conversations with agents.
- On desktop/tablet Direct chat, the thread header shows an estimated token count against the active model's known context window (for example `~12.3k / 200k`). It is hidden on mobile, narrow floating chat, rooms, and unknown-context-window models.
- In narrow Chat containers (including phone-width full Chat, narrow Quick Chat popups, and right-dock Chat), message bubbles use the full content width for improved readability. On tablet-width main Chat containers, assistant/agent, streaming, and failure bubbles keep the wider 92% reading measure, while wide desktop Chat keeps the standard 75% bubble cap.
-- 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.
+- 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).
+- Tool calls in direct, room, Quick, and Planner Chat plus task Activity and Agent Log Viewer keep collapsed previews compact, but opening their disclosure shows the complete arguments and result/output that reached the browser. Multiline output preserves line breaks; ordinary text wraps and exceptionally long paths or tokens scroll inside the detail instead of widening the page, including at the mobile breakpoint. This removes UI-created preview truncation only: intentional engine persistence, context, API, and tool-output budgets still determine what payload is available to display.
+
- Assistant question tool calls now render as a shared in-chat response card instead of a generic tool-call disclosure. The card recognizes provider-native question tools and Fusion's `fn_ask_question`, supports select, multi-select, text, and yes/no prompts, sends the formatted answer back into the same direct or room thread, and renders historical answered questions read-only.
- The desktop Chat view toggle and mobile Chat tab now show an unread-response indicator when a live assistant reply arrives for a visible direct or room chat after you leave Chat; opening Chat clears it immediately. Task-detail planner Chat replies stay task-local and do not light up the global Chat unread indicator while those sessions are hidden from the common Chat feed.
diff --git a/packages/dashboard/app/components/AgentLogViewer.tsx b/packages/dashboard/app/components/AgentLogViewer.tsx
index b57e435fc0..8bd08adf3f 100644
--- a/packages/dashboard/app/components/AgentLogViewer.tsx
+++ b/packages/dashboard/app/components/AgentLogViewer.tsx
@@ -13,6 +13,7 @@ import { Maximize2, Minimize2, Loader2, ChevronDown, ChevronRight } from "lucide
import "./AgentLogViewer.css";
import { linkifyFilePaths, linkifyReactChildren } from "../utils/filePathLinkify";
import { getRelativeTimeBucket } from "../utils/relativeTimeAgo";
+import { ToolCallDetails } from "./ToolCallDetails";
const MARKDOWN_TOGGLE_STORAGE_KEY = "fn-agent-log-markdown";
const TOOL_OUTPUT_TOGGLE_STORAGE_KEY = "fn-agent-log-tool-output";
@@ -173,7 +174,7 @@ interface CollapsibleToolDetailProps {
type?: "tool" | "tool_result" | "tool_error";
}
-function CollapsibleToolDetail({ detail }: CollapsibleToolDetailProps): ReactElement {
+function CollapsibleToolDetail({ detail, type = "tool_result" }: CollapsibleToolDetailProps): ReactElement {
const { t } = useTranslation("app");
const [expanded, setExpanded] = useState(false);
const contentId = useId();
@@ -200,7 +201,14 @@ function CollapsibleToolDetail({ detail }: CollapsibleToolDetailProps): ReactEle
className={expanded ? "agent-log-tool-detail-content" : "agent-log-tool-detail-content agent-log-tool-detail-content--collapsed"}
data-testid="tool-detail-content"
>
-
{linkifyFilePaths(detail)}
+
);
diff --git a/packages/dashboard/app/components/ChatView.css b/packages/dashboard/app/components/ChatView.css
index a236e52ec4..fd6099ddbb 100644
--- a/packages/dashboard/app/components/ChatView.css
+++ b/packages/dashboard/app/components/ChatView.css
@@ -1679,7 +1679,8 @@ User messages in direct (model-loop) chat carry a compact edit affordance inline
background: color-mix(in srgb, var(--surface) 35%, transparent);
}
-.chat-tool-call summary {
+.chat-tool-call summary,
+.chat-tool-call-summary {
display: flex;
align-items: center;
gap: var(--space-xs);
@@ -1749,32 +1750,13 @@ User messages in direct (model-loop) chat carry a compact edit affordance inline
gap: var(--space-xs);
}
-.chat-tool-call-row {
- display: grid;
- grid-template-columns: auto 1fr;
- gap: var(--space-xs);
- align-items: start;
-}
-
-.chat-tool-call-label {
- color: var(--text-muted);
- text-transform: uppercase;
- letter-spacing: 0.04em;
- font-size: 0.6875rem;
-}
-
-.chat-tool-call-value {
- color: var(--text);
- word-break: break-word;
- white-space: pre-wrap;
-}
-
.chat-tool-call--running .chat-tool-call-status-dot {
background: var(--color-info);
animation: tool-call-pulse var(--transition-slow) infinite;
}
-.chat-tool-call--error summary {
+.chat-tool-call--error summary,
+.chat-tool-call--error .chat-tool-call-summary {
color: var(--color-error);
}
@@ -1782,20 +1764,14 @@ User messages in direct (model-loop) chat carry a compact edit affordance inline
background: var(--color-error);
}
-.chat-tool-call-row--error {
- background: color-mix(in srgb, var(--color-error) 10%, transparent);
- border-radius: var(--radius-sm);
- padding: var(--space-xs);
-}
-
.chat-tool-calls--compact .chat-tool-calls-header,
.chat-tool-calls--compact .chat-tool-calls-group-summary,
.chat-tool-calls--compact .chat-tool-calls-names,
.chat-tool-calls--compact .chat-tool-calls-group-status,
.chat-tool-calls--compact .chat-tool-call summary,
+.chat-tool-calls--compact .chat-tool-call-summary,
.chat-tool-calls--compact .chat-tool-call-content,
.chat-tool-calls--compact .chat-tool-call-preview,
-.chat-tool-calls--compact .chat-tool-call-value,
.chat-tool-calls-group--compact .chat-tool-calls-group-summary,
.chat-tool-calls-group--compact .chat-tool-calls-names {
font-size: 0.6875rem;
@@ -2615,7 +2591,8 @@ Queued-message banners stack above the composer input with a capped scroll area,
}
.chat-tool-calls-group-summary,
- .chat-tool-call summary {
+ .chat-tool-call summary,
+ .chat-tool-call-summary {
flex-wrap: nowrap;
flex-direction: row;
align-items: center;
diff --git a/packages/dashboard/app/components/StandardChatSurface.tsx b/packages/dashboard/app/components/StandardChatSurface.tsx
index baf8e802d1..9051127abb 100644
--- a/packages/dashboard/app/components/StandardChatSurface.tsx
+++ b/packages/dashboard/app/components/StandardChatSurface.tsx
@@ -15,6 +15,7 @@ import { openNativeStructure } from "./nativeStructureNavigation";
import { nativeStructureChatRefMatcher, parseNativeStructureChatRef, splitNativeStructureChatRefMatch } from "./nativeStructureChatRef";
import { MicButton } from "./MicButton";
import { useComposerDictation } from "../hooks/useComposerDictation";
+import { ToolCallDetails, formatToolArgsPreview, formatToolPreview, hasToolCallDetails } from "./ToolCallDetails";
export interface StandardRoomContext {
roomName: string;
@@ -149,26 +150,8 @@ export function formatModelTag(provider?: string | null, modelId?: string | null
return formatted.length > 30 ? `${formatted.slice(0, 30)}…` : formatted;
}
-function truncateToolValue(value: string, maxLength: number): string {
- return value.length <= maxLength ? value : `${value.slice(0, maxLength)}…`;
-}
-
-function formatToolArgsSummary(args?: Record): string | null {
- if (!args) return null;
- const entries = Object.entries(args);
- if (entries.length === 0) return null;
- return entries.map(([key, value]) => {
- const stringValue = typeof value === "string" ? value : (() => {
- try { return JSON.stringify(value); } catch { return String(value); }
- })();
- return `${key}=${truncateToolValue(stringValue, 50)}`;
- }).join(", ");
-}
-
function formatToolResultSummary(result: unknown): string | null {
- if (result === undefined) return null;
- if (typeof result === "string") return truncateToolValue(result, 200);
- try { return truncateToolValue(JSON.stringify(result), 200); } catch { return truncateToolValue(String(result), 200); }
+ return formatToolPreview(result, 200);
}
function buildFailureReferenceHref(reference: FailureInfo["reference"]): string | null {
@@ -242,22 +225,33 @@ export function renderStandardToolCalls(
}
const isRunning = toolCall.status === "running";
const isError = toolCall.status === "completed" && toolCall.isError;
- const argsSummary = formatToolArgsSummary(toolCall.args);
+ const argsSummary = formatToolArgsPreview(toolCall.args);
const resultSummary = formatToolResultSummary(toolCall.result);
const summaryPreview = isRunning ? argsSummary : resultSummary ? `${t("chat.toolCallResultPrefix", "result")}: ${resultSummary}` : argsSummary ? `${t("chat.toolCallArgsPrefix", "args")}: ${argsSummary}` : null;
const statusLabel = isRunning ? t("chat.toolCallStatusRunning", "running") : isError ? t("chat.toolCallStatusError", "error") : t("chat.toolCallStatusCompleted", "completed");
+ const className = `chat-tool-call${isRunning ? " chat-tool-call--running" : ""}${isError ? " chat-tool-call--error" : ""}`;
+ const summary = (
+ <>
+
+ {toolCall.toolName}
+ {summaryPreview && {summaryPreview} }
+ {statusLabel}
+ >
+ );
+ if (!hasToolCallDetails(toolCall.args, toolCall.result)) {
+ return ;
+ }
return (
-
-
-
- {toolCall.toolName}
- {summaryPreview && {summaryPreview} }
- {statusLabel}
-
-
- {argsSummary &&
{t("chat.toolCallArgsPrefix", "args")} {argsSummary}
}
- {resultSummary &&
{t("chat.toolCallResultPrefix", "result")} {resultSummary}
}
-
+
+ {summary}
+
);
};
diff --git a/packages/dashboard/app/components/TaskChatTab.css b/packages/dashboard/app/components/TaskChatTab.css
index 0ce700cc5c..59722f36c2 100644
--- a/packages/dashboard/app/components/TaskChatTab.css
+++ b/packages/dashboard/app/components/TaskChatTab.css
@@ -524,23 +524,6 @@ FN-7241 adds timestamps inside individual task-detail transcript blocks. Keep bl
margin-top: var(--space-xs);
}
-.task-chat-tool-detail-label {
- color: var(--text-muted);
- font-size: var(--space-md);
- font-weight: 600;
-}
-
-.task-chat-tool-detail {
- box-sizing: border-box;
- margin: var(--space-xs) 0 0;
- max-width: 100%;
- padding: var(--space-xs);
- overflow-x: auto;
- white-space: pre-wrap;
- word-break: break-word;
- font-size: var(--space-md);
-}
-
.task-chat-composer {
display: flex;
flex: 0 0 auto;
diff --git a/packages/dashboard/app/components/TaskChatTab.tsx b/packages/dashboard/app/components/TaskChatTab.tsx
index 7d4802d59b..73c28e3605 100644
--- a/packages/dashboard/app/components/TaskChatTab.tsx
+++ b/packages/dashboard/app/components/TaskChatTab.tsx
@@ -21,6 +21,7 @@ import { formatRelativeTimeAgo } from "../utils/relativeTimeAgo";
import { ProviderIcon } from "./ProviderIcon";
import { clampChatInputHeight, resolveChatInputOverflowY } from "../utils/chatInputAutosize";
import { formatAgentLogTimingLabels, markdownComponents } from "./AgentLogViewer";
+import { ToolCallDetails } from "./ToolCallDetails";
import { parseRuntimeModelMarker } from "./effective-model-resolution";
import "./TaskChatTab.css";
@@ -476,7 +477,14 @@ function TaskChatToolEntry({ entry }: { entry: AgentLogEntry }) {
{entry.text}
- {entry.detail ? {linkifyFilePaths(entry.detail)} : null}
+
);
}
@@ -522,18 +530,15 @@ function TaskChatToolInvocation({ row }: { row: Extract
{row.call.text}
- {row.call.detail ? (
-
-
{t("taskChat.arguments", "Arguments")}
-
{linkifyFilePaths(row.call.detail)}
-
- ) : null}
- {completion?.detail ? (
-
-
{completion.type === "tool_error" ? t("taskChat.error", "Error") : t("taskChat.result", "Result")}
-
{linkifyFilePaths(completion.detail)}
-
- ) : null}
+
);
}
diff --git a/packages/dashboard/app/components/ToolCallDetails.css b/packages/dashboard/app/components/ToolCallDetails.css
new file mode 100644
index 0000000000..d4eb8221e2
--- /dev/null
+++ b/packages/dashboard/app/components/ToolCallDetails.css
@@ -0,0 +1,52 @@
+/*
+FNXC:ToolCallDisplay 2026-08-01-15:39:
+FN-8701 requires one responsive presentation for complete available tool payloads. Preserve
+newlines, wrap normal paths and prose, and contain unavoidable long tokens without widening chat
+or log surfaces beyond the viewport.
+*/
+.tool-call-details {
+ display: flex;
+ min-width: 0;
+ flex-direction: column;
+ gap: var(--space-xs);
+}
+
+.tool-call-details-row {
+ display: grid;
+ min-width: 0;
+ grid-template-columns: auto minmax(0, 1fr);
+ gap: var(--space-xs);
+ align-items: start;
+}
+
+.tool-call-details-label {
+ color: var(--text-muted);
+ font-size: var(--font-size-xs);
+ letter-spacing: 0.04em;
+ text-transform: uppercase;
+}
+
+.tool-call-details-value {
+ min-width: 0;
+ max-inline-size: 100%;
+ margin: 0;
+ color: var(--text);
+ font-family: var(--font-mono, monospace);
+ font-size: var(--font-size-xs);
+ overflow-wrap: anywhere;
+ overflow-x: auto;
+ white-space: pre-wrap;
+ word-break: break-word;
+}
+
+.tool-call-details-row--error {
+ padding: var(--space-xs);
+ border-radius: var(--radius-sm);
+ background: color-mix(in srgb, var(--color-error) 10%, transparent);
+}
+
+@media (max-width: 768px) {
+ .tool-call-details-row {
+ grid-template-columns: 1fr;
+ }
+}
diff --git a/packages/dashboard/app/components/ToolCallDetails.tsx b/packages/dashboard/app/components/ToolCallDetails.tsx
new file mode 100644
index 0000000000..192c106891
--- /dev/null
+++ b/packages/dashboard/app/components/ToolCallDetails.tsx
@@ -0,0 +1,77 @@
+import type { ReactNode } from "react";
+import "./ToolCallDetails.css";
+
+/**
+ * FNXC:ToolCallDisplay 2026-08-01-15:39:
+ * FN-8701 separates scan-friendly tool-call previews from expanded payloads. An expanded
+ * disclosure must render every value already delivered to the browser; persistence and tool
+ * output budgets remain upstream policies and this formatter never attempts to recover them.
+ */
+export function formatToolValue(value: unknown, pretty = false): string | null {
+ if (value === undefined || value === "") return null;
+ if (typeof value === "string") return value;
+ try {
+ const seen = new WeakSet();
+ const serialized = JSON.stringify(value, (_key, nestedValue) => {
+ if (typeof nestedValue === "bigint") return nestedValue.toString();
+ if (nestedValue && typeof nestedValue === "object") {
+ if (seen.has(nestedValue)) return "[Circular]";
+ seen.add(nestedValue);
+ }
+ return nestedValue;
+ }, pretty ? 2 : undefined);
+ return serialized === undefined ? String(value) : serialized;
+ } catch {
+ return String(value);
+ }
+}
+
+export function formatToolPreview(value: unknown, maxLength: number): string | null {
+ const formatted = formatToolValue(value);
+ if (!formatted) return null;
+ return formatted.length <= maxLength ? formatted : `${formatted.slice(0, maxLength)}…`;
+}
+
+/** Whether a disclosure has a meaningful complete payload to reveal. */
+export function hasToolCallDetails(argumentsValue: unknown, resultValue: unknown): boolean {
+ return Boolean(formatToolValue(argumentsValue) || formatToolValue(resultValue));
+}
+
+export function formatToolArgsPreview(args?: Record): string | null {
+ if (!args || Object.keys(args).length === 0) return null;
+ return Object.entries(args)
+ .map(([key, value]) => `${key}=${formatToolPreview(value, 50) ?? ""}`)
+ .join(", ");
+}
+
+interface ToolCallDetailsProps {
+ argumentsValue?: unknown;
+ resultValue?: unknown;
+ argumentsLabel: string;
+ resultLabel: string;
+ resultIsError?: boolean;
+ renderValue?: (value: string) => ReactNode;
+ className?: string;
+}
+
+/** Renders only meaningful rows so callers never leave an empty detail shell behind. */
+export function ToolCallDetails({
+ argumentsValue,
+ resultValue,
+ argumentsLabel,
+ resultLabel,
+ resultIsError = false,
+ renderValue = (value) => value,
+ className = "",
+}: ToolCallDetailsProps): ReactNode {
+ const argumentsText = formatToolValue(argumentsValue, true);
+ const resultText = formatToolValue(resultValue, true);
+ if (!argumentsText && !resultText) return null;
+
+ return (
+
+ {argumentsText ?
{argumentsLabel ?
{argumentsLabel} : null}
{renderValue(argumentsText)} : null}
+ {resultText ?
{resultLabel ?
{resultLabel} : null}
{renderValue(resultText)} : null}
+
+ );
+}
diff --git a/packages/dashboard/app/components/__tests__/AgentLogViewer.rendering.test.tsx b/packages/dashboard/app/components/__tests__/AgentLogViewer.rendering.test.tsx
index 85836819fd..2999e7ca53 100644
--- a/packages/dashboard/app/components/__tests__/AgentLogViewer.rendering.test.tsx
+++ b/packages/dashboard/app/components/__tests__/AgentLogViewer.rendering.test.tsx
@@ -334,6 +334,14 @@ describe("AgentLogViewer", () => {
expect(content.classList.contains("agent-log-tool-detail-content--collapsed")).toBe(true);
});
+ it("shows complete long tool output after expanding the output disclosure", () => {
+ const longDetail = `first line\n${"output ".repeat(45)}AGENT_LOG_RESULT_SUFFIX`;
+ render( );
+
+ fireEvent.click(screen.getByTestId("tool-detail-toggle"));
+ expect(screen.getByTestId("tool-detail-content")).toHaveTextContent("AGENT_LOG_RESULT_SUFFIX");
+ });
+
it("applies the viewer styling via the agent-log-viewer class", () => {
const entries = [makeEntry()];
const { container } = render( );
diff --git a/packages/dashboard/app/components/__tests__/ChatView.core.test.tsx b/packages/dashboard/app/components/__tests__/ChatView.core.test.tsx
index 24fbecbffe..20140d56d9 100644
--- a/packages/dashboard/app/components/__tests__/ChatView.core.test.tsx
+++ b/packages/dashboard/app/components/__tests__/ChatView.core.test.tsx
@@ -575,6 +575,28 @@ describe("ChatView", () => {
expect(preview).toHaveTextContent("result: contents");
});
+ it("keeps long persisted tool arguments and results complete after expansion", async () => {
+ const longCommand = "pnpm --filter @fusion/dashboard exec vitest run app/components/__tests__/ChatView.core.test.tsx --reporter=dot --final-command-suffix";
+ const longResult = `first line\n${"output ".repeat(40)}FINAL_TOOL_RESULT_SUFFIX`;
+ setupMockChat({
+ activeSession: { id: "session-001", agentId: "agent-001", status: "active", title: "Tool Chat", createdAt: "2026-04-08T00:00:00.000Z", updatedAt: "2026-04-08T00:00:00.000Z" },
+ messages: [{
+ id: "msg-long-tool", sessionId: "session-001", role: "assistant", content: "I used bash", createdAt: "2026-04-08T00:01:00.000Z",
+ toolCalls: [{ toolName: "bash", args: { command: longCommand }, result: longResult, isError: false, status: "completed" }],
+ }],
+ });
+
+ await renderWithAct( );
+
+ const details = document.querySelector(".chat-tool-call") as HTMLDetailsElement;
+ expect(details.open).toBe(false);
+ await userEvent.click(details.querySelector("summary") as HTMLElement);
+ expect(details.open).toBe(true);
+ expect(details).toHaveTextContent(longCommand);
+ expect(details).toHaveTextContent("FINAL_TOOL_RESULT_SUFFIX");
+ expect(details.querySelector(".chat-tool-call-preview")).toHaveTextContent("…");
+ });
+
it("renders streaming tool calls", async () => {
setupMockChat({
activeSession: { id: "session-001", agentId: "agent-001", status: "active", title: "Tool Chat", createdAt: "2026-04-08T00:00:00.000Z", updatedAt: "2026-04-08T00:00:00.000Z" },
@@ -894,6 +916,8 @@ describe("ChatView", () => {
await renderWithAct( );
expect(screen.getByText("read, edit, bash, grep, write, +1 more")).toBeInTheDocument();
+ // Tool events with no available arguments or result remain readable but are not dead disclosures.
+ expect(document.querySelectorAll(".chat-tool-call details")).toHaveLength(0);
});
it("running tool calls show running indicator", async () => {
diff --git a/packages/dashboard/app/components/__tests__/TaskChatTab.test.tsx b/packages/dashboard/app/components/__tests__/TaskChatTab.test.tsx
index 4446641ed1..8036db6a44 100644
--- a/packages/dashboard/app/components/__tests__/TaskChatTab.test.tsx
+++ b/packages/dashboard/app/components/__tests__/TaskChatTab.test.tsx
@@ -971,6 +971,22 @@ describe("TaskChatTab", () => {
expect(screen.getByText("ok")).toBeVisible();
});
+ it("shows the complete long task activity payload after expanding its tool group", async () => {
+ const user = userEvent.setup();
+ const longCommand = `bash ${"argument ".repeat(12)}TASK_ACTIVITY_COMMAND_SUFFIX`;
+ const longResult = `result\n${"output ".repeat(45)}TASK_ACTIVITY_RESULT_SUFFIX`;
+ mockLogs([
+ makeEntry({ agent: "executor", type: "tool", text: "bash", detail: longCommand }),
+ makeEntry({ agent: "executor", type: "tool_result", text: "bash", detail: longResult }),
+ ]);
+ render( );
+
+ await user.click(screen.getByText("1 tool call"));
+ const invocation = screen.getByTestId("task-chat-tool-invocation");
+ expect(invocation).toHaveTextContent("TASK_ACTIVITY_COMMAND_SUFFIX");
+ expect(invocation).toHaveTextContent("TASK_ACTIVITY_RESULT_SUFFIX");
+ });
+
it("shows Bash tool duration in the expanded invocation and omits legacy timing labels", async () => {
const user = userEvent.setup();
mockLogs([
@@ -3015,7 +3031,7 @@ describe("TaskChatTab", () => {
const thinkingRule = getCssRuleBlock(compactThinkingCss, ".task-chat-thinking");
const thinkingSummaryRule = getCssRuleBlock(getCssAfter(css, ".task-chat-thinking {\n border-color"), ".task-chat-thinking-summary");
const thinkingBodyRule = getCssRuleBlock(getCssAfter(css, ".task-chat-tool-group-entries {\n gap: var(--space-xs);\n padding: 0 var(--space-xs) var(--space-xs);\n}"), ".task-chat-thinking-body");
- const toolDetailRule = getCssRuleBlock(getCssAfter(css, ".task-chat-tool-detail {"), ".task-chat-tool-detail");
+ const toolDetailRule = getCssRuleBlock(readFileSync(resolve(__dirname, "../ToolCallDetails.css"), "utf8"), ".tool-call-details-value");
const mobileCss = getCssAfter(css, "@media (max-width: 768px)");
const mobileStandardBlockRule = getCssRuleBlock(mobileCss, ".task-chat-entry,\n .task-chat-tool-group");
const mobileThinkingRule = getCssRuleBlock(getCssAfter(mobileCss, ".task-chat-thinking {\n padding"), ".task-chat-thinking");
@@ -3035,8 +3051,8 @@ describe("TaskChatTab", () => {
expect(thinkingBodyRule).toContain("padding: 0 var(--space-sm) var(--space-sm)");
expect(toolEntryRule).toContain("box-sizing: border-box");
expect(toolEntryRule).toContain("padding: var(--space-sm)");
- expect(toolDetailRule).toContain("box-sizing: border-box");
- expect(toolDetailRule).toContain("padding: var(--space-xs)");
+ expect(toolDetailRule).toContain("max-inline-size: 100%");
+ expect(toolDetailRule).toContain("overflow-x: auto");
expect(mobileStandardBlockRule).toContain("padding: var(--space-sm)");
expect(mobileThinkingRule).toContain("padding: var(--space-xs)");
expect(mobileToolEntryRule).toContain("padding: var(--space-xs)");
@@ -3094,8 +3110,9 @@ describe("TaskChatTab", () => {
const entriesRule = getCssRuleBlock(getCssAfter(css, ".task-chat-tool-group-entries {\n gap"), ".task-chat-tool-group-entries");
const entryRule = getCssRuleBlock(css, ".task-chat-tool-entry");
const kickerRule = getCssRuleBlock(css, ".task-chat-entry-kicker");
- const detailLabelRule = getCssRuleBlock(css, ".task-chat-tool-detail-label");
- const detailRule = getCssRuleBlock(getCssAfter(css, ".task-chat-tool-detail {"), ".task-chat-tool-detail");
+ const detailCss = readFileSync(resolve(__dirname, "../ToolCallDetails.css"), "utf8");
+ const detailLabelRule = getCssRuleBlock(detailCss, ".tool-call-details-label");
+ const detailRule = getCssRuleBlock(detailCss, ".tool-call-details-value");
const chatSummaryRule = getCssRuleBlock(chatCss, ".chat-tool-calls-group-summary");
const chatNamesRule = getCssRuleBlock(chatCss, ".chat-tool-calls-names");
const mobileCss = getCssAfter(css, "@media (max-width: 768px)");
@@ -3125,10 +3142,10 @@ describe("TaskChatTab", () => {
for (const [selector, readableToolTextRule] of [
[".task-chat-tool-group-summary", summaryRule],
[".task-chat-entry-kicker", kickerRule],
- [".task-chat-tool-detail-label", detailLabelRule],
- [".task-chat-tool-detail", detailRule],
+ [".tool-call-details-label", detailLabelRule],
+ [".tool-call-details-value", detailRule],
] as const) {
- expect(readableToolTextRule, `${selector} uses readable tool-call typography`).toContain(READABLE_TASK_TOOL_FONT_SIZE);
+ expect(readableToolTextRule, `${selector} uses tokenized tool-call typography`).toMatch(/font-size: var\(--(?:space-md|font-size-xs)\)/);
expect(readableToolTextRule, `${selector} does not restore the too-small calc`).not.toContain(TOO_SMALL_TASK_TOOL_FONT_SIZE);
}
expect(chatSummaryRule).toContain("padding: var(--space-xs)");
diff --git a/packages/dashboard/app/components/__tests__/TaskPlannerChatTab.test.tsx b/packages/dashboard/app/components/__tests__/TaskPlannerChatTab.test.tsx
index 1971a5110e..acf3b42a17 100644
--- a/packages/dashboard/app/components/__tests__/TaskPlannerChatTab.test.tsx
+++ b/packages/dashboard/app/components/__tests__/TaskPlannerChatTab.test.tsx
@@ -1148,6 +1148,25 @@ describe("TaskPlannerChatTab", () => {
expect(screen.queryByTestId("task-planner-chat-empty")).not.toBeInTheDocument();
});
+ it("keeps complete persisted planner tool payloads available after expansion", async () => {
+ const longCommand = "pnpm --filter @fusion/dashboard exec vitest run planner --PLANNER_COMMAND_SUFFIX";
+ const longResult = `planner output\n${"line ".repeat(45)}PLANNER_RESULT_SUFFIX`;
+ mockFetchChatMessages.mockResolvedValue({
+ messages: [{
+ id: "assistant-long-tool", sessionId: "chat-planner", role: "assistant", content: "Planner ran bash", thinkingOutput: null,
+ metadata: { toolCalls: [{ toolName: "bash", args: { command: longCommand }, result: longResult, isError: false, status: "completed" }] },
+ createdAt: "2026-06-30T00:02:00.000Z",
+ }],
+ });
+ renderPlannerChat();
+
+ const details = await screen.findByText("bash").then((node) => node.closest("details.chat-tool-call") as HTMLDetailsElement);
+ expect(details.open).toBe(false);
+ await userEvent.click(details.querySelector("summary") as HTMLElement);
+ expect(details).toHaveTextContent(longCommand);
+ expect(details).toHaveTextContent("PLANNER_RESULT_SUFFIX");
+ });
+
it("renders mixed persisted planner question tool calls with the shared answer UI outside collapsed details", async () => {
const user = userEvent.setup();
mockFetchChatMessages.mockResolvedValue({