-
Tool Details
+
Suna's Computer
diff --git a/frontend/src/components/thread/tool-views/BrowserToolView.tsx b/frontend/src/components/thread/tool-views/BrowserToolView.tsx
new file mode 100644
index 00000000..deca3911
--- /dev/null
+++ b/frontend/src/components/thread/tool-views/BrowserToolView.tsx
@@ -0,0 +1,110 @@
+import React from "react";
+import { Globe, MonitorPlay, ExternalLink, CheckCircle, AlertTriangle } from "lucide-react";
+import { BrowserToolViewProps } from "./types";
+import { extractBrowserUrl, extractBrowserOperation, formatTimestamp } from "./utils";
+
+export function BrowserToolView({
+ name,
+ assistantContent,
+ toolContent,
+ assistantTimestamp,
+ toolTimestamp,
+ isSuccess = true,
+ project
+}: BrowserToolViewProps) {
+ const url = extractBrowserUrl(assistantContent);
+ const operation = extractBrowserOperation(name);
+
+ // Check if we have a VNC preview URL from the project
+ const vncPreviewUrl = project?.sandbox?.vnc_preview ?
+ `${project.sandbox.vnc_preview}/vnc_lite.html?password=${project?.sandbox?.pass}` :
+ undefined;
+
+ return (
+
+
+
+
+
+
+
+
{operation}
+
+
+
+ {toolContent && (
+
+ {isSuccess ? 'Success' : 'Failed'}
+
+ )}
+
+
+
+
+
+
+ Browser Window
+
+ {url && (
+
+ {url}
+
+ )}
+
+
+ {vncPreviewUrl ? (
+
+
+
+ ) : (
+
+
+
Browser preview not available
+ {url && (
+
+ Visit URL
+
+ )}
+
+ )}
+
+ {isSuccess && (
+
+
+ {operation} completed successfully
+
+ )}
+
+ {!isSuccess && (
+
+ )}
+
+
+
+ {assistantTimestamp && (
+
Called: {formatTimestamp(assistantTimestamp)}
+ )}
+ {toolTimestamp && (
+
Result: {formatTimestamp(toolTimestamp)}
+ )}
+
+
+ );
+}
\ No newline at end of file
diff --git a/frontend/src/components/thread/tool-views/CommandToolView.tsx b/frontend/src/components/thread/tool-views/CommandToolView.tsx
new file mode 100644
index 00000000..4f938c41
--- /dev/null
+++ b/frontend/src/components/thread/tool-views/CommandToolView.tsx
@@ -0,0 +1,144 @@
+import React from "react";
+import { Terminal, CheckCircle, AlertTriangle } from "lucide-react";
+import { ToolViewProps } from "./types";
+import { extractCommand, extractCommandOutput, extractExitCode, formatTimestamp } from "./utils";
+
+export function CommandToolView({
+ assistantContent,
+ toolContent,
+ assistantTimestamp,
+ toolTimestamp,
+ isSuccess = true
+}: ToolViewProps) {
+ // Clean the command by removing any leading/trailing whitespace and newlines
+ const rawCommand = extractCommand(assistantContent);
+ // First remove the prompt prefix, then remove any newlines and extra spaces
+ const command = rawCommand
+ ?.replace(/^user@machine:~\$\s*/g, '') // Remove prompt prefix
+ ?.replace(/\\n/g, '') // Remove escaped newlines
+ ?.replace(/\n/g, '') // Remove actual newlines
+ ?.trim(); // Clean up any remaining whitespace
+
+ // Extract and clean the output
+ const rawOutput = extractCommandOutput(toolContent);
+ let output = rawOutput;
+
+ // Try to parse JSON if the output contains JSON structure
+ try {
+ if (rawOutput && rawOutput.includes('"output"')) {
+ const jsonMatch = rawOutput.match(/"output":\s*"([\s\S]*?)"/);
+ if (jsonMatch && jsonMatch[1]) {
+ // Replace escaped newlines with actual newlines
+ output = jsonMatch[1].replace(/\\n/g, '\n').replace(/\\"/g, '"');
+ }
+ }
+ } catch (e) {
+ // If parsing fails, use the original output
+ console.error("Error parsing command output:", e);
+ }
+
+ const exitCode = extractExitCode(toolContent);
+
+ return (
+
+
+
+
+
+
+
+
Execute Command
+
+
+
+ {toolContent && (
+
+ {isSuccess ? 'Success' : 'Failed'}
+
+ )}
+
+
+
+
+
+ {exitCode !== null && (
+
+
+ Exit: {exitCode}
+
+ )}
+
+
+
+
+ {command && output && (
+
+
+ user@machine:~$
+ {command}
+
+
+
+ {output}
+
+
+ {isSuccess &&
user@machine:~$ _
}
+
+ )}
+
+ {command && !output && (
+
+
+ user@machine:~$
+ {command}
+
+
+
+ )}
+
+ {!command && !output && (
+
+ user@machine:~$
+
+
+ )}
+
+
+
+ {isSuccess && output && exitCode === 0 && (
+
+
+ Command completed successfully
+
+ )}
+
+ {exitCode !== null && !isSuccess && (
+
+
+
Command failed with exit code {exitCode}
+
+ )}
+
+
+
+ {assistantTimestamp && (
+
Called: {formatTimestamp(assistantTimestamp)}
+ )}
+ {toolTimestamp && (
+
Result: {formatTimestamp(toolTimestamp)}
+ )}
+
+
+ );
+}
\ No newline at end of file
diff --git a/frontend/src/components/thread/tool-views/CreateFileToolView.tsx b/frontend/src/components/thread/tool-views/CreateFileToolView.tsx
new file mode 100644
index 00000000..76d62cc5
--- /dev/null
+++ b/frontend/src/components/thread/tool-views/CreateFileToolView.tsx
@@ -0,0 +1,122 @@
+import React from "react";
+import { FileCode, FileSymlink, FolderPlus, CheckCircle, AlertTriangle } from "lucide-react";
+import { ToolViewProps } from "./types";
+import { extractFilePath, extractFileContent, getFileType, formatTimestamp } from "./utils";
+import { GenericToolView } from "./GenericToolView";
+
+export function CreateFileToolView({
+ assistantContent,
+ toolContent,
+ assistantTimestamp,
+ toolTimestamp,
+ isSuccess = true
+}: ToolViewProps) {
+ const filePath = extractFilePath(assistantContent);
+ const fileContent = extractFileContent(assistantContent, 'create-file');
+
+ if (!filePath || !fileContent) {
+ return (
+
+ );
+ }
+
+ // Split content into lines for line numbering
+ const contentLines = fileContent.split('\n');
+ const fileType = getFileType(filePath);
+ const fileName = filePath.split('/').pop() || filePath;
+ const isMarkdown = fileName.endsWith('.md');
+
+ return (
+
+
+
+
+
+
+
+
Create File
+
+
+
+ {toolContent && (
+
+ {isSuccess ? 'Success' : 'Failed'}
+
+ )}
+
+
+
+ {/* IDE Header */}
+
+
+ {isMarkdown ?
+ :
+
+ }
+ {fileName}
+
+
+ {fileType}
+
+
+
+ {/* File Path Bar */}
+
+
+ {/* IDE Content Area */}
+
+
+ {contentLines.map((line, idx) => (
+
+
+ {idx + 1}
+
+
+ {line || ' '}
+
+
+ ))}
+ {/* Add an empty line at the end */}
+
+
+
+
+ {/* Status Footer */}
+ {isSuccess ? (
+
+
+ {fileName} created successfully
+
+ ) : (
+
+
+
Failed to create file
+
+ )}
+
+
+
+ {assistantTimestamp && (
+
Called: {formatTimestamp(assistantTimestamp)}
+ )}
+ {toolTimestamp && (
+
Result: {formatTimestamp(toolTimestamp)}
+ )}
+
+
+ );
+}
\ No newline at end of file
diff --git a/frontend/src/components/thread/tool-views/DeleteFileToolView.tsx b/frontend/src/components/thread/tool-views/DeleteFileToolView.tsx
new file mode 100644
index 00000000..7fbb2295
--- /dev/null
+++ b/frontend/src/components/thread/tool-views/DeleteFileToolView.tsx
@@ -0,0 +1,90 @@
+import React from "react";
+import { FileX, CheckCircle, AlertTriangle } from "lucide-react";
+import { ToolViewProps } from "./types";
+import { extractFilePath, formatTimestamp } from "./utils";
+import { GenericToolView } from "./GenericToolView";
+
+export function DeleteFileToolView({
+ assistantContent,
+ toolContent,
+ assistantTimestamp,
+ toolTimestamp,
+ isSuccess = true
+}: ToolViewProps) {
+ const filePath = extractFilePath(assistantContent);
+
+ if (!filePath) {
+ return (
+
+ );
+ }
+
+ return (
+
+
+
+
+
+
+
+
Delete File
+
+
+
+ {toolContent && (
+
+ {isSuccess ? 'Success' : 'Failed'}
+
+ )}
+
+
+
+
+
+
+
+
{filePath}
+
This file has been deleted
+
+
+
+ {isSuccess && (
+
+
+ File deleted successfully
+
+ )}
+
+ {!isSuccess && (
+
+
+
Failed to delete file
+
+ )}
+
+
+
+ {assistantTimestamp && (
+
Called: {formatTimestamp(assistantTimestamp)}
+ )}
+ {toolTimestamp && (
+
Result: {formatTimestamp(toolTimestamp)}
+ )}
+
+
+ );
+}
\ No newline at end of file
diff --git a/frontend/src/components/thread/tool-views/FileRewriteToolView.tsx b/frontend/src/components/thread/tool-views/FileRewriteToolView.tsx
new file mode 100644
index 00000000..591631da
--- /dev/null
+++ b/frontend/src/components/thread/tool-views/FileRewriteToolView.tsx
@@ -0,0 +1,122 @@
+import React from "react";
+import { FileCode, FileSymlink, Replace, CheckCircle, AlertTriangle } from "lucide-react";
+import { ToolViewProps } from "./types";
+import { extractFilePath, extractFileContent, getFileType, formatTimestamp } from "./utils";
+import { GenericToolView } from "./GenericToolView";
+
+export function FileRewriteToolView({
+ assistantContent,
+ toolContent,
+ assistantTimestamp,
+ toolTimestamp,
+ isSuccess = true
+}: ToolViewProps) {
+ const filePath = extractFilePath(assistantContent);
+ const fileContent = extractFileContent(assistantContent, 'full-file-rewrite');
+
+ if (!filePath || !fileContent) {
+ return (
+
+ );
+ }
+
+ // Split content into lines for line numbering
+ const contentLines = fileContent.split('\n');
+ const fileType = getFileType(filePath);
+ const fileName = filePath.split('/').pop() || filePath;
+ const isMarkdown = fileName.endsWith('.md');
+
+ return (
+
+
+
+
+
+
+
+
File Rewrite
+
+
+
+ {toolContent && (
+
+ {isSuccess ? 'Success' : 'Failed'}
+
+ )}
+
+
+
+ {/* IDE Header */}
+
+
+ {isMarkdown ?
+ :
+
+ }
+ {fileName}
+
+
+ {fileType}
+
+
+
+ {/* File Path Bar */}
+
+
+ {/* IDE Content Area */}
+
+
+ {contentLines.map((line, idx) => (
+
+
+ {idx + 1}
+
+
+ {line || ' '}
+
+
+ ))}
+ {/* Add an empty line at the end */}
+
+
+
+
+ {/* Status Footer */}
+ {isSuccess ? (
+
+
+ {fileName} rewritten successfully
+
+ ) : (
+
+
+
Failed to rewrite file
+
+ )}
+
+
+
+ {assistantTimestamp && (
+
Called: {formatTimestamp(assistantTimestamp)}
+ )}
+ {toolTimestamp && (
+
Result: {formatTimestamp(toolTimestamp)}
+ )}
+
+
+ );
+}
\ No newline at end of file
diff --git a/frontend/src/components/thread/tool-views/GenericToolView.tsx b/frontend/src/components/thread/tool-views/GenericToolView.tsx
new file mode 100644
index 00000000..5d4dab6e
--- /dev/null
+++ b/frontend/src/components/thread/tool-views/GenericToolView.tsx
@@ -0,0 +1,66 @@
+import React from "react";
+import { ToolViewProps } from "./types";
+import { formatTimestamp } from "./utils";
+import { getToolIcon } from "../utils";
+
+export function GenericToolView({
+ name,
+ assistantContent,
+ toolContent,
+ isSuccess = true,
+ assistantTimestamp,
+ toolTimestamp
+}: ToolViewProps & { name?: string }) {
+ const toolName = name || 'Unknown Tool';
+
+ return (
+
+
+
+
+ {React.createElement(getToolIcon(toolName), { className: "h-4 w-4" })}
+
+
+
{toolName}
+
+
+
+ {toolContent && (
+
+ {isSuccess ? 'Success' : 'Failed'}
+
+ )}
+
+
+ {/* Assistant Message */}
+
+
+
Assistant Message
+ {assistantTimestamp && (
+
{formatTimestamp(assistantTimestamp)}
+ )}
+
+
+
+
+ {/* Tool Result */}
+ {toolContent && (
+
+
+
Tool Result
+ {toolTimestamp && (
+
{formatTimestamp(toolTimestamp)}
+ )}
+
+
+
+ )}
+
+ );
+}
\ No newline at end of file
diff --git a/frontend/src/components/thread/tool-views/StrReplaceToolView.tsx b/frontend/src/components/thread/tool-views/StrReplaceToolView.tsx
new file mode 100644
index 00000000..21159399
--- /dev/null
+++ b/frontend/src/components/thread/tool-views/StrReplaceToolView.tsx
@@ -0,0 +1,149 @@
+import React from "react";
+import { FileSearch, FileDiff, CheckCircle, AlertTriangle } from "lucide-react";
+import { ToolViewProps } from "./types";
+import { extractFilePath, extractStrReplaceContent, formatTimestamp } from "./utils";
+import { GenericToolView } from "./GenericToolView";
+
+export function StrReplaceToolView({
+ assistantContent,
+ toolContent,
+ assistantTimestamp,
+ toolTimestamp,
+ isSuccess = true
+}: ToolViewProps) {
+ const filePath = extractFilePath(assistantContent);
+ const { oldStr, newStr } = extractStrReplaceContent(assistantContent);
+
+ if (!oldStr || !newStr) {
+ return (
+
+ );
+ }
+
+ // Perform a character-level diff to identify changes
+ const generateDiff = (oldText: string, newText: string) => {
+ let i = 0;
+ let j = 0;
+
+ // Find common prefix length
+ let prefixLength = 0;
+ while (prefixLength < oldText.length && prefixLength < newText.length &&
+ oldText[prefixLength] === newText[prefixLength]) {
+ prefixLength++;
+ }
+
+ // Find common suffix length
+ let oldSuffixStart = oldText.length;
+ let newSuffixStart = newText.length;
+ while (oldSuffixStart > prefixLength && newSuffixStart > prefixLength &&
+ oldText[oldSuffixStart - 1] === newText[newSuffixStart - 1]) {
+ oldSuffixStart--;
+ newSuffixStart--;
+ }
+
+ // Generate unified diff parts
+ const parts = [];
+
+ // Add common prefix
+ if (prefixLength > 0) {
+ parts.push({ text: oldText.substring(0, prefixLength), type: 'unchanged' });
+ }
+
+ // Add the changed middle parts
+ if (oldSuffixStart > prefixLength) {
+ parts.push({ text: oldText.substring(prefixLength, oldSuffixStart), type: 'removed' });
+ }
+ if (newSuffixStart > prefixLength) {
+ parts.push({ text: newText.substring(prefixLength, newSuffixStart), type: 'added' });
+ }
+
+ // Add common suffix
+ if (oldSuffixStart < oldText.length) {
+ parts.push({ text: oldText.substring(oldSuffixStart), type: 'unchanged' });
+ }
+
+ return parts;
+ };
+
+ const diffParts = generateDiff(oldStr, newStr);
+
+ return (
+
+
+
+
+
+
+
+
String Replace
+
+
+
+ {toolContent && (
+
+ {isSuccess ? 'Success' : 'Failed'}
+
+ )}
+
+
+
+
+
+
+ {filePath || 'Unknown file'}
+
+
+
+ {diffParts.map((part, i) => (
+
+ {part.text}
+
+ ))}
+
+
+ {isSuccess && (
+
+
+ Replacement applied successfully
+
+ )}
+
+ {!isSuccess && (
+
+ )}
+
+
+
+ {assistantTimestamp && (
+
Called: {formatTimestamp(assistantTimestamp)}
+ )}
+ {toolTimestamp && (
+
Result: {formatTimestamp(toolTimestamp)}
+ )}
+
+
+ );
+}
\ No newline at end of file
diff --git a/frontend/src/components/thread/tool-views/WebCrawlToolView.tsx b/frontend/src/components/thread/tool-views/WebCrawlToolView.tsx
new file mode 100644
index 00000000..6f0899ff
--- /dev/null
+++ b/frontend/src/components/thread/tool-views/WebCrawlToolView.tsx
@@ -0,0 +1,149 @@
+import React from "react";
+import { Globe, ArrowUpRight, Copy, CheckCircle, AlertTriangle } from "lucide-react";
+import { ToolViewProps } from "./types";
+import { extractCrawlUrl, extractWebpageContent, formatTimestamp } from "./utils";
+import { GenericToolView } from "./GenericToolView";
+
+export function WebCrawlToolView({
+ assistantContent,
+ toolContent,
+ assistantTimestamp,
+ toolTimestamp,
+ isSuccess = true
+}: ToolViewProps) {
+ const url = extractCrawlUrl(assistantContent);
+ const webpageContent = extractWebpageContent(toolContent);
+
+ if (!url) {
+ return (
+
+ );
+ }
+
+ // Format domain for display
+ const formatDomain = (url: string): string => {
+ try {
+ const urlObj = new URL(url);
+ return urlObj.hostname.replace('www.', '');
+ } catch (e) {
+ return url;
+ }
+ };
+
+ const domain = url ? formatDomain(url) : 'Unknown';
+
+ // Format the extracted text into paragraphs
+ const formatTextContent = (text: string): React.ReactNode[] => {
+ if (!text) return [
No content extracted
];
+
+ return text.split('\n\n').map((paragraph, idx) => {
+ if (!paragraph.trim()) return null;
+ return (
+
+ {paragraph.trim()}
+
+ );
+ }).filter(Boolean);
+ };
+
+ return (
+
+
+
+
+ {toolContent && (
+
+ {isSuccess ? 'Success' : 'Failed'}
+
+ )}
+
+
+
+ {/* Webpage Header */}
+
+
+
+
+ {webpageContent?.title || domain}
+
+
+
+
+
+ {/* URL Bar */}
+
+
+ {/* Webpage Content */}
+
+ {webpageContent ? (
+
+
{webpageContent.title}
+
+ {formatTextContent(webpageContent.text)}
+
+
+ ) : (
+
+
+
No content extracted
+
+ )}
+
+
+ {/* Status Footer */}
+ {isSuccess ? (
+
+
+ Webpage crawled successfully
+
+ ) : (
+
+
+
Failed to crawl webpage
+
+ )}
+
+
+
+ {assistantTimestamp && (
+
Called: {formatTimestamp(assistantTimestamp)}
+ )}
+ {toolTimestamp && (
+
Result: {formatTimestamp(toolTimestamp)}
+ )}
+
+
+ );
+}
\ No newline at end of file
diff --git a/frontend/src/components/thread/tool-views/WebSearchToolView.tsx b/frontend/src/components/thread/tool-views/WebSearchToolView.tsx
new file mode 100644
index 00000000..2a76548e
--- /dev/null
+++ b/frontend/src/components/thread/tool-views/WebSearchToolView.tsx
@@ -0,0 +1,101 @@
+import React from "react";
+import { Search } from "lucide-react";
+import { ToolViewProps } from "./types";
+import { extractSearchQuery, extractSearchResults, cleanUrl, formatTimestamp } from "./utils";
+
+export function WebSearchToolView({
+ assistantContent,
+ toolContent,
+ assistantTimestamp,
+ toolTimestamp,
+ isSuccess = true
+}: ToolViewProps) {
+ const query = extractSearchQuery(assistantContent);
+ const searchResults = extractSearchResults(toolContent);
+
+ return (
+
+
+
+
+ {toolContent && (
+
+ {isSuccess ? 'Success' : 'Failed'}
+
+ )}
+
+
+
+
+
+
+
+
Query:
+
{query || 'Unknown query'}
+
+
+ {searchResults.length > 0 ? `Found ${searchResults.length} results` : 'No results found'}
+
+
+
+
+ {searchResults.length > 0 ? (
+
+ {searchResults.map((result, idx) => (
+
+
+ {result.snippet && (
+
+ {result.snippet}
+
+ )}
+
+ ))}
+
+ ) : (
+
+
+
No results found
+
Try a different search query
+
+ )}
+
+
+
+
+ {assistantTimestamp && (
+
Called: {formatTimestamp(assistantTimestamp)}
+ )}
+ {toolTimestamp && (
+
Result: {formatTimestamp(toolTimestamp)}
+ )}
+
+
+ );
+}
\ No newline at end of file
diff --git a/frontend/src/components/thread/tool-views/types.ts b/frontend/src/components/thread/tool-views/types.ts
new file mode 100644
index 00000000..fc8a920b
--- /dev/null
+++ b/frontend/src/components/thread/tool-views/types.ts
@@ -0,0 +1,14 @@
+import { Project } from "@/lib/api";
+
+export interface ToolViewProps {
+ assistantContent?: string;
+ toolContent?: string;
+ assistantTimestamp?: string;
+ toolTimestamp?: string;
+ isSuccess?: boolean;
+ project?: Project;
+}
+
+export interface BrowserToolViewProps extends ToolViewProps {
+ name?: string;
+}
\ No newline at end of file
diff --git a/frontend/src/components/thread/tool-views/utils.ts b/frontend/src/components/thread/tool-views/utils.ts
new file mode 100644
index 00000000..bf1d5038
--- /dev/null
+++ b/frontend/src/components/thread/tool-views/utils.ts
@@ -0,0 +1,301 @@
+// Helper function to format timestamp
+export function formatTimestamp(isoString?: string): string {
+ if (!isoString) return '';
+ try {
+ const date = new Date(isoString);
+ return isNaN(date.getTime()) ? 'Invalid date' : date.toLocaleString();
+ } catch (e) {
+ return 'Invalid date';
+ }
+}
+
+// Helper to extract command from execute-command content
+export function extractCommand(content: string | undefined): string | null {
+ if (!content) return null;
+ const commandMatch = content.match(/
([\s\S]*?)<\/execute-command>/);
+ return commandMatch ? commandMatch[1].trim() : null;
+}
+
+// Helper to extract command output from tool result content
+export function extractCommandOutput(content: string | undefined): string | null {
+ if (!content) return null;
+
+ try {
+ // First try to parse the JSON content
+ const parsedContent = JSON.parse(content);
+ if (parsedContent.content && typeof parsedContent.content === 'string') {
+ // Look for a tool_result tag
+ const toolResultMatch = parsedContent.content.match(/\s*([\s\S]*?)<\/execute-command>\s*<\/tool_result>/);
+ if (toolResultMatch) {
+ return toolResultMatch[1].trim();
+ }
+
+ // Look for output field in a ToolResult pattern
+ const outputMatch = parsedContent.content.match(/ToolResult\(.*?output='([\s\S]*?)'.*?\)/);
+ if (outputMatch) {
+ return outputMatch[1];
+ }
+
+ // Return the content itself as a fallback
+ return parsedContent.content;
+ }
+ } catch (e) {
+ // If JSON parsing fails, try regex directly
+ const toolResultMatch = content.match(/\s*([\s\S]*?)<\/execute-command>\s*<\/tool_result>/);
+ if (toolResultMatch) {
+ return toolResultMatch[1].trim();
+ }
+
+ const outputMatch = content.match(/ToolResult\(.*?output='([\s\S]*?)'.*?\)/);
+ if (outputMatch) {
+ return outputMatch[1];
+ }
+ }
+
+ return content;
+}
+
+// Helper to extract the exit code from tool result
+export function extractExitCode(content: string | undefined): number | null {
+ if (!content) return null;
+
+ try {
+ const exitCodeMatch = content.match(/exit_code=(\d+)/);
+ if (exitCodeMatch && exitCodeMatch[1]) {
+ return parseInt(exitCodeMatch[1], 10);
+ }
+ return 0; // Assume success if no exit code found but command completed
+ } catch (e) {
+ return null;
+ }
+}
+
+// Helper to extract file path from commands
+export function extractFilePath(content: string | undefined): string | null {
+ if (!content) return null;
+
+ // Try to parse JSON content first
+ try {
+ const parsedContent = JSON.parse(content);
+ if (parsedContent.content && typeof parsedContent.content === 'string') {
+ content = parsedContent.content;
+ }
+ } catch (e) {
+ // Continue with original content if parsing fails
+ }
+
+ // Look for file_path in different formats
+ const filePathMatch = content.match(/file_path=["']([\s\S]*?)["']/);
+ if (filePathMatch) return filePathMatch[1];
+
+ // Look for file_path in XML-like tags
+ const xmlFilePathMatch = content.match(/([\s\S]*?)<\/old_str>/);
+ const newMatch = content.match(/([\s\S]*?)<\/new_str>/);
+
+ return {
+ oldStr: oldMatch ? oldMatch[1] : null,
+ newStr: newMatch ? newMatch[1] : null
+ };
+}
+
+// Helper to extract file content from create-file or file-rewrite
+export function extractFileContent(content: string | undefined, toolType: 'create-file' | 'full-file-rewrite'): string | null {
+ if (!content) return null;
+
+ const tagName = toolType === 'create-file' ? 'create-file' : 'full-file-rewrite';
+ const contentMatch = content.match(new RegExp(`<${tagName}[^>]*>([\\s\\S]*?)<\\/${tagName}>`, 'i'));
+ return contentMatch ? contentMatch[1] : null;
+}
+
+// Helper to determine file type (for syntax highlighting)
+export function getFileType(filePath: string): string {
+ const extension = filePath.split('.').pop()?.toLowerCase() || '';
+
+ switch (extension) {
+ case 'js': return 'JavaScript';
+ case 'ts': return 'TypeScript';
+ case 'jsx': case 'tsx': return 'React';
+ case 'py': return 'Python';
+ case 'html': return 'HTML';
+ case 'css': return 'CSS';
+ case 'json': return 'JSON';
+ case 'md': return 'Markdown';
+ default: return extension.toUpperCase() || 'Text';
+ }
+}
+
+// Helper to extract URL from browser navigate operations
+export function extractBrowserUrl(content: string | undefined): string | null {
+ if (!content) return null;
+ const urlMatch = content.match(/url=["'](https?:\/\/[^"']+)["']/);
+ return urlMatch ? urlMatch[1] : null;
+}
+
+// Helper to extract browser operation type
+export function extractBrowserOperation(toolName: string | undefined): string {
+ if (!toolName) return 'Browser Operation';
+
+ const operation = toolName.replace('browser-', '').replace(/-/g, ' ');
+ return operation.charAt(0).toUpperCase() + operation.slice(1);
+}
+
+// Helper to extract search query
+export function extractSearchQuery(content: string | undefined): string | null {
+ if (!content) return null;
+ const queryMatch = content.match(/query=["']([\s\S]*?)["']/);
+ return queryMatch ? queryMatch[1] : null;
+}
+
+// Helper to extract search results from tool response
+export function extractSearchResults(content: string | undefined): Array<{ title: string, url: string, snippet?: string }> {
+ if (!content) return [];
+
+ try {
+ // Try to parse JSON content first
+ const parsedContent = JSON.parse(content);
+ if (parsedContent.content && typeof parsedContent.content === 'string') {
+ // Look for a tool_result tag
+ const toolResultMatch = parsedContent.content.match(/\s*([\s\S]*?)<\/web-search>\s*<\/tool_result>/);
+ if (toolResultMatch) {
+ // Try to parse the results array
+ try {
+ return JSON.parse(toolResultMatch[1]);
+ } catch (e) {
+ // Fallback to regex extraction of URLs and titles
+ return extractUrlsAndTitles(toolResultMatch[1]);
+ }
+ }
+
+ // Look for ToolResult pattern
+ const outputMatch = parsedContent.content.match(/ToolResult\(.*?output='([\s\S]*?)'.*?\)/);
+ if (outputMatch) {
+ try {
+ return JSON.parse(outputMatch[1]);
+ } catch (e) {
+ return extractUrlsAndTitles(outputMatch[1]);
+ }
+ }
+
+ // Try to find JSON array in the content
+ const jsonArrayMatch = parsedContent.content.match(/\[\s*{[\s\S]*}\s*\]/);
+ if (jsonArrayMatch) {
+ try {
+ return JSON.parse(jsonArrayMatch[0]);
+ } catch (e) {
+ return [];
+ }
+ }
+ }
+ } catch (e) {
+ // If JSON parsing fails, try regex direct extraction
+ const urlMatch = content.match(/https?:\/\/[^\s"]+/g);
+ if (urlMatch) {
+ return urlMatch.map(url => ({
+ title: cleanUrl(url),
+ url
+ }));
+ }
+ }
+
+ return [];
+}
+
+// Helper to extract URLs and titles with regex
+export function extractUrlsAndTitles(content: string): Array<{ title: string, url: string, snippet?: string }> {
+ const results: Array<{ title: string, url: string, snippet?: string }> = [];
+
+ // Match URL and title pairs
+ const urlMatches = content.match(/https?:\/\/[^\s"]+/g) || [];
+ urlMatches.forEach(url => {
+ // Try to find a title near this URL
+ const urlIndex = content.indexOf(url);
+ const surroundingText = content.substring(Math.max(0, urlIndex - 100), urlIndex + url.length + 100);
+
+ // Look for "Title:" or similar patterns
+ const titleMatch = surroundingText.match(/Title[:\s]+([^\n]+)/i) ||
+ surroundingText.match(/\"(.*?)\"[\s\n]*?https?:\/\//);
+
+ const title = titleMatch ? titleMatch[1] : cleanUrl(url);
+
+ results.push({
+ title: title,
+ url: url
+ });
+ });
+
+ return results;
+}
+
+// Helper to clean URL for display
+export function cleanUrl(url: string): string {
+ try {
+ const urlObj = new URL(url);
+ return urlObj.hostname.replace('www.', '') + (urlObj.pathname !== '/' ? urlObj.pathname : '');
+ } catch (e) {
+ return url;
+ }
+}
+
+// Helper to extract URL for webpage crawling
+export function extractCrawlUrl(content: string | undefined): string | null {
+ if (!content) return null;
+ const urlMatch = content.match(/url=["'](https?:\/\/[^"']+)["']/);
+ return urlMatch ? urlMatch[1] : null;
+}
+
+// Helper to extract webpage content from crawl result
+export function extractWebpageContent(content: string | undefined): { title: string, text: string } | null {
+ if (!content) return null;
+
+ try {
+ // Try to parse the JSON content
+ const parsedContent = JSON.parse(content);
+ if (parsedContent.content && typeof parsedContent.content === 'string') {
+ // Look for tool_result tag
+ const toolResultMatch = parsedContent.content.match(/\s*([\s\S]*?)<\/crawl-webpage>\s*<\/tool_result>/);
+ if (toolResultMatch) {
+ try {
+ const crawlData = JSON.parse(toolResultMatch[1]);
+ return {
+ title: crawlData.title || '',
+ text: crawlData.text || crawlData.content || ''
+ };
+ } catch (e) {
+ // Fallback to basic text extraction
+ return {
+ title: 'Webpage Content',
+ text: toolResultMatch[1]
+ };
+ }
+ }
+ }
+
+ // Direct content extraction from parsed JSON
+ if (parsedContent.content) {
+ return {
+ title: 'Webpage Content',
+ text: parsedContent.content
+ };
+ }
+ } catch (e) {
+ // If JSON parsing fails, return the content as-is
+ if (content) {
+ return {
+ title: 'Webpage Content',
+ text: content
+ };
+ }
+ }
+
+ return null;
+}
\ No newline at end of file