Skip to content

feat: add support for rendering tool calls#30

Open
nikochiko wants to merge 2 commits intomasterfrom
function-rendering
Open

feat: add support for rendering tool calls#30
nikochiko wants to merge 2 commits intomasterfrom
function-rendering

Conversation

@nikochiko
Copy link
Member

Legal Boilerplate

Look, I get it. The entity doing business as “Gooey.AI” and/or “Dara.network” was incorporated in the State of Delaware in 2020 as Dara Network Inc. and is gonna need some rights from me in order to utilize my contributions in this PR. So here's the deal: I retain all rights, title and interest in and to my contributions, and by keeping this boilerplate intact I confirm that Dara Network Inc can use, modify, copy, and redistribute my contributions, under its choice of terms.

@coderabbitai
Copy link

coderabbitai bot commented Feb 2, 2026

📝 Walkthrough

Walkthrough

Adds a ToolCall interface and optional tool_calls array to MessagePart in the messages context. Introduces a new ToolCalls React component (default export) that renders a vertical, collapsible list of tool invocation records with icons, argument summary extraction, JSON-formatted inputs, and return value display. The streaming handler for MESSAGE_PART now preserves tool_calls when updating messages. Incoming message rendering is updated to conditionally render ToolCalls when tool_calls data is present.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat: add support for rendering tool calls' directly and clearly describes the main change across all files.
Description check ✅ Passed The pull request description contains the required legal boilerplate section in full, matching the template exactly.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch function-rendering

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@src/components/shared/ToolCalls.tsx`:
- Line 146: Wrap the JSON.stringify calls that render inputs and return_value in
a try/catch to avoid runtime crashes on circular structures: in the JSX where
{JSON.stringify(inputs, null, 2)} is used (and likewise where return_value is
stringified around lines referencing return_value), attempt JSON.stringify and
on error render a safe fallback such as a descriptive string ("[Circular]" or
util.inspect/another safe serializer) or the object's toString(); update the
rendering logic in the ToolCalls component to catch stringify errors and display
the fallback instead of letting the exception propagate.
🧹 Nitpick comments (3)
src/contexts/MessagesContext.tsx (1)

66-74: Consider making url optional.

The url field is currently required, but not all tool calls may have an associated URL. If the backend can send tool calls without URLs, this could cause type mismatches.

💡 Suggested change
 export interface ToolCall {
   id: string | number;
   title: string;
-  url: string;
+  url?: string;
   inputs: Record<string, any>;
   return_value: any;
   is_running: boolean;
   icon?: string;
 }
src/components/shared/ToolCalls.tsx (2)

52-56: Inline <style> tag renders per card instance.

This <style> block will be duplicated for each ToolCallCard rendered, which is inefficient. Consider using the addInlineStyle pattern used elsewhere in the codebase (see IncomingMsg.tsx line 19), or move this to a shared CSS file.

💡 Suggested approach

Create a separate style file or add to an existing one:

/* toolcalls.scss */
.tool-call-summary::after {
  display: none !important;
}

Then import it once at component level using addInlineStyle:

 import React, { useState } from "react";
 import clsx from "clsx";
 import { ToolCall } from "src/contexts/MessagesContext";
+import { addInlineStyle } from "src/addStyles";
+import style from "./toolcalls.scss?inline";
+addInlineStyle(style);

And remove the inline <style> tag from the component.


67-82: Icon URL check may miss data: URLs.

The check icon.startsWith('http') || icon.startsWith('/') handles absolute and relative URLs but would treat data: URLs (base64 images) as text/emoji icons.

💡 Suggested fix
-{icon && (
-  icon.startsWith('http') || icon.startsWith('/') ? (
+{icon && (
+  icon.startsWith('http') || icon.startsWith('/') || icon.startsWith('data:') ? (
     <img

lineHeight: "1.4",
}}
>
{JSON.stringify(inputs, null, 2)}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

JSON.stringify can throw on circular references.

If inputs contains circular references, JSON.stringify will throw an error. Consider wrapping in a try/catch with a fallback.

🛡️ Proposed defensive fix
+const safeStringify = (obj: any): string => {
+  try {
+    return JSON.stringify(obj, null, 2);
+  } catch {
+    return "[Unable to display]";
+  }
+};
+
 // Then use in the component:
-{JSON.stringify(inputs, null, 2)}
+{safeStringify(inputs)}

Apply the same pattern for return_value on lines 168-170.

🤖 Prompt for AI Agents
In `@src/components/shared/ToolCalls.tsx` at line 146, Wrap the JSON.stringify
calls that render inputs and return_value in a try/catch to avoid runtime
crashes on circular structures: in the JSX where {JSON.stringify(inputs, null,
2)} is used (and likewise where return_value is stringified around lines
referencing return_value), attempt JSON.stringify and on error render a safe
fallback such as a descriptive string ("[Circular]" or util.inspect/another safe
serializer) or the object's toString(); update the rendering logic in the
ToolCalls component to catch stringify errors and display the fallback instead
of letting the exception propagate.

</div>
)}

{!is_running && return_value && (
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correctness: The conditional !is_running && return_value on line 156 incorrectly hides valid falsy return values such as 0, false, or an empty string. This prevents legitimate tool outputs from being displayed in the UI. Use an explicit check (e.g., return_value !== undefined) to ensure all completed results are rendered.

🤖 AI Agent Prompt for Cursor/Windsurf

📋 Copy this prompt to your AI coding assistant (Cursor, Windsurf, etc.) to get help fixing this issue

File: src/components/shared/ToolCalls.tsx. Update the conditional rendering of the return value so it doesn't filter out valid falsy values. Replace `!is_running && return_value` with a null/undefined check (e.g., `return_value != null`) while keeping the rest of the JSX intact.
✨ Committable Code Suggestion

💡 This is a one-click fix! Click "Commit suggestion" to apply this change directly to your branch.

Suggested change
{!is_running && return_value && (
import React, { useState } from "react";
import clsx from "clsx";
import { ToolCall } from "src/contexts/MessagesContext";
interface ToolCallsProps {
toolCalls: ToolCall[];
className?: string;
}
const extractArgumentSummary = (inputs: Record<string, any>): string | null => {
const findFirstString = (obj: any, maxLength = 50): string | null => {
if (typeof obj === "string") {
return obj.length > maxLength ? obj.substring(0, maxLength) + "..." : obj;
}
if (Array.isArray(obj)) {
for (const item of obj) {
const result = findFirstString(item, maxLength);
if (result) return result;
}
}
if (obj && typeof obj === "object") {
for (const value of Object.values(obj)) {
const result = findFirstString(value, maxLength);
if (result) return result;
}
}
return null;
};
if (!inputs || Object.keys(inputs).length === 0) {
return null;
}
return findFirstString(inputs);
};
const ToolCallCard: React.FC<{ toolCall: ToolCall }> = ({ toolCall }) => {
const { title, inputs, return_value, is_running, icon } = toolCall;
const [isExpanded, setIsExpanded] = useState(false);
const argumentSummary = extractArgumentSummary(inputs || {});
return (
<details
open={isExpanded}
onToggle={(e) => setIsExpanded((e.target as HTMLDetailsElement).open)}
style={{
backgroundColor: "transparent",
border: "1px solid #e9ecef",
}}
>
<style>{`
.tool-call-summary::after {
display: none !important;
}
`}</style>
<summary
className="tool-call-summary"
style={{
backgroundColor: "transparent",
display: "flex",
alignItems: "center",
gap: "6px",
listStyle: "none",
}}
>
{icon && (
icon.startsWith('http') || icon.startsWith('/') ? (
<img
src={icon}
alt=""
style={{
width: "16px",
height: "16px",
objectFit: "contain",
flexShrink: 0,
}}
/>
) : (
<span style={{ fontSize: "16px", flexShrink: 0 }}>{icon}</span>
)
)}
<div
style={{
overflow: "hidden",
textOverflow: "ellipsis",
whiteSpace: "nowrap",
minWidth: 0,
flex: 1,
}}
>
<span className="font_14_600">{title}</span>
{argumentSummary && (
<>
<span className="font_14_600"> - </span>
<span className="font_14_400 text-muted">{argumentSummary}</span>
</>
)}
{is_running && (
<span className="font_12_400 text-muted"> Running...</span>
)}
</div>
<div
className={clsx(
"tool-status-indicator rounded-circle",
is_running ? "bg-primary" : "bg-success"
)}
style={{
width: "8px",
height: "8px",
animation: is_running ? "pulse 2s infinite" : "none",
flexShrink: 0,
}}
/>
<div
style={{
fontSize: "10px",
color: "#6c757d",
flexShrink: 0,
transform: isExpanded ? "rotate(180deg)" : "rotate(0deg)",
transition: "transform 0.2s ease",
}}
>
</div>
</summary>
<div style={{ padding: "8px 0" }}>
{inputs && Object.keys(inputs).length > 0 && (
<div className="mb-3">
<div className="font_14_600 text-dark mb-2">Inputs</div>
<div
className="font_12_400"
style={{
fontFamily: "ui-monospace, SFMono-Regular, 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace",
backgroundColor: "#f8f9fa",
border: "1px solid #e9ecef",
padding: "12px",
borderRadius: "6px",
maxHeight: "200px",
overflow: "auto",
whiteSpace: "pre-wrap",
lineHeight: "1.4",
}}
>
{JSON.stringify(inputs, null, 2)}
</div>
</div>
)}
{!is_running && return_value && (
<div>
<div className="font_14_600 text-dark mb-2">Return value</div>
<div
className="font_12_400"
style={{
fontFamily: "ui-monospace, SFMono-Regular, 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace",
backgroundColor: "#f8f9fa",
border: "1px solid #e9ecef",
padding: "12px",
borderRadius: "6px",
maxHeight: "200px",
overflow: "auto",
whiteSpace: "pre-wrap",
lineHeight: "1.4",
}}
>
{typeof return_value === "string"
? return_value
: JSON.stringify(return_value, null, 2)}
</div>
</div>
)}
</div>
</details>
);
};
const ToolCalls: React.FC<ToolCallsProps> = ({ toolCalls, className }) => {
if (!toolCalls || toolCalls.length === 0) {
return null;
}
return (
<div className={clsx("tool-calls-container", className)}>
{toolCalls.map((toolCall) => (
<ToolCallCard key={String(toolCall.id)} toolCall={toolCall} />
))}
</div>
);
};
export default ToolCalls;

buttons,
tool_calls: payload.tool_calls || prevMessage?.tool_calls || [],
});
return newConversations;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correctness: The assignment to tool_calls in the MESSAGE_PART block overwrites the existing state whenever payload.tool_calls is present. This causes data loss when tool calls are delivered incrementally across multiple chunks, inconsistent with the accumulation logic used for text and buttons in the same block.

🤖 AI Agent Prompt for Cursor/Windsurf

📋 Copy this prompt to your AI coding assistant (Cursor, Windsurf, etc.) to get help fixing this issue

In `src/contexts/messages/useStreamingHandler.ts` at line 141, the new `tool_calls` assignment overwrites previous streamed tool calls. Update it to merge `prevMessage.tool_calls` with `payload.tool_calls` (similar to `buttons`) so partial chunks are preserved. Replace the line with a concatenated array of both.
✨ Committable Code Suggestion

💡 This is a one-click fix! Click "Commit suggestion" to apply this change directly to your branch.

Suggested change
return newConversations;
...payload,
id: currentStreamRef.current,
text,
buttons,
tool_calls: [
...(prevMessage?.tool_calls || []),
...(payload.tool_calls || []),
],
});
return newConversations;
}

@entelligence-ai-pr-reviews
Copy link
Contributor

entelligence-ai-pr-reviews bot commented Feb 3, 2026

✅ Review completed for commit e9149f0.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@src/components/shared/ToolCalls.tsx`:
- Line 194: The render condition currently checks {!is_running && return_value}
which incorrectly hides valid falsy outputs (0, "", false); update the condition
in the ToolCalls component to explicitly check for null/undefined instead (e.g.,
{!is_running && return_value !== null && return_value !== undefined}) so valid
falsy return_value values still render, while still suppressing truly absent
values.
🧹 Nitpick comments (1)
src/components/shared/ToolCalls.tsx (1)

55-59: Move <style> tag outside the component to avoid DOM duplication.

This <style> tag is rendered inside each ToolCallCard instance. If multiple tool calls are displayed, identical style blocks will be duplicated in the DOM.

Consider moving this to a shared CSS file, or lifting it to the parent ToolCalls component so it renders only once.

♻️ Suggested refactor: lift style to parent component
 const ToolCalls: React.FC<ToolCallsProps> = ({ toolCalls, className }) => {
   if (!toolCalls || toolCalls.length === 0) {
     return null;
   }
 
   return (
     <div className={clsx("tool-calls-container", className)}>
+      <style>{`
+        .tool-call-summary::after {
+          display: none !important;
+        }
+      `}</style>
       {toolCalls.map((toolCall) => (
         <ToolCallCard key={String(toolCall.id)} toolCall={toolCall} />
       ))}
     </div>
   );
 };

Then remove the <style> block from ToolCallCard (lines 55-59).

</div>
)}

{!is_running && return_value && (
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Falsy return_value values (0, "", false) will not render.

The condition return_value will evaluate to false for valid return values like 0, "", or false. If these are valid tool outputs, consider using an explicit check:

-{!is_running && return_value && (
+{!is_running && return_value !== undefined && return_value !== null && (

Alternatively, if the intent is to hide empty outputs, the current behavior may be acceptable.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
{!is_running && return_value && (
{!is_running && return_value !== undefined && return_value !== null && (
🤖 Prompt for AI Agents
In `@src/components/shared/ToolCalls.tsx` at line 194, The render condition
currently checks {!is_running && return_value} which incorrectly hides valid
falsy outputs (0, "", false); update the condition in the ToolCalls component to
explicitly check for null/undefined instead (e.g., {!is_running && return_value
!== null && return_value !== undefined}) so valid falsy return_value values
still render, while still suppressing truly absent values.

Comment on lines +155 to +158
display: "flex",
alignItems: "center",
fontSize: "11px",
textDecoration: "none",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correctness: The href={url} attribute is vulnerable to Cross-Site Scripting (XSS). It allows execution of arbitrary code via javascript: or data: URIs. Sanitize url to permit only http: and https: protocols.

🤖 AI Agent Prompt for Cursor/Windsurf

📋 Copy this prompt to your AI coding assistant (Cursor, Windsurf, etc.) to get help fixing this issue

File: src/components/shared/ToolCalls.tsx. Lines: 155-158. The new external link uses toolCall.url directly, which can allow javascript: or data: URLs. Guard rendering with a protocol allowlist (http/https) or validate via new URL() before rendering. Apply the diff: wrap the link with a protocol check so only http/https URLs render.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

Comments