diff --git a/apps/client/src/widgets/type_widgets/llm_chat/ChatMessage.tsx b/apps/client/src/widgets/type_widgets/llm_chat/ChatMessage.tsx
index c5213f6ebc..7180b026fe 100644
--- a/apps/client/src/widgets/type_widgets/llm_chat/ChatMessage.tsx
+++ b/apps/client/src/widgets/type_widgets/llm_chat/ChatMessage.tsx
@@ -2,10 +2,11 @@ import "./ChatMessage.css";
import { Marked } from "marked";
import { useMemo } from "preact/hooks";
+
import { t } from "../../../services/i18n.js";
import utils from "../../../services/utils.js";
import { SanitizedHtml } from "../../react/RawHtml.js";
-import { type ContentBlock, getMessageText, type StoredMessage } from "./llm_chat_types.js";
+import { type ContentBlock, getMessageText, type StoredMessage, type TextBlock, type ToolCallBlock } from "./llm_chat_types.js";
import ToolCallCard from "./ToolCallCard.js";
function shortenNumber(n: number): string {
@@ -30,23 +31,9 @@ interface Props {
isStreaming?: boolean;
}
-function renderContentBlocks(blocks: ContentBlock[], isStreaming?: boolean) {
- return blocks.map((block, idx) => {
- if (block.type === "text") {
- const html = renderMarkdown(block.content);
- return (
-
-
- {isStreaming && idx === blocks.length - 1 && }
-
- );
- }
- if (block.type === "tool_call") {
- return ;
- }
- return null;
- });
-}
+type ContentGroup =
+ | { type: "text"; block: TextBlock; index: number }
+ | { type: "tool_calls"; blocks: ToolCallBlock[]; index: number };
export default function ChatMessage({ message, isStreaming }: Props) {
const roleLabel = message.role === "user" ? t("llm_chat.role_user") : t("llm_chat.role_assistant");
@@ -189,3 +176,40 @@ export default function ChatMessage({ message, isStreaming }: Props) {
);
}
+
+/** Group content blocks so that consecutive tool_calls are merged into one entry. */
+function groupContentBlocks(blocks: ContentBlock[]): ContentGroup[] {
+ const groups: ContentGroup[] = [];
+
+ for (let i = 0; i < blocks.length; i++) {
+ const block = blocks[i];
+ if (block.type === "tool_call") {
+ const last = groups[groups.length - 1];
+ if (last?.type === "tool_calls") {
+ last.blocks.push(block);
+ } else {
+ groups.push({ type: "tool_calls", blocks: [block], index: i });
+ }
+ } else {
+ groups.push({ type: "text", block, index: i });
+ }
+ }
+
+ return groups;
+}
+
+function renderContentBlocks(blocks: ContentBlock[], isStreaming?: boolean) {
+ return groupContentBlocks(blocks).map((group) => {
+ if (group.type === "text") {
+ const html = renderMarkdown(group.block.content);
+ return (
+
+
+ {isStreaming && group.index === blocks.length - 1 && }
+
+ );
+ }
+
+ return b.toolCall)} />;
+ });
+}
diff --git a/apps/client/src/widgets/type_widgets/llm_chat/ToolCallCard.css b/apps/client/src/widgets/type_widgets/llm_chat/ToolCallCard.css
index a07a5526f3..4d83d61103 100644
--- a/apps/client/src/widgets/type_widgets/llm_chat/ToolCallCard.css
+++ b/apps/client/src/widgets/type_widgets/llm_chat/ToolCallCard.css
@@ -1,21 +1,17 @@
-.llm-chat-tool-call-input strong,
-.llm-chat-tool-call-result strong {
- display: block;
- font-size: 0.75rem;
- color: var(--muted-text-color);
- margin-bottom: 0.25rem;
- text-transform: uppercase;
-}
-
-/* Inline tool call cards */
-.llm-chat-tool-call-inline {
+/* Tool call card — groups sequential tool calls */
+.llm-chat-tool-call-card {
margin: 0.5rem 0;
border: 1px solid var(--main-border-color);
border-radius: 8px;
font-size: 0.85rem;
}
-.llm-chat-tool-call-inline-summary {
+/* Tool call section — individual tool call within a card */
+.llm-chat-tool-call-section + .llm-chat-tool-call-section {
+ border-top: 1px solid var(--main-border-color);
+}
+
+.llm-chat-tool-call-section-summary {
display: flex;
flex-wrap: wrap;
align-items: baseline;
@@ -27,20 +23,20 @@
color: var(--muted-text-color);
}
-.llm-chat-tool-call-inline-summary::-webkit-details-marker {
+.llm-chat-tool-call-section-summary::-webkit-details-marker {
display: none;
}
-.llm-chat-tool-call-inline-summary .llm-chat-tool-call-chevron {
+.llm-chat-tool-call-section-summary .llm-chat-tool-call-chevron {
margin-left: auto;
transition: transform 0.2s ease;
}
-.llm-chat-tool-call-inline[open] .llm-chat-tool-call-chevron {
+.llm-chat-tool-call-section[open] .llm-chat-tool-call-chevron {
transform: rotate(180deg);
}
-.llm-chat-tool-call-inline-summary > .bx {
+.llm-chat-tool-call-section-summary > .bx {
font-size: 1rem;
margin-right: 0.15rem;
}
@@ -58,22 +54,23 @@
color: var(--muted-text-color);
}
-.llm-chat-tool-call-inline-body {
+/* Section body (input + result) */
+.llm-chat-tool-call-section-body {
padding: 0;
}
-.llm-chat-tool-call-inline-body .llm-chat-tool-call-input,
-.llm-chat-tool-call-inline-body .llm-chat-tool-call-result {
+.llm-chat-tool-call-section-body .llm-chat-tool-call-input,
+.llm-chat-tool-call-section-body .llm-chat-tool-call-result {
padding: 0.5rem 0.75rem;
max-height: 300px;
overflow: auto;
}
-.llm-chat-tool-call-inline-body .llm-chat-tool-call-result {
+.llm-chat-tool-call-section-body .llm-chat-tool-call-result {
border-top: 1px solid var(--main-border-color);
}
-.llm-chat-tool-call-inline-body pre {
+.llm-chat-tool-call-section-body pre {
margin: 0;
padding: 0.5rem;
background: var(--main-background-color);
@@ -82,11 +79,13 @@
font-family: var(--monospace-font-family, monospace);
}
-.llm-chat-tool-call-inline-body strong {
+.llm-chat-tool-call-input strong,
+.llm-chat-tool-call-result strong {
display: block;
font-size: 0.75rem;
color: var(--muted-text-color);
margin-bottom: 0.25rem;
+ text-transform: uppercase;
}
/* Tool call key-value table */
@@ -146,11 +145,7 @@
}
/* Tool call error styling */
-.llm-chat-tool-call-error {
- border-color: var(--danger-color, #dc3545);
-}
-
-.llm-chat-tool-call-error .llm-chat-tool-call-inline-summary {
+.llm-chat-tool-call-error .llm-chat-tool-call-section-summary {
color: var(--danger-color, #dc3545);
}
diff --git a/apps/client/src/widgets/type_widgets/llm_chat/ToolCallCard.tsx b/apps/client/src/widgets/type_widgets/llm_chat/ToolCallCard.tsx
index a0a46aa78f..9e6480b09f 100644
--- a/apps/client/src/widgets/type_widgets/llm_chat/ToolCallCard.tsx
+++ b/apps/client/src/widgets/type_widgets/llm_chat/ToolCallCard.tsx
@@ -137,16 +137,14 @@ function KeyValueTable({ data, className, depth = 0 }: { data: unknown; classNam
);
}
-export default function ToolCallCard({ toolCall }: { toolCall: ToolCall }) {
- const classes = [
- "llm-chat-tool-call-inline",
- toolCall.isError && "llm-chat-tool-call-error"
- ].filter(Boolean).join(" ");
+/** A single tool call section within a ToolCallCard. */
+function ToolCallSection({ toolCall }: { toolCall: ToolCall }) {
const { noteId: refNoteId, parentNoteId: refParentId, detailText } = getToolCallContext(toolCall);
+ const hasError = toolCall.isError;
return (
-
-
+
+
{t(`llm.tools.${toolCall.toolName}`, { defaultValue: toolCall.toolName })}
{detailText && (
@@ -167,17 +165,17 @@ export default function ToolCallCard({ toolCall }: { toolCall: ToolCall }) {
)}
)}
- {toolCall.isError && {t("llm_chat.tool_error")}}
+ {hasError && {t("llm_chat.tool_error")}}
-
+
{t("llm_chat.input")}
{toolCall.result && (
-
-
{toolCall.isError ? t("llm_chat.error") : t("llm_chat.result")}
+
+ {hasError ? t("llm_chat.error") : t("llm_chat.result")}
)}
@@ -185,3 +183,14 @@ export default function ToolCallCard({ toolCall }: { toolCall: ToolCall }) {
);
}
+
+/** A card that groups one or more sequential tool calls together. */
+export default function ToolCallCard({ toolCalls }: { toolCalls: ToolCall[] }) {
+ return (
+
+ {toolCalls.map((tc, idx) => (
+
+ ))}
+
+ );
+}