Skip to content

Commit aa7ac5c

Browse files
committed
fix (chat): tool code rendering (WIP)
1 parent 068d892 commit aa7ac5c

File tree

1 file changed

+40
-47
lines changed

1 file changed

+40
-47
lines changed

src/client/components/ChatBubble.js

Lines changed: 40 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -236,70 +236,64 @@ const ChatBubble = ({
236236
components={{
237237
a: ({ href, children }) => (
238238
<LinkButton href={href} children={children} />
239-
),
240-
// Ensure code blocks are rendered with a plain background
241-
pre: ({ node, ...props }) => (
242-
<pre className="bg-transparent p-0" {...props} />
243-
),
244-
code: ({ node, ...props }) => (
245-
<code className="text-white" {...props} />
246239
)
247240
}}
248241
/>
249242
)
250243
}
251244

252245
const contentParts = []
253-
// Regex to capture all custom tags: <think>, <tool_code>, and <tool_result>
254246
const regex =
255247
/(<think>[\s\S]*?<\/think>|<tool_code name="[^"]+">[\s\S]*?<\/tool_code>|<tool_result tool_name="[^"]+">[\s\S]*?<\/tool_result>)/g
256248
let lastIndex = 0
257249

250+
// --- START OF THE ROBUST FIX ---
251+
252+
// Helper function to check for and filter out junk tokens
253+
const isJunk = (text) => {
254+
const trimmed = text.trim()
255+
if (trimmed === "") return true // Ignore whitespace-only strings
256+
257+
// This regex identifies common junk patterns seen in logs.
258+
// It looks for fragments of closing tags or orphaned tag-like words.
259+
const junkRegex = /^(<\/(\w+>)?|_code>|code>|ode>|>)$/
260+
return junkRegex.test(trimmed)
261+
}
262+
258263
for (const match of message.matchAll(regex)) {
259-
// Capture the text before the current tag
264+
// 1. Process the text *before* the current valid tag
260265
if (match.index > lastIndex) {
261266
const textContent = message.substring(lastIndex, match.index)
262-
const isPartialTag =
263-
textContent.startsWith("<tool_") ||
264-
textContent.startsWith("<think")
265-
if (textContent.trim() && !isPartialTag) {
266-
contentParts.push({
267-
type: "text",
268-
content: textContent
269-
})
267+
// Only add the text if it's NOT junk
268+
if (!isJunk(textContent)) {
269+
contentParts.push({ type: "text", content: textContent })
270270
}
271271
}
272272

273-
const tag = match[0] // The full tag match
273+
// 2. Process the valid, complete tag itself
274+
const tag = match[0]
274275
let subMatch
275276

276-
// Check for <tool_code>
277277
if (
278278
(subMatch = tag.match(
279279
/<tool_code name="([^"]+)">([\s\S]*?)<\/tool_code>/
280280
))
281281
) {
282282
const toolName = subMatch[1]
283-
const toolCode = subMatch[2].trim()
284-
if (toolName && toolCode) {
285-
contentParts.push({
286-
type: "tool_code",
287-
name: toolName,
288-
code: toolCode
289-
})
290-
}
291-
}
292-
// Check for <tool_result>
293-
else if (
283+
// Handle empty tool_code blocks gracefully
284+
const toolCode = subMatch[2] ? subMatch[2].trim() : "{}"
285+
contentParts.push({
286+
type: "tool_code",
287+
name: toolName,
288+
code: toolCode
289+
})
290+
} else if (
294291
(subMatch = tag.match(
295292
/<tool_result tool_name="([^"]+)">([\s\S]*?)<\/tool_result>/
296293
))
297294
) {
298-
// We parse tool_result but do not add it to contentParts, effectively hiding it.
299-
// The agent's summary should be in a subsequent "text" part.
300-
}
301-
// Check for <think>
302-
if ((subMatch = tag.match(/<think>([\s\S]*?)<\/think>/))) {
295+
// We correctly parse and then ignore/hide tool_result
296+
} else if ((subMatch = tag.match(/<think>([\s\S]*?)<\/think>/))) {
303297
const thinkContent = subMatch[1].trim()
304298
if (thinkContent) {
305299
contentParts.push({
@@ -309,23 +303,24 @@ const ChatBubble = ({
309303
}
310304
}
311305

306+
// 3. Update our position in the message string
312307
lastIndex = match.index + tag.length
313308
}
314309

315-
// Capture any remaining text after the last tag
310+
// 4. Process any remaining text after the last valid tag
316311
if (lastIndex < message.length) {
317-
const textContent = message.substring(lastIndex)
318-
const isPartialTag =
319-
textContent.startsWith("<tool_") ||
320-
textContent.startsWith("<think")
321-
if (textContent.trim() && !isPartialTag) {
322-
contentParts.push({
323-
type: "text", // The final part of the message
324-
content: textContent
325-
})
312+
const remainingText = message.substring(lastIndex)
313+
// Also check the final part for junk or incomplete streaming tags
314+
const openBrackets = (message.match(/</g) || []).length
315+
const closeBrackets = (message.match(/>/g) || []).length
316+
317+
if (!isJunk(remainingText) && openBrackets <= closeBrackets) {
318+
contentParts.push({ type: "text", content: remainingText })
326319
}
327320
}
321+
// --- END OF THE ROBUST FIX ---
328322

323+
// The rest of the rendering logic remains the same
329324
return contentParts.map((part, index) => {
330325
const partId = `${part.type}_${index}`
331326
if (part.type === "think" && part.content) {
@@ -368,14 +363,12 @@ const ChatBubble = ({
368363
/>
369364
)
370365
}
371-
372366
if (part.type === "text" && part.content.trim()) {
373367
return (
374368
<ReactMarkdown
375369
key={partId}
376370
className="prose prose-invert"
377371
remarkPlugins={[remarkGfm]}
378-
// The agent's summary and textual response
379372
children={part.content}
380373
components={{
381374
a: ({ href, children }) => (

0 commit comments

Comments
 (0)