mirror of https://github.com/kortix-ai/suna.git
fix: add vnc preload
This commit is contained in:
parent
f203484bb8
commit
4cc8bd173e
|
@ -63,6 +63,7 @@ import { useBillingStatusQuery } from '@/hooks/react-query/threads/use-billing-s
|
|||
import { useSubscription, isPlan } from '@/hooks/react-query/subscriptions/use-subscriptions';
|
||||
import { SubscriptionStatus } from '@/components/thread/chat-input/_use-model-selection';
|
||||
import { ParsedContent } from '@/components/thread/types';
|
||||
import { useVncPreloader } from '@/hooks/useVncPreloader';
|
||||
|
||||
// Extend the base Message type with the expected database fields
|
||||
interface ApiMessageType extends BaseApiMessageType {
|
||||
|
@ -161,6 +162,8 @@ export default function ThreadPage({
|
|||
? 'active'
|
||||
: 'no_subscription';
|
||||
|
||||
// Preload VNC iframe as soon as project data is available
|
||||
useVncPreloader(project);
|
||||
|
||||
const handleProjectRenamed = useCallback((newName: string) => {
|
||||
setProjectName(newName);
|
||||
|
@ -720,7 +723,7 @@ export default function ThreadPage({
|
|||
return assistantMsg.content;
|
||||
}
|
||||
})();
|
||||
|
||||
|
||||
// Try to extract tool name from content
|
||||
const xmlMatch = assistantContent.match(
|
||||
/<([a-zA-Z\-_]+)(?:\s+[^>]*)?>|<([a-zA-Z\-_]+)(?:\s+[^>]*)?\/>/,
|
||||
|
@ -762,7 +765,7 @@ export default function ThreadPage({
|
|||
return resultMessage.content;
|
||||
}
|
||||
})();
|
||||
|
||||
|
||||
// Check for ToolResult pattern first
|
||||
if (toolResultContent && typeof toolResultContent === 'string') {
|
||||
// Look for ToolResult(success=True/False) pattern
|
||||
|
|
|
@ -30,6 +30,7 @@ import { safeJsonParse } from '@/components/thread/utils';
|
|||
import { useAgentStream } from '@/hooks/useAgentStream';
|
||||
import { threadErrorCodeMessages } from '@/lib/constants/errorCodeMessages';
|
||||
import { ThreadSkeleton } from '@/components/thread/content/ThreadSkeleton';
|
||||
import { useVncPreloader } from '@/hooks/useVncPreloader';
|
||||
|
||||
// Extend the base Message type with the expected database fields
|
||||
interface ApiMessageType extends BaseApiMessageType {
|
||||
|
@ -101,6 +102,9 @@ export default function ThreadPage({
|
|||
|
||||
const userClosedPanelRef = useRef(false);
|
||||
|
||||
// Preload VNC iframe as soon as project data is available
|
||||
useVncPreloader(project);
|
||||
|
||||
useEffect(() => {
|
||||
userClosedPanelRef.current = true;
|
||||
setIsSidePanelOpen(false);
|
||||
|
@ -361,9 +365,9 @@ export default function ThreadPage({
|
|||
|
||||
const projectData = threadData?.project_id
|
||||
? await getProject(threadData.project_id).catch((err) => {
|
||||
console.warn('[SHARE] Could not load project data:', err);
|
||||
return null;
|
||||
})
|
||||
console.warn('[SHARE] Could not load project data:', err);
|
||||
return null;
|
||||
})
|
||||
: null;
|
||||
|
||||
if (isMounted) {
|
||||
|
@ -423,7 +427,7 @@ export default function ThreadPage({
|
|||
return assistantMsg.content;
|
||||
}
|
||||
})();
|
||||
|
||||
|
||||
// Try to extract tool name from content
|
||||
const xmlMatch = assistantContent.match(
|
||||
/<([a-zA-Z\-_]+)(?:\s+[^>]*)?>|<([a-zA-Z\-_]+)(?:\s+[^>]*)?\/>/,
|
||||
|
@ -460,7 +464,7 @@ export default function ThreadPage({
|
|||
return resultMessage.content;
|
||||
}
|
||||
})();
|
||||
|
||||
|
||||
// Check for ToolResult pattern first
|
||||
if (toolResultContent && typeof toolResultContent === 'string') {
|
||||
// Look for ToolResult(success=True/False) pattern
|
||||
|
@ -721,7 +725,7 @@ export default function ThreadPage({
|
|||
(m) => m.message_id === assistantId && m.type === 'assistant'
|
||||
);
|
||||
if (!assistantMessage) return false;
|
||||
|
||||
|
||||
// Check if this tool call matches
|
||||
return tc.assistantCall?.content === assistantMessage.content;
|
||||
});
|
||||
|
|
|
@ -0,0 +1,140 @@
|
|||
import { useEffect, useRef, useCallback } from 'react';
|
||||
import { Project } from '@/lib/api';
|
||||
|
||||
export function useVncPreloader(project: Project | null) {
|
||||
const preloadedIframeRef = useRef<HTMLIFrameElement | null>(null);
|
||||
const isPreloadedRef = useRef(false);
|
||||
const retryTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||
const maxRetriesRef = useRef(0);
|
||||
const isRetryingRef = useRef(false);
|
||||
|
||||
const startPreloading = useCallback((vncUrl: string) => {
|
||||
// Prevent multiple simultaneous preload attempts
|
||||
if (isRetryingRef.current || isPreloadedRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
isRetryingRef.current = true;
|
||||
console.log(`[VNC PRELOADER] Attempt ${maxRetriesRef.current + 1}/10 - Starting VNC preload:`, vncUrl);
|
||||
|
||||
// Create hidden iframe for preloading
|
||||
const iframe = document.createElement('iframe');
|
||||
iframe.src = vncUrl;
|
||||
iframe.style.position = 'absolute';
|
||||
iframe.style.left = '-9999px';
|
||||
iframe.style.top = '-9999px';
|
||||
iframe.style.width = '1024px';
|
||||
iframe.style.height = '768px';
|
||||
iframe.style.border = '0';
|
||||
iframe.title = 'VNC Preloader';
|
||||
|
||||
// Set a timeout to detect if iframe fails to load (for 502 errors)
|
||||
const loadTimeout = setTimeout(() => {
|
||||
console.log('[VNC PRELOADER] Load timeout - VNC service likely not ready');
|
||||
|
||||
// Clean up current iframe
|
||||
if (iframe.parentNode) {
|
||||
iframe.parentNode.removeChild(iframe);
|
||||
}
|
||||
|
||||
// Retry if we haven't exceeded max retries
|
||||
if (maxRetriesRef.current < 10) {
|
||||
maxRetriesRef.current++;
|
||||
isRetryingRef.current = false;
|
||||
|
||||
// Exponential backoff: 2s, 3s, 4.5s, 6.75s, etc. (max 15s)
|
||||
const delay = Math.min(2000 * Math.pow(1.5, maxRetriesRef.current - 1), 15000);
|
||||
console.log(`[VNC PRELOADER] Retrying in ${delay}ms (attempt ${maxRetriesRef.current + 1}/10)`);
|
||||
|
||||
retryTimeoutRef.current = setTimeout(() => {
|
||||
startPreloading(vncUrl);
|
||||
}, delay);
|
||||
} else {
|
||||
console.log('[VNC PRELOADER] Max retries reached, giving up on preloading');
|
||||
isRetryingRef.current = false;
|
||||
}
|
||||
}, 5000); // 5 second timeout
|
||||
|
||||
// Handle successful iframe load
|
||||
iframe.onload = () => {
|
||||
clearTimeout(loadTimeout);
|
||||
console.log('[VNC PRELOADER] ✅ VNC iframe preloaded successfully!');
|
||||
isPreloadedRef.current = true;
|
||||
isRetryingRef.current = false;
|
||||
preloadedIframeRef.current = iframe;
|
||||
};
|
||||
|
||||
// Handle iframe load errors
|
||||
iframe.onerror = () => {
|
||||
clearTimeout(loadTimeout);
|
||||
console.log('[VNC PRELOADER] VNC iframe failed to load (onerror)');
|
||||
|
||||
// Clean up current iframe
|
||||
if (iframe.parentNode) {
|
||||
iframe.parentNode.removeChild(iframe);
|
||||
}
|
||||
|
||||
// Retry if we haven't exceeded max retries
|
||||
if (maxRetriesRef.current < 10) {
|
||||
maxRetriesRef.current++;
|
||||
isRetryingRef.current = false;
|
||||
|
||||
const delay = Math.min(2000 * Math.pow(1.5, maxRetriesRef.current - 1), 15000);
|
||||
console.log(`[VNC PRELOADER] Retrying in ${delay}ms (attempt ${maxRetriesRef.current + 1}/10)`);
|
||||
|
||||
retryTimeoutRef.current = setTimeout(() => {
|
||||
startPreloading(vncUrl);
|
||||
}, delay);
|
||||
} else {
|
||||
console.log('[VNC PRELOADER] Max retries reached, giving up on preloading');
|
||||
isRetryingRef.current = false;
|
||||
}
|
||||
};
|
||||
|
||||
// Add to DOM to start loading
|
||||
document.body.appendChild(iframe);
|
||||
console.log('[VNC PRELOADER] VNC iframe added to DOM, waiting for load...');
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
// Only preload if we have project data with VNC info and haven't started preloading yet
|
||||
if (!project?.sandbox?.vnc_preview || !project?.sandbox?.pass || isPreloadedRef.current || isRetryingRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
const vncUrl = `${project.sandbox.vnc_preview}/vnc_lite.html?password=${project.sandbox.pass}&autoconnect=true&scale=local&width=1024&height=768`;
|
||||
|
||||
// Reset retry counter for new project
|
||||
maxRetriesRef.current = 0;
|
||||
isRetryingRef.current = false;
|
||||
|
||||
// Start the preloading process with a small delay to let the sandbox initialize
|
||||
const initialDelay = setTimeout(() => {
|
||||
startPreloading(vncUrl);
|
||||
}, 1000); // 1 second initial delay
|
||||
|
||||
// Cleanup function
|
||||
return () => {
|
||||
clearTimeout(initialDelay);
|
||||
|
||||
if (retryTimeoutRef.current) {
|
||||
clearTimeout(retryTimeoutRef.current);
|
||||
retryTimeoutRef.current = null;
|
||||
}
|
||||
|
||||
if (preloadedIframeRef.current && preloadedIframeRef.current.parentNode) {
|
||||
preloadedIframeRef.current.parentNode.removeChild(preloadedIframeRef.current);
|
||||
preloadedIframeRef.current = null;
|
||||
}
|
||||
|
||||
isPreloadedRef.current = false;
|
||||
isRetryingRef.current = false;
|
||||
maxRetriesRef.current = 0;
|
||||
};
|
||||
}, [project?.sandbox?.vnc_preview, project?.sandbox?.pass, startPreloading]);
|
||||
|
||||
return {
|
||||
isPreloaded: isPreloadedRef.current,
|
||||
preloadedIframe: preloadedIframeRef.current
|
||||
};
|
||||
}
|
Loading…
Reference in New Issue