Skip to content

Commit 09350a8

Browse files
committed
feat: create ToolResultRenderer for enhanced tool output
1 parent d284ac1 commit 09350a8

File tree

1 file changed

+234
-0
lines changed

1 file changed

+234
-0
lines changed
Lines changed: 234 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,234 @@
1+
import React, { memo, useMemo } from 'react';
2+
import ReactMarkdown from 'react-markdown';
3+
import { extractToolContent, sanitizeContent, truncateContent, preprocessMarkdown } from '../utils/markdownUtils';
4+
import { MarkdownCodeBlock } from './CodeBlockLazy';
5+
import TodoList from './TodoList';
6+
7+
/**
8+
* Component for rendering tool results with enhanced markdown support
9+
*/
10+
const ToolResultRenderer = memo(({
11+
toolName,
12+
toolInput,
13+
toolResult,
14+
showRawParameters = false,
15+
autoExpandTools = true,
16+
truncationLimit = 5000,
17+
className = ''
18+
}) => {
19+
// Extract meaningful content from tool result with error handling
20+
const extractedContent = useMemo(() => {
21+
try {
22+
return extractToolContent(toolName, toolInput, toolResult);
23+
} catch (error) {
24+
console.error('Error extracting tool content:', error);
25+
return {
26+
contentType: 'text',
27+
primaryContent: toolInput || '',
28+
metadata: { toolName },
29+
fallback: true
30+
};
31+
}
32+
}, [toolName, toolInput, toolResult]);
33+
34+
// Sanitize and prepare content for rendering
35+
const { primaryContent, contentType, metadata, fallback } = extractedContent;
36+
const sanitizedContent = sanitizeContent(primaryContent);
37+
const { content: displayContent, isTruncated, fullLength } = truncateContent(sanitizedContent, truncationLimit);
38+
39+
// Special handling for TodoWrite tool
40+
if (toolName === 'TodoWrite') {
41+
try {
42+
const input = JSON.parse(toolInput);
43+
if (input.todos && Array.isArray(input.todos)) {
44+
return (
45+
<details className="mt-2" open={autoExpandTools}>
46+
<summary className="text-sm text-blue-700 dark:text-blue-300 cursor-pointer hover:text-blue-800 dark:hover:text-blue-200 flex items-center gap-2">
47+
<svg className="w-4 h-4 transition-transform details-chevron" fill="none" stroke="currentColor" viewBox="0 0 24 24">
48+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
49+
</svg>
50+
Updating Todo List
51+
</summary>
52+
<div className="mt-3">
53+
<TodoList todos={input.todos} />
54+
{showRawParameters && (
55+
<RawParametersView content={toolInput} />
56+
)}
57+
</div>
58+
</details>
59+
);
60+
}
61+
} catch (e) {
62+
// Fall through to default rendering
63+
}
64+
}
65+
66+
// Special handling for exit_plan_mode with enhanced markdown
67+
if (toolName === 'exit_plan_mode' || toolName === 'ExitPlanMode') {
68+
return (
69+
<details className="mt-2" open={autoExpandTools}>
70+
<summary className="text-sm text-blue-700 dark:text-blue-300 cursor-pointer hover:text-blue-800 dark:hover:text-blue-200 flex items-center gap-2">
71+
<svg className="w-4 h-4 transition-transform details-chevron" fill="none" stroke="currentColor" viewBox="0 0 24 24">
72+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
73+
</svg>
74+
📋 {metadata?.title || 'View implementation plan'}
75+
</summary>
76+
<div className="mt-3">
77+
<MarkdownContent content={displayContent} />
78+
{isTruncated && (
79+
<p className="text-xs text-gray-500 dark:text-gray-400 mt-2">
80+
Content truncated ({fullLength} characters total)
81+
</p>
82+
)}
83+
{showRawParameters && (
84+
<RawParametersView content={toolInput} />
85+
)}
86+
</div>
87+
</details>
88+
);
89+
}
90+
91+
// Result tool with enhanced display
92+
if (toolName === 'Result' && !fallback) {
93+
return (
94+
<div className={`mt-2 ${className}`}>
95+
<div className="flex items-center gap-2 mb-2 text-green-700 dark:text-green-300">
96+
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
97+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
98+
</svg>
99+
<span className="font-medium">Result</span>
100+
</div>
101+
<div className="ml-6">
102+
<MarkdownContent content={displayContent} contentType={contentType} />
103+
{isTruncated && (
104+
<p className="text-xs text-gray-500 dark:text-gray-400 mt-2">
105+
Content truncated ({fullLength} characters total)
106+
</p>
107+
)}
108+
</div>
109+
</div>
110+
);
111+
}
112+
113+
// Generic tool result rendering
114+
if (!fallback && primaryContent && primaryContent !== toolInput) {
115+
return (
116+
<details className="mt-2" open={autoExpandTools}>
117+
<summary className="text-sm text-blue-700 dark:text-blue-300 cursor-pointer hover:text-blue-800 dark:hover:text-blue-200 flex items-center gap-2">
118+
<svg className="w-4 h-4 transition-transform details-chevron" fill="none" stroke="currentColor" viewBox="0 0 24 24">
119+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
120+
</svg>
121+
{metadata?.title || `View ${toolName} result`}
122+
</summary>
123+
<div className="mt-3">
124+
<MarkdownContent content={displayContent} contentType={contentType} metadata={metadata} />
125+
{isTruncated && (
126+
<p className="text-xs text-gray-500 dark:text-gray-400 mt-2">
127+
Content truncated ({fullLength} characters total)
128+
</p>
129+
)}
130+
{showRawParameters && (
131+
<RawParametersView content={toolInput} />
132+
)}
133+
</div>
134+
</details>
135+
);
136+
}
137+
138+
// Fallback to raw parameters display
139+
return showRawParameters ? (
140+
<details className="mt-2" open={autoExpandTools}>
141+
<summary className="text-sm text-gray-600 dark:text-gray-400 cursor-pointer hover:text-gray-700 dark:hover:text-gray-300">
142+
View {toolName} parameters
143+
</summary>
144+
<RawParametersView content={toolInput} />
145+
</details>
146+
) : null;
147+
});
148+
149+
/**
150+
* Component for rendering markdown content with enhanced features
151+
*/
152+
const MarkdownContent = memo(({ content, contentType = 'markdown', metadata = {} }) => {
153+
// Custom components for ReactMarkdown
154+
const components = useMemo(() => ({
155+
code: MarkdownCodeBlock,
156+
pre: ({ children }) => <>{children}</>, // Remove default pre wrapper
157+
table: ({ children }) => (
158+
<div className="overflow-x-auto my-4">
159+
<table className="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
160+
{children}
161+
</table>
162+
</div>
163+
),
164+
thead: ({ children }) => (
165+
<thead className="bg-gray-50 dark:bg-gray-800">{children}</thead>
166+
),
167+
th: ({ children }) => (
168+
<th className="px-3 py-2 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
169+
{children}
170+
</th>
171+
),
172+
td: ({ children }) => (
173+
<td className="px-3 py-2 whitespace-nowrap text-sm text-gray-900 dark:text-gray-100">
174+
{children}
175+
</td>
176+
),
177+
a: ({ href, children }) => (
178+
<a
179+
href={href}
180+
target="_blank"
181+
rel="noopener noreferrer"
182+
className="text-blue-600 dark:text-blue-400 hover:underline"
183+
>
184+
{children}
185+
</a>
186+
),
187+
}), []);
188+
189+
if (contentType === 'json') {
190+
return (
191+
<pre className="text-xs bg-gray-100 dark:bg-gray-800 p-3 rounded overflow-x-auto">
192+
<code className="text-gray-900 dark:text-gray-100">
193+
{content}
194+
</code>
195+
</pre>
196+
);
197+
}
198+
199+
if (contentType === 'text' && metadata.language) {
200+
return (
201+
<MarkdownCodeBlock language={metadata.language}>
202+
{content}
203+
</MarkdownCodeBlock>
204+
);
205+
}
206+
207+
return (
208+
<div className="prose prose-sm max-w-none dark:prose-invert">
209+
<ReactMarkdown components={components}>
210+
{preprocessMarkdown(content)}
211+
</ReactMarkdown>
212+
</div>
213+
);
214+
});
215+
216+
/**
217+
* Component for displaying raw parameters
218+
*/
219+
const RawParametersView = memo(({ content }) => (
220+
<details className="mt-3">
221+
<summary className="text-xs text-blue-600 dark:text-blue-400 cursor-pointer hover:text-blue-700 dark:hover:text-blue-300">
222+
View raw parameters
223+
</summary>
224+
<pre className="mt-2 text-xs bg-blue-100 dark:bg-blue-800/30 p-2 rounded whitespace-pre-wrap break-words overflow-hidden text-blue-900 dark:text-blue-100">
225+
{content}
226+
</pre>
227+
</details>
228+
));
229+
230+
ToolResultRenderer.displayName = 'ToolResultRenderer';
231+
MarkdownContent.displayName = 'MarkdownContent';
232+
RawParametersView.displayName = 'RawParametersView';
233+
234+
export default ToolResultRenderer;

0 commit comments

Comments
 (0)