port & other

This commit is contained in:
marko-kraemer 2025-04-21 15:38:27 +01:00
parent 2b8c3a9ffb
commit 2a59bd4baf
16 changed files with 300 additions and 116 deletions

View File

@ -302,7 +302,7 @@ async def start_agent(
# Get preview links
vnc_link = sandbox.get_preview_link(6080)
website_link = sandbox.get_preview_link(8080)
website_link = sandbox.get_preview_link(8099)
# Extract the actual URLs and token from the preview link objects
vnc_url = vnc_link.url if hasattr(vnc_link, 'url') else str(vnc_link).split("url='")[1].split("'")[0]

View File

@ -51,7 +51,8 @@ You have the ability to execute operations using both Python and CLI tools:
- Executing scheduled or event-driven tasks
- Exposing ports to the public internet using the 'expose-port' tool:
* Use this tool to make services running in the sandbox accessible to users
* Example: Expose a web server on port 8080 to share with users
* Example: Expose something running on port 8000 to share with users
* Port 8099 is already exposed by default and running a WEB HTTP server - no need to expose it again.
* The tool generates a public URL that users can access
* Essential for sharing web applications, APIs, and other network services
* Always expose ports when you need to show running services to users

View File

@ -34,10 +34,10 @@ class SandboxExposeTool(SandboxToolsBase):
{"param_name": "port", "node_type": "content", "path": "."}
],
example='''
<!-- Example 1: Expose a web server running on port 8080 -->
<!-- Example 1: Expose a web server running on port 8099 -->
<!-- This will generate a public URL that users can access to view the web application -->
<expose-port>
8080
8099
</expose-port>
<!-- Example 2: Expose an API service running on port 3000 -->

View File

@ -123,6 +123,6 @@ ENV RESOLUTION_HEIGHT=1080
RUN mkdir -p /var/log/supervisor
COPY supervisord.conf /etc/supervisor/conf.d/supervisord.conf
EXPOSE 7788 6080 5901 8000 8080
EXPOSE 7788 6080 5901 8000 8099
CMD ["/usr/bin/supervisord", "-c", "/etc/supervisor/conf.d/supervisord.conf"]

View File

@ -12,7 +12,7 @@ services:
- "5901:5901" # VNC port
- "9222:9222" # Chrome remote debugging port
- "8000:8000" # API server port
- "8080:8080" # HTTP server port
- "8099:8099" # HTTP server port
environment:
- ANONYMIZED_TELEMETRY=${ANONYMIZED_TELEMETRY:-false}
- CHROME_PATH=/usr/bin/google-chrome

View File

@ -26,4 +26,4 @@ app.mount('/', StaticFiles(directory=workspace_dir, html=True), name='site')
if __name__ == '__main__':
print(f"Starting server with auto-reload, serving files from: {workspace_dir}")
# Don't use reload directly in the run call
uvicorn.run("server:app", host="0.0.0.0", port=8080, reload=True)
uvicorn.run("server:app", host="0.0.0.0", port=8099, reload=True)

View File

@ -116,7 +116,7 @@ def create_sandbox(password: str):
5900, # VNC port
5901, # VNC port
9222, # Chrome remote debugging port
8080, # HTTP website port
8099, # HTTP website port
8002, # The browser api port
]
))
@ -154,7 +154,7 @@ class SandboxToolsBase(Tool):
# Get preview links
vnc_link = self.sandbox.get_preview_link(6080)
website_link = self.sandbox.get_preview_link(8080)
website_link = self.sandbox.get_preview_link(8099)
# Extract the actual URLs from the preview link objects
vnc_url = vnc_link.url if hasattr(vnc_link, 'url') else str(vnc_link)

View File

@ -1107,6 +1107,7 @@ export default function ThreadPage({ params }: { params: Promise<ThreadParams> }
projectId={project?.id || ""}
onViewFiles={handleOpenFileViewer}
onToggleSidePanel={toggleSidePanel}
isMobileView={isMobile}
/>
<div className="flex flex-1 items-center justify-center p-4">
<div className="flex w-full max-w-md flex-col items-center gap-4 rounded-lg border bg-card p-6 text-center">
@ -1134,16 +1135,15 @@ export default function ThreadPage({ params }: { params: Promise<ThreadParams> }
return (
<div className="flex h-screen">
<div className={`flex flex-col flex-1 overflow-hidden transition-all duration-200 ease-in-out ${isSidePanelOpen ? 'mr-[90%] sm:mr-[450px] md:mr-[500px] lg:mr-[550px] xl:mr-[650px]' : ''}`}>
{!isMobile && (
<SiteHeader
threadId={threadId}
projectName={projectName}
projectId={project?.id || ""}
onViewFiles={handleOpenFileViewer}
onToggleSidePanel={toggleSidePanel}
onProjectRenamed={handleProjectRenamed}
/>
)}
<SiteHeader
threadId={threadId}
projectName={projectName}
projectId={project?.id || ""}
onViewFiles={handleOpenFileViewer}
onToggleSidePanel={toggleSidePanel}
onProjectRenamed={handleProjectRenamed}
isMobileView={isMobile}
/>
<div
ref={messagesContainerRef}
className="flex-1 overflow-y-auto px-6 py-4 pb-24 bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60"

View File

@ -296,9 +296,34 @@
html {
scroll-behavior: smooth;
zoom: 100%;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
body {
@apply bg-background text-foreground;
font-feature-settings: "palt"; /* Adjusts spacing for CJK text */
}
/* Add font fallbacks for CJK characters */
.cjk-text,
.prose p,
.prose li,
.prose table td,
.prose table th,
.markdown-content {
font-family: var(--font-sans), ui-sans-serif, -apple-system, "Segoe UI", "Helvetica Neue",
"Noto Sans", "Noto Sans CJK JP", "Noto Sans CJK KR", "Noto Sans CJK SC",
"Noto Sans CJK TC", sans-serif;
line-height: 1.7;
}
/* Specific handling for monospace/code with CJK character support */
code, pre, .font-mono {
font-family: var(--font-mono), ui-monospace, SFMono-Regular, "SF Mono", Menlo, Monaco,
Consolas, "Liberation Mono", "Courier New", monospace,
"Noto Sans Mono CJK JP", "Noto Sans Mono CJK KR", "Noto Sans Mono CJK SC",
"Noto Sans Mono CJK TC";
}
}

View File

@ -0,0 +1,38 @@
# Japanese Language Test
## Basic Phrases
| Japanese | Pronunciation | English |
|----------|--------------|---------|
| こんにちは | Kon-nee-chee-wah | Hello |
| おはようございます | Oh-hah-yoh goh-zai-mahs | Good morning |
| こんばんは | Kon-bahn-wah | Good evening |
| さようなら | Sah-yoh-nah-rah | Goodbye |
| ありがとうございます | Ah-ree-gah-toh goh-zai-mahs | Thank you very much |
| どういたしまして | Doh-ee-tah-shee-mah-shteh | You're welcome |
| すみません | Soo-mee-mah-sen | Excuse me / Sorry |
| はい | Hai | Yes |
| いいえ | Ee-eh | No |
| お願いします | Oh-neh-gai shee-mahs | Please |
| わかりません | Wah-kah-ree-mah-sen | I don't understand |
| 英語を話せますか? | Ay-go oh hah-nah-seh-mahs-kah? | Do you speak English? |
| ゆっくりお願いします | Yook-koo-ree oh-neh-gai shee-mahs | Please speak slowly |
## Travel & Transportation
| Japanese | Pronunciation | English |
|----------|--------------|---------|
| __はどこですか? | _____ wa doh-koh dehs-kah? | Where is _____? |
| 駅 | Eh-kee | Station |
| トイレ | Toy-reh | Toilet/bathroom |
| 入口 | Ee-ree-goo-chee | Entrance |
| 出口 | Deh-goo-chee | Exit |
| 新幹線 | Sheen-kahn-sen | Bullet train |
| 電車 | Den-shah | Train |
| バス | Bah-soo | Bus |
| タクシー | Tak-shee | Taxi |
| いくらですか? | Ee-koo-rah dehs-kah? | How much is it? |
| __に行きたいです | _____ nee ee-kee-tai dehs | I want to go to _____ |
| 予約 | Yoh-yah-koo | Reservation |
| 切符 | Kee-ppoo | Ticket |
## Escaped Unicode Test
This is testing Unicode escape sequences: \u3053\u3093\u306b\u3061\u306f (should show as こんにちは)

View File

@ -0,0 +1,74 @@
'use client';
import React, { useEffect, useState } from 'react';
import { MarkdownRenderer } from '@/components/file-renderers/markdown-renderer';
export default function TestUnicodePage() {
const [markdownContent, setMarkdownContent] = useState('');
const [escapedContent, setEscapedContent] = useState('');
useEffect(() => {
// Fetch the test markdown file
fetch('/test-japanese.md')
.then(response => response.text())
.then(text => {
setMarkdownContent(text);
// Create a test string with Unicode escape sequences
const japaneseWithEscapes = "\\u3053\\u3093\\u306b\\u3061\\u306f (Hello)\\n\\u3042\\u308a\\u304c\\u3068\\u3046 (Thank you)";
setEscapedContent(japaneseWithEscapes);
})
.catch(error => console.error('Error loading markdown file:', error));
}, []);
return (
<div className="container mx-auto py-8">
<h1 className="text-2xl font-bold mb-6">Unicode/Japanese Text Testing</h1>
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
<div className="border rounded-md shadow-sm">
<div className="p-4 border-b bg-slate-50 dark:bg-slate-800">
<h2 className="font-medium">Direct Japanese Characters</h2>
</div>
<div className="p-4">
<p className="mb-4 cjk-text">
- Hello<br />
- Good morning<br />
- Thank you<br />
- Test displaying Japanese
</p>
</div>
</div>
<div className="border rounded-md shadow-sm">
<div className="p-4 border-b bg-slate-50 dark:bg-slate-800">
<h2 className="font-medium">Unicode Escape Sequence Test</h2>
</div>
<div className="p-4">
<p className="mb-2">Raw escaped content:</p>
<pre className="bg-slate-100 dark:bg-slate-900 p-2 rounded mb-4 overflow-x-auto">
{escapedContent}
</pre>
<p className="mb-2">Processed content:</p>
<div className="bg-slate-100 dark:bg-slate-900 p-2 rounded">
<MarkdownRenderer content={escapedContent} />
</div>
</div>
</div>
</div>
<div className="mt-8 border rounded-md shadow-sm">
<div className="p-4 border-b bg-slate-50 dark:bg-slate-800">
<h2 className="font-medium">Markdown File with Japanese Characters</h2>
</div>
<div className="p-4">
{markdownContent ? (
<MarkdownRenderer content={markdownContent} />
) : (
<p>Loading markdown content...</p>
)}
</div>
</div>
</div>
);
}

View File

@ -3,7 +3,7 @@
import React, { useState, useRef, useEffect } from "react";
import { cn } from "@/lib/utils";
import { Button } from "@/components/ui/button";
import { ZoomIn, ZoomOut, RotateCw, Download, Maximize2, Minimize2, Info } from "lucide-react";
import { ZoomIn, ZoomOut, RotateCw, Maximize2, Minimize2, Info } from "lucide-react";
interface ImageRendererProps {
url: string;
@ -77,17 +77,6 @@ export function ImageRenderer({ url, className }: ImageRendererProps) {
setRotation(prev => (prev + 90) % 360);
};
// Function for download
const handleDownload = () => {
const link = document.createElement('a');
link.href = url;
const filename = url.split('/').pop() || 'image';
link.download = filename;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
};
// Toggle fit to screen
const toggleFitToScreen = () => {
if (isFitToScreen) {
@ -197,15 +186,6 @@ export function ImageRenderer({ url, className }: ImageRendererProps) {
<Minimize2 className="h-4 w-4" />
)}
</Button>
<Button
variant="ghost"
size="sm"
className="h-8 w-8 p-0"
onClick={handleDownload}
title="Download"
>
<Download className="h-4 w-4" />
</Button>
</div>
</div>

View File

@ -2,12 +2,30 @@
import React, { forwardRef } from "react";
import ReactMarkdown from "react-markdown";
import remarkGfm from "remark-gfm";
import rehypeRaw from "rehype-raw";
import rehypeSanitize from "rehype-sanitize";
import { ScrollArea } from "@/components/ui/scroll-area";
import { cn } from "@/lib/utils";
import { CodeRenderer } from "./code-renderer";
// Process Unicode escape sequences in content
export const processUnicodeContent = (content: string): string => {
if (!content) return '';
// Process \uXXXX Unicode escape sequences (BMP characters)
const bmpProcessed = content.replace(/\\u([0-9a-fA-F]{4})/g, (_, codePoint) => {
return String.fromCharCode(parseInt(codePoint, 16));
});
// Process \uXXXXXXXX Unicode escape sequences (supplementary plane characters)
return bmpProcessed.replace(/\\u([0-9a-fA-F]{8})/g, (_, codePoint) => {
const highSurrogate = parseInt(codePoint.substring(0, 4), 16);
const lowSurrogate = parseInt(codePoint.substring(4, 8), 16);
return String.fromCharCode(highSurrogate, lowSurrogate);
});
};
interface MarkdownRendererProps {
content: string;
className?: string;
@ -15,10 +33,14 @@ interface MarkdownRendererProps {
export const MarkdownRenderer = forwardRef<HTMLDivElement, MarkdownRendererProps>(
({ content, className }, ref) => {
// Process Unicode escape sequences in the content
const processedContent = processUnicodeContent(content);
return (
<ScrollArea className={cn("w-full h-full rounded-md relative", className)}>
<div className="p-4 markdown prose prose-sm dark:prose-invert max-w-none" ref={ref}>
<ReactMarkdown
remarkPlugins={[remarkGfm]}
rehypePlugins={[rehypeRaw, rehypeSanitize]}
components={{
code(props) {
@ -48,7 +70,7 @@ export const MarkdownRenderer = forwardRef<HTMLDivElement, MarkdownRendererProps
h2: ({ node, ...props }) => <h2 className="text-xl font-bold my-3" {...props} />,
h3: ({ node, ...props }) => <h3 className="text-lg font-bold my-2" {...props} />,
a: ({ node, ...props }) => <a className="text-primary hover:underline" {...props} />,
p: ({ node, ...props }) => <p className="my-2" {...props} />,
p: ({ node, ...props }) => <p className="my-2 font-sans cjk-text" {...props} />,
ul: ({ node, ...props }) => <ul className="list-disc pl-5 my-2" {...props} />,
ol: ({ node, ...props }) => <ol className="list-decimal pl-5 my-2" {...props} />,
li: ({ node, ...props }) => <li className="my-1" {...props} />,
@ -59,9 +81,12 @@ export const MarkdownRenderer = forwardRef<HTMLDivElement, MarkdownRendererProps
<img className="max-w-full h-auto rounded-md my-2" {...props} alt={props.alt || ""} />
),
pre: ({ node, ...props }) => <pre className="p-0 my-2 bg-transparent" {...props} />,
table: ({ node, ...props }) => <table className="w-full border-collapse my-3 text-sm" {...props} />,
th: ({ node, ...props }) => <th className="border border-slate-300 dark:border-zinc-700 px-3 py-2 text-left font-semibold bg-slate-100 dark:bg-zinc-800" {...props} />,
td: ({ node, ...props }) => <td className="border border-slate-300 dark:border-zinc-700 px-3 py-2 cjk-text" {...props} />,
}}
>
{content}
{processedContent}
</ReactMarkdown>
</div>
</ScrollArea>

View File

@ -14,6 +14,8 @@ import { useState, useRef, KeyboardEvent } from "react"
import { Input } from "@/components/ui/input"
import { updateProject } from "@/lib/api"
import { Skeleton } from "@/components/ui/skeleton"
import { useIsMobile } from "@/hooks/use-mobile"
import { cn } from "@/lib/utils"
interface ThreadSiteHeaderProps {
threadId: string
@ -22,6 +24,7 @@ interface ThreadSiteHeaderProps {
onViewFiles: () => void
onToggleSidePanel: () => void
onProjectRenamed?: (newName: string) => void
isMobileView?: boolean
}
export function SiteHeader({
@ -30,12 +33,14 @@ export function SiteHeader({
projectName,
onViewFiles,
onToggleSidePanel,
onProjectRenamed
onProjectRenamed,
isMobileView
}: ThreadSiteHeaderProps) {
const pathname = usePathname()
const [isEditing, setIsEditing] = useState(false)
const [editName, setEditName] = useState(projectName)
const inputRef = useRef<HTMLInputElement>(null)
const isMobile = useIsMobile() || isMobileView
const copyCurrentUrl = () => {
const url = window.location.origin + pathname
@ -100,7 +105,10 @@ export function SiteHeader({
}
return (
<header className="bg-background sticky top-0 flex h-14 shrink-0 items-center gap-2 z-20 border-b w-full">
<header className={cn(
"bg-background sticky top-0 flex h-14 shrink-0 items-center gap-2 z-20 border-b w-full",
isMobile && "px-2"
)}>
<div className="flex flex-1 items-center gap-2 px-3">
{isEditing ? (
<div className="flex items-center gap-1">
@ -144,55 +152,69 @@ export function SiteHeader({
</div>
<div className="flex items-center gap-1 pr-4">
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="icon"
onClick={onViewFiles}
className="h-9 w-9 cursor-pointer"
>
<FolderOpen className="h-4 w-4" />
</Button>
</TooltipTrigger>
<TooltipContent>
<p>View Files in Task</p>
</TooltipContent>
</Tooltip>
{isMobile ? (
// Mobile view - only show the side panel toggle
<Button
variant="ghost"
size="icon"
onClick={onToggleSidePanel}
className="h-9 w-9 cursor-pointer"
aria-label="Toggle computer panel"
>
<PanelRightOpen className="h-4 w-4" />
</Button>
) : (
// Desktop view - show all buttons with tooltips
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="icon"
onClick={onViewFiles}
className="h-9 w-9 cursor-pointer"
>
<FolderOpen className="h-4 w-4" />
</Button>
</TooltipTrigger>
<TooltipContent>
<p>View Files in Task</p>
</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="icon"
onClick={copyCurrentUrl}
className="h-9 w-9 cursor-pointer"
>
<Link className="h-4 w-4" />
</Button>
</TooltipTrigger>
<TooltipContent>
<p>Copy Link</p>
</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="icon"
onClick={copyCurrentUrl}
className="h-9 w-9 cursor-pointer"
>
<Link className="h-4 w-4" />
</Button>
</TooltipTrigger>
<TooltipContent>
<p>Copy Link</p>
</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="icon"
onClick={onToggleSidePanel}
className="h-9 w-9 cursor-pointer"
>
<PanelRightOpen className="h-4 w-4" />
</Button>
</TooltipTrigger>
<TooltipContent>
<p>Toggle Computer Preview (CMD+I)</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="icon"
onClick={onToggleSidePanel}
className="h-9 w-9 cursor-pointer"
>
<PanelRightOpen className="h-4 w-4" />
</Button>
</TooltipTrigger>
<TooltipContent>
<p>Toggle Computer Preview (CMD+I)</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
)}
</div>
</header>
)

View File

@ -5,8 +5,10 @@ import { getToolIcon } from "@/components/thread/utils";
import React from "react";
import { Slider } from "@/components/ui/slider";
import { ApiMessageType } from '@/components/thread/types';
import { CircleDashed } from "lucide-react";
import { CircleDashed, X } from "lucide-react";
import { cn } from "@/lib/utils";
import { useIsMobile } from "@/hooks/use-mobile";
import { Button } from "@/components/ui/button";
// Import tool view components from the tool-views directory
import { CommandToolView } from "./tool-views/CommandToolView";
@ -220,6 +222,7 @@ export function ToolCallSidePanel({
const CurrentToolIcon = getToolIcon(currentToolName === 'Tool Call' ? 'unknown' : currentToolName);
const isStreaming = currentToolCall?.toolResult?.content === "STREAMING";
const isSuccess = currentToolCall?.toolResult?.isSuccess ?? true;
const isMobile = useIsMobile();
// Add keyboard shortcut for CMD+I to close panel
React.useEffect(() => {
@ -298,31 +301,41 @@ export function ToolCallSidePanel({
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<h2 className="text-lg font-medium text-zinc-900 dark:text-zinc-100">Suna's Computer</h2>
{/* <div className="h-6 w-6 rounded-full bg-zinc-100 dark:bg-zinc-800 flex items-center justify-center">
<CurrentToolIcon className="h-3.5 w-3.5 text-zinc-800 dark:text-zinc-300" />
</div>
<span className="text-sm text-zinc-700 dark:text-zinc-300">{currentToolName}</span> */}
</div>
{currentToolCall.toolResult?.content && !isStreaming && (
{isMobile && (
<Button
variant="ghost"
size="icon"
onClick={onClose}
className="h-8 w-8 -mr-2"
>
<X className="h-4 w-4" />
</Button>
)}
<div className="flex items-center gap-2">
<div className="h-6 w-6 rounded-full bg-zinc-100 dark:bg-zinc-800 flex items-center justify-center">
<CurrentToolIcon className="h-3.5 w-3.5 text-zinc-800 dark:text-zinc-300" />
</div>
<span className="text-sm text-zinc-700 dark:text-zinc-300">{currentToolName}</span>
<div className={cn(
"px-2.5 py-0.5 rounded-full text-xs font-medium",
isSuccess
? "bg-emerald-50 text-emerald-700 dark:bg-emerald-900/20 dark:text-emerald-400"
: "bg-red-50 text-red-700 dark:bg-red-900/20 dark:text-red-400"
)}>
{isSuccess ? 'Success' : 'Failed'}
{currentToolCall.toolResult?.content && !isStreaming && (
<div className="flex items-center gap-2">
<div className="h-6 w-6 rounded-full bg-zinc-100 dark:bg-zinc-800 flex items-center justify-center">
<CurrentToolIcon className="h-3.5 w-3.5 text-zinc-800 dark:text-zinc-300" />
</div>
<span className={cn(
"text-sm text-zinc-700 dark:text-zinc-300",
isMobile && "hidden sm:inline" // Hide on small mobile
)}>
{currentToolName}
</span>
<div className={cn(
"px-2.5 py-0.5 rounded-full text-xs font-medium",
isSuccess
? "bg-emerald-50 text-emerald-700 dark:bg-emerald-900/20 dark:text-emerald-400"
: "bg-red-50 text-red-700 dark:bg-red-900/20 dark:text-red-400"
)}>
{isSuccess ? 'Success' : 'Failed'}
</div>
</div>
)}
{isStreaming && (
<div className="px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-50 text-blue-700 dark:bg-blue-900/20 dark:text-blue-400 flex items-center gap-1.5">
<CircleDashed className="h-3 w-3 animate-spin" />
@ -339,7 +352,13 @@ export function ToolCallSidePanel({
};
return (
<div className={`fixed inset-y-0 right-0 w-[90%] sm:w-[450px] md:w-[500px] lg:w-[550px] xl:w-[650px] border-l flex flex-col z-10 h-screen transition-all duration-200 ease-in-out ${!isOpen ? 'translate-x-full' : ''}`}>
<div className={cn(
"fixed inset-y-0 right-0 border-l flex flex-col z-10 h-screen transition-all duration-200 ease-in-out",
isMobile
? "w-[90%]"
: "w-[90%] sm:w-[450px] md:w-[500px] lg:w-[550px] xl:w-[650px]",
!isOpen && "translate-x-full"
)}>
<div className="flex-1 flex flex-col overflow-hidden">
{renderContent()}
</div>

View File

@ -5,7 +5,7 @@ import { oneDark, oneLight } from 'react-syntax-highlighter/dist/esm/styles/pris
import { ToolViewProps } from "./types";
import { extractFilePath, extractFileContent, getFileType, formatTimestamp, getToolTitle } from "./utils";
import { GenericToolView } from "./GenericToolView";
import { MarkdownRenderer } from "@/components/file-renderers/markdown-renderer";
import { MarkdownRenderer, processUnicodeContent } from "@/components/file-renderers/markdown-renderer";
import { CsvRenderer } from "@/components/file-renderers/csv-renderer";
import { cn } from "@/lib/utils";
import { useTheme } from "next-themes";
@ -343,7 +343,7 @@ export function FileOperationToolView({
})}
showLineNumbers={false}
>
{fileContent}
{processUnicodeContent(fileContent)}
</SyntaxHighlighter>
</div>
</div>
@ -355,7 +355,7 @@ export function FileOperationToolView({
{idx + 1}
</div>
<div className="table-cell pl-3 py-0.5 text-xs font-mono whitespace-pre text-zinc-800 dark:text-zinc-300">
{line || ' '}
{processUnicodeContent(line) || ' '}
</div>
</div>
))}
@ -381,14 +381,14 @@ export function FileOperationToolView({
{/* Markdown Preview */}
{isMarkdown && viewMode === 'preview' && isSuccess && (
<div className="flex-1 overflow-auto bg-white dark:bg-zinc-950 text-zinc-900 dark:text-zinc-100">
<MarkdownRenderer content={fileContent} />
<MarkdownRenderer content={processUnicodeContent(fileContent)} />
</div>
)}
{/* CSV Preview */}
{isCsv && viewMode === 'preview' && isSuccess && (
<div className="flex-1 overflow-hidden bg-white dark:bg-zinc-950">
<CsvRenderer content={fileContent} />
<CsvRenderer content={processUnicodeContent(fileContent)} />
</div>
)}