diff --git a/frontend/src/lib/components/chat/CodeBlock.svelte b/frontend/src/lib/components/chat/CodeBlock.svelte index f1eda46..3a5a250 100644 --- a/frontend/src/lib/components/chat/CodeBlock.svelte +++ b/frontend/src/lib/components/chat/CodeBlock.svelte @@ -209,10 +209,10 @@ }); -
+
{language}
@@ -283,9 +283,9 @@
-
+
{#if isLoading} -
{code}
+
{code}
{:else} {@html highlightedHtml} {/if} @@ -293,15 +293,15 @@ {#if showOutput && (isExecuting || executionResult)} -
+
- + - Output + Output {#if isExecuting} @@ -313,7 +313,7 @@ {:else if executionResult} {#if executionResult.status === 'success'} - Completed + Completed {:else} @@ -322,13 +322,13 @@ Error {/if} - {executionResult.duration}ms + {executionResult.duration}ms {/if}
-
+
{#if executionResult?.outputs.length}
{#each executionResult.outputs as output}{output.content}{/each}
{:else if !isExecuting} -

No output

+

No output

{/if}
diff --git a/frontend/src/lib/components/chat/MessageContent.svelte b/frontend/src/lib/components/chat/MessageContent.svelte index 9cf5875..eed6831 100644 --- a/frontend/src/lib/components/chat/MessageContent.svelte +++ b/frontend/src/lib/components/chat/MessageContent.svelte @@ -29,6 +29,10 @@ // Supports both ... and ... (qwen3 format) const THINKING_PATTERN = /<(?:thinking|think)>([\s\S]*?)<\/(?:thinking|think)>/g; + // Pattern to detect JSON tool call objects (for models that output them as text) + // Matches: {"name": "...", "arguments": {...}} + const JSON_TOOL_CALL_PATTERN = /^(\s*\{[\s\S]*"name"\s*:\s*"[^"]+"\s*,\s*"arguments"\s*:\s*\{[\s\S]*\}\s*\}\s*)$/; + // Pattern to detect tool results in various formats const TOOL_RESULT_PATTERN = /Tool result:\s*(\{[\s\S]*?\}|\S[\s\S]*?)(?=\n\n|$)/; const TOOL_ERROR_PATTERN = /Tool error:\s*(.+?)(?=\n\n|$)/; @@ -75,6 +79,24 @@ return text.includes('Tool execution results:') || text.includes('Tool result:') || text.includes('Tool error:'); } + /** + * Check if text is a JSON tool call (for models that output them as text) + */ + function isJsonToolCall(text: string): boolean { + const trimmed = text.trim(); + if (!trimmed.startsWith('{')) return false; + try { + const parsed = JSON.parse(trimmed); + return typeof parsed === 'object' && + 'name' in parsed && + 'arguments' in parsed && + typeof parsed.name === 'string' && + typeof parsed.arguments === 'object'; + } catch { + return false; + } + } + /** * Parse a text section for tool results */ @@ -167,12 +189,20 @@ // Find all code blocks let match; while ((match = CODE_BLOCK_PATTERN.exec(remainingText)) !== null) { - // Add text before this code block (may contain tool results) + // Add text before this code block (may contain tool results or JSON tool calls) if (match.index > lastIndex) { const textBefore = remainingText.slice(lastIndex, match.index); if (textBefore.trim()) { if (containsToolResult(textBefore)) { parts.push(...parseTextForToolResults(textBefore)); + } else if (isJsonToolCall(textBefore)) { + // Render JSON tool calls as code blocks + try { + const formatted = JSON.stringify(JSON.parse(textBefore.trim()), null, 2); + parts.push({ type: 'code', content: formatted, language: 'json' }); + } catch { + parts.push({ type: 'text', content: textBefore }); + } } else { parts.push({ type: 'text', content: textBefore }); } @@ -197,6 +227,14 @@ if (remaining.trim()) { if (containsToolResult(remaining)) { parts.push(...parseTextForToolResults(remaining)); + } else if (isJsonToolCall(remaining)) { + // Render JSON tool calls as code blocks + try { + const formatted = JSON.stringify(JSON.parse(remaining.trim()), null, 2); + parts.push({ type: 'code', content: formatted, language: 'json' }); + } catch { + parts.push({ type: 'text', content: remaining }); + } } else { parts.push({ type: 'text', content: remaining }); } @@ -207,6 +245,14 @@ if (parts.length === thinkingParts.length && remainingText.trim()) { if (containsToolResult(remainingText)) { parts.push(...parseTextForToolResults(remainingText)); + } else if (isJsonToolCall(remainingText)) { + // Render JSON tool calls as code blocks + try { + const formatted = JSON.stringify(JSON.parse(remainingText.trim()), null, 2); + parts.push({ type: 'code', content: formatted, language: 'json' }); + } catch { + parts.push({ type: 'text', content: remainingText }); + } } else { parts.push({ type: 'text', content: remainingText }); }