From 8d492d7d4b0ccebd7affb69cf98aed4c46d1b47d Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sun, 29 Mar 2026 13:37:35 +0300 Subject: [PATCH] feat(llm): show tool calls as references --- .../src/translations/en/translation.json | 5 +- .../type_widgets/llm_chat/ChatMessage.tsx | 45 ++++++++++ .../widgets/type_widgets/llm_chat/LlmChat.css | 83 +++++++++++++++++++ .../widgets/type_widgets/llm_chat/LlmChat.tsx | 30 ++++++- 4 files changed, 159 insertions(+), 4 deletions(-) diff --git a/apps/client/src/translations/en/translation.json b/apps/client/src/translations/en/translation.json index b8c2ed8811..64728311bd 100644 --- a/apps/client/src/translations/en/translation.json +++ b/apps/client/src/translations/en/translation.json @@ -1622,7 +1622,10 @@ "sources": "Sources", "extended_thinking": "Extended thinking", "thinking": "Thinking...", - "thought_process": "Thought process" + "thought_process": "Thought process", + "tool_calls": "{{count}} tool call(s)", + "input": "Input", + "result": "Result" }, "shared_switch": { "shared": "Shared", 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 113a35c422..0f700d6416 100644 --- a/apps/client/src/widgets/type_widgets/llm_chat/ChatMessage.tsx +++ b/apps/client/src/widgets/type_widgets/llm_chat/ChatMessage.tsx @@ -12,6 +12,13 @@ marked.setOptions({ type MessageType = "message" | "error" | "thinking"; +interface ToolCall { + id: string; + toolName: string; + input: Record; + result?: string; +} + interface StoredMessage { id: string; role: "user" | "assistant" | "system"; @@ -20,6 +27,8 @@ interface StoredMessage { citations?: LlmCitation[]; /** Message type for special rendering. Defaults to "message" if omitted. */ type?: MessageType; + /** Tool calls made during this response */ + toolCalls?: ToolCall[]; } interface Props { @@ -81,6 +90,42 @@ export default function ChatMessage({ message, isStreaming }: Props) { message.content )} + {message.toolCalls && message.toolCalls.length > 0 && ( +
+ + + {t("llm_chat.tool_calls", { count: message.toolCalls.length })} + +
+ {message.toolCalls.map((tool) => ( +
+
+ {tool.toolName} +
+
+ {t("llm_chat.input")}: +
{JSON.stringify(tool.input, null, 2)}
+
+ {tool.result && ( +
+ {t("llm_chat.result")}: +
{(() => {
+                                            if (typeof tool.result === "string" && (tool.result.startsWith("{") || tool.result.startsWith("["))) {
+                                                try {
+                                                    return JSON.stringify(JSON.parse(tool.result), null, 2);
+                                                } catch {
+                                                    return tool.result;
+                                                }
+                                            }
+                                            return tool.result;
+                                        })()}
+
+ )} +
+ ))} +
+
+ )} {message.citations && message.citations.length > 0 && (
diff --git a/apps/client/src/widgets/type_widgets/llm_chat/LlmChat.css b/apps/client/src/widgets/type_widgets/llm_chat/LlmChat.css index 51ff8f1382..66c42c8eac 100644 --- a/apps/client/src/widgets/type_widgets/llm_chat/LlmChat.css +++ b/apps/client/src/widgets/type_widgets/llm_chat/LlmChat.css @@ -422,3 +422,86 @@ .llm-chat-markdown em { font-style: italic; } + +/* Tool calls display */ +.llm-chat-tool-calls { + margin-top: 0.75rem; + padding-top: 0.75rem; + border-top: 1px solid var(--main-border-color); +} + +.llm-chat-tool-calls-summary { + display: flex; + align-items: center; + gap: 0.5rem; + font-size: 0.85rem; + font-weight: 500; + color: var(--muted-text-color); + padding: 0.25rem 0; + cursor: pointer; + list-style: none; +} + +.llm-chat-tool-calls-summary::-webkit-details-marker { + display: none; +} + +.llm-chat-tool-calls-summary::before { + content: "▶"; + font-size: 0.7em; + transition: transform 0.2s ease; +} + +.llm-chat-tool-calls[open] .llm-chat-tool-calls-summary::before { + transform: rotate(90deg); +} + +.llm-chat-tool-calls-summary .bx { + font-size: 1rem; +} + +.llm-chat-tool-calls-list { + margin-top: 0.5rem; + display: flex; + flex-direction: column; + gap: 0.75rem; +} + +.llm-chat-tool-call { + background: var(--accented-background-color); + border-radius: 6px; + padding: 0.75rem; + font-size: 0.85rem; +} + +.llm-chat-tool-call-name { + font-weight: 600; + margin-bottom: 0.5rem; + color: var(--main-text-color); + font-family: var(--monospace-font-family, monospace); +} + +.llm-chat-tool-call-input, +.llm-chat-tool-call-result { + margin-top: 0.5rem; +} + +.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; +} + +.llm-chat-tool-call pre { + margin: 0; + padding: 0.5rem; + background: var(--main-background-color); + border-radius: 4px; + overflow-x: auto; + font-size: 0.8rem; + font-family: var(--monospace-font-family, monospace); + max-height: 200px; + overflow-y: auto; +} diff --git a/apps/client/src/widgets/type_widgets/llm_chat/LlmChat.tsx b/apps/client/src/widgets/type_widgets/llm_chat/LlmChat.tsx index 7ecf2eab09..6f4ed84d71 100644 --- a/apps/client/src/widgets/type_widgets/llm_chat/LlmChat.tsx +++ b/apps/client/src/widgets/type_widgets/llm_chat/LlmChat.tsx @@ -10,6 +10,13 @@ import "./LlmChat.css"; type MessageType = "message" | "error" | "thinking"; +interface ToolCall { + id: string; + toolName: string; + input: Record; + result?: string; +} + interface StoredMessage { id: string; role: "user" | "assistant" | "system"; @@ -18,6 +25,8 @@ interface StoredMessage { citations?: LlmCitation[]; /** Message type for special rendering. Defaults to "message" if omitted. */ type?: MessageType; + /** Tool calls made during this response */ + toolCalls?: ToolCall[]; } interface LlmChatContent { @@ -137,6 +146,7 @@ export default function LlmChat({ note, ntxId, noteContext }: TypeWidgetProps) { let assistantContent = ""; let thinkingContent = ""; const citations: LlmCitation[] = []; + const toolCalls: ToolCall[] = []; const apiMessages: LlmMessage[] = newMessages.map(m => ({ role: m.role, @@ -157,11 +167,24 @@ export default function LlmChat({ note, ntxId, noteContext }: TypeWidgetProps) { setStreamingThinking(thinkingContent); setToolActivity(t("llm_chat.thinking")); }, - onToolUse: (toolName, _input) => { + onToolUse: (toolName, toolInput) => { const toolLabel = toolName === "web_search" ? t("llm_chat.searching_web") : `Using ${toolName}...`; setToolActivity(toolLabel); + // Track the tool call + toolCalls.push({ + id: randomString(), + toolName, + input: toolInput + }); + }, + onToolResult: (toolName, result) => { + // Find the most recent tool call with this name and add the result + const toolCall = [...toolCalls].reverse().find(tc => tc.toolName === toolName && !tc.result); + if (toolCall) { + toolCall.result = result; + } }, onCitation: (citation) => { citations.push(citation); @@ -198,13 +221,14 @@ export default function LlmChat({ note, ntxId, noteContext }: TypeWidgetProps) { }); } - if (assistantContent) { + if (assistantContent || toolCalls.length > 0) { newMessages.push({ id: randomString(), role: "assistant", content: assistantContent, createdAt: new Date().toISOString(), - citations: citations.length > 0 ? citations : undefined + citations: citations.length > 0 ? citations : undefined, + toolCalls: toolCalls.length > 0 ? toolCalls : undefined }); }