fix: hide tool result user messages and improve visibility

- Hide tool result messages (role: user) from chat display
  They're internal API messages, not actual user input
- Improve pattern matching to catch all tool result formats
- Clean up "Tool execution results:" and "Called tool:" text
- Detect and hide HTML garbage from failed fetch attempts
- Add fetch result type with proper styling
- Improve text visibility in fallback displays

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2025-12-31 21:13:09 +01:00
parent f5fa9121f4
commit 9627bd3afc
3 changed files with 97 additions and 23 deletions

View File

@@ -21,12 +21,16 @@
// Pattern to find fenced code blocks
const CODE_BLOCK_PATTERN = /```(\w+)?\n([\s\S]*?)```/g;
// Pattern to detect tool execution results
const TOOL_RESULT_PATTERN = /Tool execution results:\s*\n(Tool (?:result|error):[\s\S]*?)(?=\n\nBased on these results|$)/;
// 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|$)/;
// Pattern for "Called tool:" text (redundant with ToolCallDisplay)
const CALLED_TOOL_PATTERN = /Called tool:\s*\w+\([^)]*\)\s*\n*/g;
// Pattern for "Tool execution results:" header
const TOOL_EXEC_HEADER_PATTERN = /Tool execution results:\s*\n?/g;
// Languages that should show a preview
const PREVIEW_LANGUAGES = ['html', 'htm'];
@@ -46,10 +50,14 @@
let modalImage = $state<string | null>(null);
/**
* Clean redundant "Called tool:" text (shown via ToolCallDisplay)
* Clean redundant tool text (shown via ToolCallDisplay)
*/
function cleanCalledToolText(text: string): string {
return text.replace(CALLED_TOOL_PATTERN, '').trim();
function cleanToolText(text: string): string {
return text
.replace(CALLED_TOOL_PATTERN, '')
.replace(TOOL_EXEC_HEADER_PATTERN, '')
.replace(/^Based on these results.*$/gm, '')
.trim();
}
/**
@@ -209,7 +217,7 @@
}
// Clean and parse content into parts
const cleanedContent = $derived(cleanCalledToolText(content));
const cleanedContent = $derived(cleanToolText(content));
const contentParts = $derived(parseContent(cleanedContent));
</script>

View File

@@ -40,6 +40,15 @@
const hasContent = $derived(node.message.content.length > 0);
const hasToolCalls = $derived(node.message.toolCalls && node.message.toolCalls.length > 0);
// Detect tool result messages (sent as user role but should be hidden or styled differently)
const isToolResultMessage = $derived(
isUser && (
node.message.content.startsWith('Tool execution results:') ||
node.message.content.startsWith('Tool result:') ||
node.message.content.startsWith('Tool error:')
)
);
/**
* Start editing a message
*/
@@ -80,6 +89,10 @@
}
</script>
<!-- Hide tool result messages - they're internal API messages -->
{#if isToolResultMessage}
<!-- Tool results are handled in the assistant message display -->
{:else}
<article
class="group mb-6 flex gap-4"
class:justify-end={isUser}
@@ -218,3 +231,4 @@
</div>
{/if}
</article>
{/if}

View File

@@ -11,7 +11,7 @@
let { content }: Props = $props();
interface ParsedResult {
type: 'location' | 'search' | 'error' | 'text' | 'json';
type: 'location' | 'search' | 'error' | 'text' | 'json' | 'fetch';
data: unknown;
}
@@ -39,36 +39,62 @@
results?: SearchResult[];
}
/**
* Try to extract JSON from text
*/
function extractJSON(text: string): unknown | null {
// Try to find JSON object in text
const jsonMatch = text.match(/\{[\s\S]*\}/);
if (jsonMatch) {
try {
return JSON.parse(jsonMatch[0]);
} catch {
return null;
}
}
return null;
}
/**
* Parse the tool result content
*/
function parseResult(text: string): ParsedResult {
// Try to extract JSON from "Tool result: {...}" format
const jsonMatch = text.match(/Tool result:\s*(\{[\s\S]*\})/);
if (!jsonMatch) {
// Check for error
if (text.includes('Tool error:')) {
const errorMatch = text.match(/Tool error:\s*(.+)/);
return { type: 'error', data: errorMatch?.[1] || text };
}
return { type: 'text', data: text };
// Check for error first
if (text.includes('Tool error:') || text.includes('HTTP 403') || text.includes('HTTP 4') || text.includes('HTTP 5')) {
const errorMatch = text.match(/(?:Tool error:\s*)?(.+)/);
return { type: 'error', data: errorMatch?.[1]?.trim() || text };
}
try {
const data = JSON.parse(jsonMatch[1]);
// Try to extract JSON
const json = extractJSON(text);
if (json && typeof json === 'object') {
const data = json as Record<string, unknown>;
// Detect result type
if (data.location && (data.location.city || data.location.latitude)) {
if (data.location && typeof data.location === 'object') {
return { type: 'location', data };
}
if (data.results && Array.isArray(data.results) && data.query) {
return { type: 'search', data };
}
if (data.title || data.text || data.url) {
return { type: 'fetch', data };
}
return { type: 'json', data };
} catch {
return { type: 'text', data: text };
}
// Plain text result (might be scraped content)
const cleanText = text.replace(/^Tool result:\s*/i, '').trim();
if (cleanText.length > 0) {
// Check if it looks like HTML garbage
if (cleanText.includes('<script') || cleanText.includes('googletagmanager')) {
return { type: 'error', data: 'Could not extract meaningful content from page' };
}
return { type: 'text', data: cleanText };
}
return { type: 'text', data: text };
}
const parsed = $derived(parseResult(content));
@@ -139,6 +165,28 @@
</div>
</div>
{:else if parsed.type === 'fetch'}
{@const data = parsed.data as Record<string, unknown>}
<div class="my-3 overflow-hidden rounded-xl border border-violet-500/30 bg-gradient-to-r from-violet-500/10 to-purple-500/10">
<div class="px-4 py-3">
<div class="flex items-center gap-2 text-sm">
<span>🌐</span>
{#if data.title}
<span class="font-medium text-slate-200">{data.title}</span>
{:else if data.url}
<a href={String(data.url)} target="_blank" rel="noopener noreferrer" class="text-violet-400 hover:underline">
{data.url}
</a>
{:else}
<span class="text-slate-400">Fetched content</span>
{/if}
</div>
{#if data.text && typeof data.text === 'string'}
<p class="mt-2 line-clamp-4 text-sm text-slate-400">{data.text.substring(0, 300)}{data.text.length > 300 ? '...' : ''}</p>
{/if}
</div>
</div>
{:else if parsed.type === 'json'}
{@const data = parsed.data as Record<string, unknown>}
<div class="my-3 rounded-xl border border-slate-700/50 bg-slate-800/50 p-3">
@@ -146,6 +194,10 @@
</div>
{:else}
<!-- Fallback: just show the text -->
<p class="text-slate-300">{parsed.data}</p>
<!-- Fallback: just show the text (if not empty/whitespace) -->
{#if typeof parsed.data === 'string' && parsed.data.trim().length > 0}
<div class="my-3 rounded-xl border border-slate-700/50 bg-slate-800/50 p-3">
<p class="text-sm text-slate-300">{parsed.data}</p>
</div>
{/if}
{/if}