feat(llm): show tool calls as references

This commit is contained in:
Elian Doran
2026-03-29 13:37:35 +03:00
parent 246c561b64
commit 8d492d7d4b
4 changed files with 159 additions and 4 deletions

View File

@@ -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",

View File

@@ -12,6 +12,13 @@ marked.setOptions({
type MessageType = "message" | "error" | "thinking";
interface ToolCall {
id: string;
toolName: string;
input: Record<string, unknown>;
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
)}
</div>
{message.toolCalls && message.toolCalls.length > 0 && (
<details className="llm-chat-tool-calls">
<summary className="llm-chat-tool-calls-summary">
<span className="bx bx-wrench" />
{t("llm_chat.tool_calls", { count: message.toolCalls.length })}
</summary>
<div className="llm-chat-tool-calls-list">
{message.toolCalls.map((tool) => (
<div key={tool.id} className="llm-chat-tool-call">
<div className="llm-chat-tool-call-name">
{tool.toolName}
</div>
<div className="llm-chat-tool-call-input">
<strong>{t("llm_chat.input")}:</strong>
<pre>{JSON.stringify(tool.input, null, 2)}</pre>
</div>
{tool.result && (
<div className="llm-chat-tool-call-result">
<strong>{t("llm_chat.result")}:</strong>
<pre>{(() => {
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;
})()}</pre>
</div>
)}
</div>
))}
</div>
</details>
)}
{message.citations && message.citations.length > 0 && (
<div className="llm-chat-citations">
<div className="llm-chat-citations-label">

View File

@@ -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;
}

View File

@@ -10,6 +10,13 @@ import "./LlmChat.css";
type MessageType = "message" | "error" | "thinking";
interface ToolCall {
id: string;
toolName: string;
input: Record<string, unknown>;
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
});
}