Skip to content

Commit 04252cf

Browse files
authored
fix: Duplicate tool calls showing in assistant UI (#374)
1 parent a0dbcb6 commit 04252cf

File tree

2 files changed

+37
-12
lines changed

2 files changed

+37
-12
lines changed

src/components/runbooks/editor/ui/AIAssistant.tsx

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import React, {
44
useEffect,
55
useLayoutEffect,
66
useCallback,
7+
useMemo,
78
memo,
89
Component,
910
ReactNode,
@@ -449,6 +450,12 @@ export default function AIAssistant({
449450
cancel,
450451
} = chat;
451452

453+
// Filter out system messages for display (keep them in messages array for dev tools)
454+
const visibleMessages = useMemo(
455+
() => messages.filter((m) => m.role !== "system"),
456+
[messages],
457+
);
458+
452459
useEffect(() => {
453460
if (!sessionId) return;
454461
changeChargeTarget(sessionId, chargeTarget);
@@ -695,7 +702,7 @@ export default function AIAssistant({
695702
<Spinner size="lg" />
696703
</div>
697704
)}
698-
{!isCreatingSession && messages.length === 0 && (
705+
{!isCreatingSession && visibleMessages.length === 0 && (
699706
<div className="flex flex-col items-center justify-center h-full text-center text-gray-500 dark:text-gray-400">
700707
<BotIcon className="h-12 w-12 mb-4 opacity-50" />
701708
<p className="text-sm">Ask me to help edit your runbook.</p>
@@ -714,7 +721,7 @@ export default function AIAssistant({
714721
</div>
715722
</div>
716723
)}
717-
{messages.map((message, idx) => (
724+
{visibleMessages.map((message, idx) => (
718725
<MessageBubble
719726
key={idx}
720727
message={message}
@@ -755,7 +762,8 @@ export default function AIAssistant({
755762
</ScrollShadow>
756763

757764
{/* Scroll to bottom button */}
758-
{!lockedToBottom && (messages.length > 0 || streamingContent !== null) && (
765+
{!lockedToBottom &&
766+
(visibleMessages.length > 0 || streamingContent !== null) && (
759767
<Button
760768
isIconOnly
761769
size="sm"
@@ -837,7 +845,7 @@ export default function AIAssistant({
837845
<span className="text-xs text-gray-400">
838846
<kbd className="px-1 py-0.5 bg-gray-100 dark:bg-gray-800 rounded">Enter</kbd> to send
839847
</span>
840-
{messages.length > 0 && (
848+
{visibleMessages.length > 0 && (
841849
<Button
842850
size="sm"
843851
variant="light"

src/lib/ai/useAIChat.ts

Lines changed: 25 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -96,16 +96,33 @@ export default function useAIChat(sessionId: string): AIChatAPI {
9696

9797
case "toolsRequested":
9898
setPendingToolCalls(event.calls);
99-
// Add assistant message with tool calls
99+
// Add assistant message with tool calls (only if not already in messages from history)
100100
setStreamingContent((content) => {
101-
const parts: AIMessage["content"]["parts"] = [];
102-
if (content) {
103-
parts.push({ type: "text", data: content });
104-
}
105-
event.calls.forEach((call) => {
106-
parts.push({ type: "toolCall", data: call });
101+
setMessages((prev) => {
102+
// Check if these tool calls already exist in messages (e.g., from history restore)
103+
const existingToolCallIds = new Set(
104+
prev.flatMap((msg) =>
105+
msg.content.parts
106+
.filter((part): part is { type: "toolCall"; data: AIToolCall } => part.type === "toolCall")
107+
.map((part) => part.data.id),
108+
),
109+
);
110+
const newCalls = event.calls.filter((call) => !existingToolCallIds.has(call.id));
111+
112+
// If all tool calls already exist, don't add a duplicate message
113+
if (newCalls.length === 0) {
114+
return prev;
115+
}
116+
117+
const parts: AIMessage["content"]["parts"] = [];
118+
if (content) {
119+
parts.push({ type: "text", data: content });
120+
}
121+
newCalls.forEach((call) => {
122+
parts.push({ type: "toolCall", data: call });
123+
});
124+
return [...prev, { role: "assistant", content: { parts } }];
107125
});
108-
setMessages((prev) => [...prev, { role: "assistant", content: { parts } }]);
109126
return null;
110127
});
111128
break;

0 commit comments

Comments
 (0)