diff --git a/frontend/src/app/globals.css b/frontend/src/app/globals.css index 649324fb..c2cf2118 100644 --- a/frontend/src/app/globals.css +++ b/frontend/src/app/globals.css @@ -417,7 +417,8 @@ margin-top: 0.5em; margin-bottom: 0.5em; padding: 0.75em 1em; - background-color: theme('colors.slate.100'); + /* background 95 */ + background-color: theme('colors.background/95'); border-radius: 0.375rem; overflow-x: auto; font-family: var(--font-mono); @@ -439,7 +440,7 @@ padding: 0.2em 0.4em; font-size: 0.85em; font-family: var(--font-mono); - background-color: theme('colors.slate.100'); + background-color: theme('colors.background/95'); border-radius: 3px; word-break: break-word; } @@ -478,14 +479,14 @@ .dark & { /* Code blocks in dark mode */ & pre { - background-color: theme('colors.zinc.800'); - border: 1px solid theme('colors.zinc.700'); + background-color: theme('colors.background/95'); + /* border: 1px solid theme('colors.zinc.700'); */ } & code:not([class*='language-']) { - background-color: theme('colors.zinc.800'); + background-color: theme('colors.background/95'); color: theme('colors.zinc.200'); - border: 1px solid theme('colors.zinc.700'); + /* border: 1px solid theme('colors.zinc.700'); */ } /* Tables in dark mode */ diff --git a/frontend/src/components/thread/content/ThreadContent.tsx b/frontend/src/components/thread/content/ThreadContent.tsx index 594cfad5..2ef0a6b8 100644 --- a/frontend/src/components/thread/content/ThreadContent.tsx +++ b/frontend/src/components/thread/content/ThreadContent.tsx @@ -262,7 +262,7 @@ export const ThreadContent: React.FC = ({ className="flex-1 overflow-y-auto scrollbar-thin scrollbar-track-secondary/0 scrollbar-thumb-primary/10 scrollbar-thumb-rounded-full hover:scrollbar-thumb-primary/10 px-6 py-4 pb-72 bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60" onScroll={handleScroll} > -
+
{displayMessages.length === 0 && !streamingTextContent && !streamingToolCall && !streamingText && !currentToolCall && agentStatus === 'idle' ? (
@@ -271,7 +271,7 @@ export const ThreadContent: React.FC = ({
) : ( -
+
{(() => { type MessageGroup = { @@ -379,8 +379,8 @@ export const ThreadContent: React.FC = ({ if (debugMode) { return (
-
-
+                                                    
+
                                                             {message.content}
                                                         
@@ -402,10 +402,10 @@ export const ThreadContent: React.FC = ({ return (
-
-
+
+
{cleanContent && ( - {cleanContent} + {cleanContent} )} {/* Use the helper function to render user attachments */} @@ -422,8 +422,8 @@ export const ThreadContent: React.FC = ({
-
-
+
+
{(() => { // In debug mode, just show raw messages content if (debugMode) { @@ -487,7 +487,7 @@ export const ThreadContent: React.FC = ({ elements.push(
0 ? "mt-2" : ""}> -
+
{renderedContent}
@@ -534,7 +534,7 @@ export const ThreadContent: React.FC = ({ return ( <> {textBeforeTag && ( - {textBeforeTag} + {textBeforeTag} )} {showCursor && ( @@ -611,7 +611,7 @@ export const ThreadContent: React.FC = ({ ) : ( <> {textBeforeTag && ( - {textBeforeTag} + {textBeforeTag} )} {showCursor && ( diff --git a/frontend/src/components/ui/code-block.tsx b/frontend/src/components/ui/code-block.tsx index 3d33a9b2..2dc3b09c 100644 --- a/frontend/src/components/ui/code-block.tsx +++ b/frontend/src/components/ui/code-block.tsx @@ -12,7 +12,7 @@ export type CodeBlockProps = { function CodeBlock({ children, className, ...props }: CodeBlockProps) { return ( -
+
{children}
); @@ -41,6 +41,10 @@ function CodeBlockCode({ useEffect(() => { async function highlight() { + if (!code || typeof code !== 'string') { + setHighlightedHtml(null); + return; + } const html = await codeToHtml(code, { lang: language, theme, @@ -60,7 +64,7 @@ function CodeBlockCode({ highlight(); }, [code, language, theme]); - const classNames = cn('[&_pre]:!bg-background/95 [&_pre]:rounded-lg [&_pre]:p-4', className); + const classNames = cn('[&_pre]:!bg-background/95 [&_pre]:rounded-lg [&_pre]:p-4 [&_pre]:!overflow-x-auto [&_pre]:!w-px [&_pre]:!flex-grow [&_pre]:!min-w-0 [&_pre]:!box-border [&_.shiki]:!overflow-x-auto [&_.shiki]:!w-px [&_.shiki]:!flex-grow [&_.shiki]:!min-w-0 [&_code]:!min-w-0 [&_code]:!whitespace-pre', 'w-px flex-grow min-w-0 overflow-hidden flex w-full', className); // SSR fallback: render plain code if not hydrated yet return highlightedHtml ? ( @@ -71,7 +75,7 @@ function CodeBlockCode({ /> ) : (
-
+      
         {code}
       
diff --git a/frontend/src/components/ui/markdown.tsx b/frontend/src/components/ui/markdown.tsx index 92586d7e..825deca5 100644 --- a/frontend/src/components/ui/markdown.tsx +++ b/frontend/src/components/ui/markdown.tsx @@ -47,11 +47,11 @@ const INITIAL_COMPONENTS: Partial = { const language = extractLanguage(className); return ( - + ); diff --git a/frontend/src/hooks/use-cached-file.ts b/frontend/src/hooks/use-cached-file.ts index 3d4ab7d1..3d5faf08 100644 --- a/frontend/src/hooks/use-cached-file.ts +++ b/frontend/src/hooks/use-cached-file.ts @@ -128,15 +128,33 @@ export function useCachedFile( url.searchParams.append('path', normalizedPath); // Fetch with authentication - const response = await fetch(url.toString(), { - headers: { - 'Authorization': `Bearer ${session?.access_token}`, - }, - }); + const attemptFetch = async (isRetry: boolean = false): Promise => { + const response = await fetch(url.toString(), { + headers: { + 'Authorization': `Bearer ${session?.access_token}` + } + }); + + if (!response.ok) { + const responseText = await response.text(); + const errorMessage = `Failed to load file: ${response.status} ${response.statusText}`; + + // Check if this is a workspace initialization error and we haven't retried yet + const isWorkspaceNotRunning = responseText.includes('Workspace is not running'); + if (isWorkspaceNotRunning && !isRetry) { + console.log(`[FILE CACHE] Workspace not ready, retrying in 2s for ${normalizedPath}`); + await new Promise(resolve => setTimeout(resolve, 2000)); + return attemptFetch(true); + } + + console.error(`[FILE CACHE] Failed response for ${normalizedPath}: Status ${response.status}`); + throw new Error(errorMessage); + } + + return response; + }; - if (!response.ok) { - throw new Error(`Failed to load file: ${response.status} ${response.statusText}`); - } + const response = await attemptFetch(); // Process content based on contentType let content; @@ -502,13 +520,32 @@ export const FileCache = { // Properly encode the path parameter for UTF-8 support url.searchParams.append('path', normalizedPath); - const response = await fetch(url.toString(), { - headers: { - 'Authorization': `Bearer ${token}` - }, - }); + const attemptFetch = async (isRetry: boolean = false): Promise => { + const response = await fetch(url.toString(), { + headers: { + 'Authorization': `Bearer ${token}` + }, + }); + + if (!response.ok) { + const responseText = await response.text(); + const errorMessage = `Failed to preload file: ${response.status}`; + + // Check if this is a workspace initialization error and we haven't retried yet + const isWorkspaceNotRunning = responseText.includes('Workspace is not running'); + if (isWorkspaceNotRunning && !isRetry) { + console.log(`[FILE CACHE] Workspace not ready during preload, retrying in 2s for ${normalizedPath}`); + await new Promise(resolve => setTimeout(resolve, 2000)); + return attemptFetch(true); + } + + throw new Error(errorMessage); + } + + return response; + }; - if (!response.ok) throw new Error(`Failed to preload file: ${response.status}`); + const response = await attemptFetch(); // Determine how to process the content based on file type const extension = path.split('.').pop()?.toLowerCase(); @@ -623,16 +660,33 @@ export async function getCachedFile( console.log(`[FILE CACHE] Fetching file: ${url.toString()}`); - const response = await fetch(url.toString(), { - headers: { - 'Authorization': `Bearer ${options.token}` + const attemptFetch = async (isRetry: boolean = false): Promise => { + const response = await fetch(url.toString(), { + headers: { + 'Authorization': `Bearer ${options.token}` + } + }); + + if (!response.ok) { + const responseText = await response.text(); + const errorMessage = `Failed to load file: ${response.status} ${response.statusText}`; + + // Check if this is a workspace initialization error and we haven't retried yet + const isWorkspaceNotRunning = responseText.includes('Workspace is not running'); + if (isWorkspaceNotRunning && !isRetry) { + console.log(`[FILE CACHE] Workspace not ready, retrying in 2s for ${normalizedPath}`); + await new Promise(resolve => setTimeout(resolve, 2000)); + return attemptFetch(true); + } + + console.error(`[FILE CACHE] Failed response for ${normalizedPath}: Status ${response.status}`); + throw new Error(errorMessage); } - }); + + return response; + }; - if (!response.ok) { - console.error(`[FILE CACHE] Failed response for ${normalizedPath}: Status ${response.status}`); - throw new Error(`Failed to load file: ${response.status} ${response.statusText}`); - } + const response = await attemptFetch(); // Process content based on type let content; @@ -688,51 +742,66 @@ export async function fetchFileContent( const requestId = Math.random().toString(36).substring(2, 9); console.log(`[FILE CACHE] Fetching fresh content for ${sandboxId}:${filePath}`); - try { - // Prepare the API URL - const apiUrl = `${process.env.NEXT_PUBLIC_BACKEND_URL}/sandboxes/${sandboxId}/files/content`; - const url = new URL(apiUrl); - url.searchParams.append('path', filePath); - - // Set up fetch options - const fetchOptions: RequestInit = { - method: 'GET', - headers: { - Authorization: `Bearer ${token}`, - }, - }; - - // Execute fetch - const response = await fetch(url.toString(), fetchOptions); - if (!response.ok) { - const errorText = await response.text(); - throw new Error(`Failed to fetch file content: ${response.status} ${errorText}`); - } - - // CRITICAL: Detect correct response handling based on file type - // Excel files, PDFs and other binary documents should be handled as blobs - const extension = filePath.split('.').pop()?.toLowerCase(); - const isBinaryFile = ['xlsx', 'xls', 'docx', 'doc', 'pptx', 'ppt', 'pdf', 'png', 'jpg', 'jpeg', 'gif', 'zip'].includes(extension || ''); - - // Handle response based on content type - if (contentType === 'blob' || isBinaryFile) { - const blob = await response.blob(); + const attemptFetch = async (isRetry: boolean = false): Promise => { + try { + // Prepare the API URL + const apiUrl = `${process.env.NEXT_PUBLIC_BACKEND_URL}/sandboxes/${sandboxId}/files/content`; + const url = new URL(apiUrl); + url.searchParams.append('path', filePath); - // Set correct MIME type for known file types - if (extension) { - const mimeType = FileCache.getMimeType(filePath); - if (mimeType && mimeType !== blob.type) { - // Create a new blob with correct type - return new Blob([blob], { type: mimeType }); - } + // Set up fetch options + const fetchOptions: RequestInit = { + method: 'GET', + headers: { + Authorization: `Bearer ${token}`, + }, + }; + + // Execute fetch + const response = await fetch(url.toString(), fetchOptions); + if (!response.ok) { + const errorText = await response.text(); + throw new Error(`Failed to fetch file content: ${response.status} ${errorText}`); } - return blob; - } else if (contentType === 'json') { - return await response.json(); - } else { - return await response.text(); + // CRITICAL: Detect correct response handling based on file type + // Excel files, PDFs and other binary documents should be handled as blobs + const extension = filePath.split('.').pop()?.toLowerCase(); + const isBinaryFile = ['xlsx', 'xls', 'docx', 'doc', 'pptx', 'ppt', 'pdf', 'png', 'jpg', 'jpeg', 'gif', 'zip'].includes(extension || ''); + + // Handle response based on content type + if (contentType === 'blob' || isBinaryFile) { + const blob = await response.blob(); + + // Set correct MIME type for known file types + if (extension) { + const mimeType = FileCache.getMimeType(filePath); + if (mimeType && mimeType !== blob.type) { + // Create a new blob with correct type + return new Blob([blob], { type: mimeType }); + } + } + + return blob; + } else if (contentType === 'json') { + return await response.json(); + } else { + return await response.text(); + } + } catch (error: any) { + // Check if this is a workspace initialization error and we haven't retried yet + const isWorkspaceNotRunning = error.message?.includes('Workspace is not running'); + if (isWorkspaceNotRunning && !isRetry) { + console.log(`[FILE CACHE] Workspace not ready, retrying in 2s for ${filePath}`); + await new Promise(resolve => setTimeout(resolve, 2000)); + return attemptFetch(true); + } + throw error; } + }; + + try { + return await attemptFetch(); } catch (error) { console.error(`[FILE CACHE] Error fetching file content:`, error); throw error;