feat(FN-2926): enhance grouped tool-call UX across dashboard and TUI
- Inline grouped tool-call rendering in chat surfaces and update summary logic for clearer aggregation - Refine chat, quick FAB, settings, and agents UI styles/behavior with matching test coverage updates - Extend dashboard TUI state/controller flow with status and shortcut improvements - Update engine and dashboard route handling related to agent runs, settings memory sync, and remote access adapters Fusion-Task-Id: FN-2926
This commit is contained in:
@@ -18,6 +18,7 @@ import {
|
||||
EyeOff,
|
||||
Paperclip,
|
||||
File,
|
||||
Wrench,
|
||||
} from "lucide-react";
|
||||
import { useChat, type ToolCallInfo } from "../hooks/useChat";
|
||||
import { useViewportMode } from "./Header";
|
||||
@@ -30,7 +31,6 @@ import { AgentMentionPopup } from "./AgentMentionPopup";
|
||||
import { FileMentionPopup } from "./FileMentionPopup";
|
||||
import { useFileMention } from "../hooks/useFileMention";
|
||||
import { useMobileKeyboard } from "../hooks/useMobileKeyboard";
|
||||
import { ChatToolCalls } from "./ChatToolCalls";
|
||||
|
||||
export interface ChatViewProps {
|
||||
projectId?: string;
|
||||
@@ -119,8 +119,135 @@ function formatModelTag(provider?: string | null, modelId?: string | null): stri
|
||||
return formatted.length > 30 ? formatted.slice(0, 30) + "…" : formatted;
|
||||
}
|
||||
|
||||
function truncateToolValue(value: string, maxLength: number): string {
|
||||
if (value.length <= maxLength) return value;
|
||||
return `${value.slice(0, maxLength)}…`;
|
||||
}
|
||||
|
||||
function formatToolArgsSummary(args?: Record<string, unknown>): 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);
|
||||
}
|
||||
}
|
||||
|
||||
function renderToolCalls(toolCalls?: ToolCallInfo[]): ReactNode {
|
||||
return <ChatToolCalls toolCalls={toolCalls} />;
|
||||
if (!toolCalls || toolCalls.length === 0) return null;
|
||||
|
||||
const renderToolCallItem = (toolCall: ToolCallInfo, index: number) => {
|
||||
const isRunning = toolCall.status === "running";
|
||||
const isError = toolCall.status === "completed" && toolCall.isError;
|
||||
const argsSummary = formatToolArgsSummary(toolCall.args);
|
||||
const resultSummary = formatToolResultSummary(toolCall.result);
|
||||
const summaryPreview = isRunning
|
||||
? argsSummary
|
||||
: resultSummary
|
||||
? `result: ${resultSummary}`
|
||||
: argsSummary
|
||||
? `args: ${argsSummary}`
|
||||
: null;
|
||||
const statusLabel = isRunning ? "running" : isError ? "error" : "completed";
|
||||
|
||||
return (
|
||||
<details
|
||||
key={`${toolCall.toolName}-${index}`}
|
||||
className={`chat-tool-call${isRunning ? " chat-tool-call--running" : ""}${isError ? " chat-tool-call--error" : ""}`}
|
||||
open={isRunning}
|
||||
>
|
||||
<summary>
|
||||
<span className="chat-tool-call-status-dot" aria-hidden="true" />
|
||||
<span className="chat-tool-call-name">{toolCall.toolName}</span>
|
||||
{summaryPreview && (
|
||||
<span className="chat-tool-call-preview" title={summaryPreview}>
|
||||
{summaryPreview}
|
||||
</span>
|
||||
)}
|
||||
<span className="chat-tool-call-status-text">{statusLabel}</span>
|
||||
</summary>
|
||||
<div className="chat-tool-call-content">
|
||||
{argsSummary && (
|
||||
<div className="chat-tool-call-row">
|
||||
<span className="chat-tool-call-label">args</span>
|
||||
<span className="chat-tool-call-value">{argsSummary}</span>
|
||||
</div>
|
||||
)}
|
||||
{resultSummary && (
|
||||
<div className={`chat-tool-call-row${isError ? " chat-tool-call-row--error" : ""}`}>
|
||||
<span className="chat-tool-call-label">result</span>
|
||||
<span className="chat-tool-call-value">{resultSummary}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</details>
|
||||
);
|
||||
};
|
||||
|
||||
const className = "chat-tool-calls";
|
||||
if (toolCalls.length === 1) {
|
||||
return (
|
||||
<div className={className} data-testid="chat-tool-calls">
|
||||
<div className="chat-tool-calls-header">
|
||||
<Wrench size={12} aria-hidden="true" />
|
||||
<span>Tool calls</span>
|
||||
</div>
|
||||
{renderToolCallItem(toolCalls[0], 0)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const runningCount = toolCalls.filter((toolCall) => toolCall.status === "running").length;
|
||||
const errorCount = toolCalls.filter((toolCall) => toolCall.status === "completed" && toolCall.isError).length;
|
||||
const hasRunning = runningCount > 0;
|
||||
const uniqueNames = Array.from(new Set(toolCalls.map((toolCall) => toolCall.toolName)));
|
||||
const visibleNames = uniqueNames.slice(0, 5);
|
||||
const overflowCount = Math.max(0, uniqueNames.length - visibleNames.length);
|
||||
const namesSummary = overflowCount > 0
|
||||
? `${visibleNames.join(", ")}, +${overflowCount} more`
|
||||
: visibleNames.join(", ");
|
||||
const statusSummary = hasRunning
|
||||
? `(${runningCount} running)`
|
||||
: errorCount > 0
|
||||
? `(${errorCount} ${errorCount === 1 ? "error" : "errors"})`
|
||||
: null;
|
||||
|
||||
return (
|
||||
<div className={className} data-testid="chat-tool-calls">
|
||||
<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-names" title={namesSummary}>{namesSummary}</span>
|
||||
{statusSummary && <span className="chat-tool-calls-group-status">{statusSummary}</span>}
|
||||
</summary>
|
||||
{toolCalls.map((toolCall, index) => renderToolCallItem(toolCall, index))}
|
||||
</details>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const chatMarkdownComponents: Components = {
|
||||
|
||||
Reference in New Issue
Block a user