mirror of https://github.com/kortix-ai/suna.git
fix: overflow issue and file refetch
This commit is contained in:
parent
c14a2e4cb4
commit
65fa837ae1
|
@ -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 */
|
||||
|
|
|
@ -262,7 +262,7 @@ export const ThreadContent: React.FC<ThreadContentProps> = ({
|
|||
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}
|
||||
>
|
||||
<div className="mx-auto max-w-3xl">
|
||||
<div className="mx-auto max-w-3xl min-w-0">
|
||||
{displayMessages.length === 0 && !streamingTextContent && !streamingToolCall &&
|
||||
!streamingText && !currentToolCall && agentStatus === 'idle' ? (
|
||||
<div className="flex h-full items-center justify-center">
|
||||
|
@ -271,7 +271,7 @@ export const ThreadContent: React.FC<ThreadContentProps> = ({
|
|||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-8">
|
||||
<div className="space-y-8 min-w-0">
|
||||
{(() => {
|
||||
|
||||
type MessageGroup = {
|
||||
|
@ -379,8 +379,8 @@ export const ThreadContent: React.FC<ThreadContentProps> = ({
|
|||
if (debugMode) {
|
||||
return (
|
||||
<div key={group.key} className="flex justify-end">
|
||||
<div className="inline-flex max-w-[85%] rounded-xl bg-primary/10 px-4 py-3">
|
||||
<pre className="text-xs font-mono whitespace-pre-wrap overflow-x-auto">
|
||||
<div className="flex max-w-[85%] rounded-xl bg-primary/10 px-4 py-3 break-words overflow-hidden">
|
||||
<pre className="text-xs font-mono whitespace-pre-wrap overflow-x-auto min-w-0 flex-1">
|
||||
{message.content}
|
||||
</pre>
|
||||
</div>
|
||||
|
@ -402,10 +402,10 @@ export const ThreadContent: React.FC<ThreadContentProps> = ({
|
|||
|
||||
return (
|
||||
<div key={group.key} className="flex justify-end">
|
||||
<div className="inline-flex max-w-[85%] rounded-xl bg-primary/10 px-4 py-3">
|
||||
<div className="space-y-3">
|
||||
<div className="flex max-w-[85%] rounded-xl bg-primary/10 px-4 py-3 break-words overflow-hidden">
|
||||
<div className="space-y-3 min-w-0 flex-1">
|
||||
{cleanContent && (
|
||||
<Markdown className="text-sm prose prose-sm dark:prose-invert chat-markdown max-w-none [&>:first-child]:mt-0 prose-headings:mt-3">{cleanContent}</Markdown>
|
||||
<Markdown className="text-sm prose prose-sm dark:prose-invert chat-markdown max-w-none [&>:first-child]:mt-0 prose-headings:mt-3 break-words overflow-wrap-anywhere">{cleanContent}</Markdown>
|
||||
)}
|
||||
|
||||
{/* Use the helper function to render user attachments */}
|
||||
|
@ -422,8 +422,8 @@ export const ThreadContent: React.FC<ThreadContentProps> = ({
|
|||
<KortixLogo />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<div className="inline-flex max-w-[90%] rounded-lg px-4 text-sm">
|
||||
<div className="space-y-2">
|
||||
<div className="flex max-w-[90%] rounded-lg px-4 text-sm break-words overflow-hidden">
|
||||
<div className="space-y-2 min-w-0 flex-1">
|
||||
{(() => {
|
||||
// In debug mode, just show raw messages content
|
||||
if (debugMode) {
|
||||
|
@ -487,7 +487,7 @@ export const ThreadContent: React.FC<ThreadContentProps> = ({
|
|||
|
||||
elements.push(
|
||||
<div key={msgKey} className={assistantMessageCount > 0 ? "mt-2" : ""}>
|
||||
<div className="prose prose-sm dark:prose-invert chat-markdown max-w-none [&>:first-child]:mt-0 prose-headings:mt-3">
|
||||
<div className="prose prose-sm dark:prose-invert chat-markdown max-w-none [&>:first-child]:mt-0 prose-headings:mt-3 break-words overflow-hidden">
|
||||
{renderedContent}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -534,7 +534,7 @@ export const ThreadContent: React.FC<ThreadContentProps> = ({
|
|||
return (
|
||||
<>
|
||||
{textBeforeTag && (
|
||||
<Markdown className="text-sm prose prose-sm dark:prose-invert chat-markdown max-w-none [&>:first-child]:mt-0 prose-headings:mt-3">{textBeforeTag}</Markdown>
|
||||
<Markdown className="text-sm prose prose-sm dark:prose-invert chat-markdown max-w-none [&>:first-child]:mt-0 prose-headings:mt-3 break-words overflow-wrap-anywhere">{textBeforeTag}</Markdown>
|
||||
)}
|
||||
{showCursor && (
|
||||
<span className="inline-block h-4 w-0.5 bg-primary ml-0.5 -mb-1 animate-pulse" />
|
||||
|
@ -611,7 +611,7 @@ export const ThreadContent: React.FC<ThreadContentProps> = ({
|
|||
) : (
|
||||
<>
|
||||
{textBeforeTag && (
|
||||
<Markdown className="text-sm prose prose-sm dark:prose-invert chat-markdown max-w-none [&>:first-child]:mt-0 prose-headings:mt-3">{textBeforeTag}</Markdown>
|
||||
<Markdown className="text-sm prose prose-sm dark:prose-invert chat-markdown max-w-none [&>:first-child]:mt-0 prose-headings:mt-3 break-words overflow-wrap-anywhere">{textBeforeTag}</Markdown>
|
||||
)}
|
||||
{showCursor && (
|
||||
<span className="inline-block h-4 w-0.5 bg-primary ml-0.5 -mb-1 animate-pulse" />
|
||||
|
|
|
@ -12,7 +12,7 @@ export type CodeBlockProps = {
|
|||
|
||||
function CodeBlock({ children, className, ...props }: CodeBlockProps) {
|
||||
return (
|
||||
<div className={cn(className)} {...props}>
|
||||
<div className={cn('w-px flex-grow min-w-0 overflow-hidden flex', className)} {...props}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
|
@ -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({
|
|||
/>
|
||||
) : (
|
||||
<div className={classNames} {...props}>
|
||||
<pre>
|
||||
<pre className="!overflow-x-auto !w-px !flex-grow !min-w-0 !box-border">
|
||||
<code>{code}</code>
|
||||
</pre>
|
||||
</div>
|
||||
|
|
|
@ -47,11 +47,11 @@ const INITIAL_COMPONENTS: Partial<Components> = {
|
|||
const language = extractLanguage(className);
|
||||
|
||||
return (
|
||||
<CodeBlock className="rounded-md overflow-hidden my-4 border border-zinc-200 dark:border-zinc-800">
|
||||
<CodeBlock className="rounded-md overflow-hidden my-4 border border-zinc-200 dark:border-zinc-800 max-w-full min-w-0 w-full">
|
||||
<CodeBlockCode
|
||||
code={children as string}
|
||||
language={language}
|
||||
className="text-sm overflow-x-auto"
|
||||
className="text-sm"
|
||||
/>
|
||||
</CodeBlock>
|
||||
);
|
||||
|
|
|
@ -128,15 +128,33 @@ export function useCachedFile<T = string>(
|
|||
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<Response> => {
|
||||
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<Response> => {
|
||||
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<Response> => {
|
||||
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<string | Blob | any> => {
|
||||
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;
|
||||
|
|
Loading…
Reference in New Issue