mirror of https://github.com/kortix-ai/suna.git
Merge pull request #583 from kubet/feat/port-fix-and-panel-ajustments
Feat/port fix and panel ajustments
This commit is contained in:
commit
312b2f1e71
|
@ -138,11 +138,17 @@ class SandboxFilesTool(SandboxToolsBase):
|
||||||
self.sandbox.fs.upload_file(full_path, file_contents.encode())
|
self.sandbox.fs.upload_file(full_path, file_contents.encode())
|
||||||
self.sandbox.fs.set_file_permissions(full_path, permissions)
|
self.sandbox.fs.set_file_permissions(full_path, permissions)
|
||||||
|
|
||||||
# Get preview URL if it's an HTML file
|
|
||||||
# preview_url = self._get_preview_url(file_path)
|
|
||||||
message = f"File '{file_path}' created successfully."
|
message = f"File '{file_path}' created successfully."
|
||||||
# if preview_url:
|
|
||||||
# message += f"\n\nYou can preview this HTML file at the automatically served HTTP server: {preview_url}"
|
# Check if index.html was created and add 8080 server info (only in root workspace)
|
||||||
|
if file_path.lower() == 'index.html':
|
||||||
|
try:
|
||||||
|
website_link = self.sandbox.get_preview_link(8080)
|
||||||
|
website_url = website_link.url if hasattr(website_link, 'url') else str(website_link).split("url='")[1].split("'")[0]
|
||||||
|
message += f"\n\n[Auto-detected index.html - HTTP server available at: {website_url}]"
|
||||||
|
message += "\n[Note: Use the provided HTTP server URL above instead of starting a new server]"
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Failed to get website URL for index.html: {str(e)}")
|
||||||
|
|
||||||
return self.success_response(message)
|
return self.success_response(message)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
@ -291,11 +297,17 @@ class SandboxFilesTool(SandboxToolsBase):
|
||||||
self.sandbox.fs.upload_file(full_path, file_contents.encode())
|
self.sandbox.fs.upload_file(full_path, file_contents.encode())
|
||||||
self.sandbox.fs.set_file_permissions(full_path, permissions)
|
self.sandbox.fs.set_file_permissions(full_path, permissions)
|
||||||
|
|
||||||
# Get preview URL if it's an HTML file
|
|
||||||
# preview_url = self._get_preview_url(file_path)
|
|
||||||
message = f"File '{file_path}' completely rewritten successfully."
|
message = f"File '{file_path}' completely rewritten successfully."
|
||||||
# if preview_url:
|
|
||||||
# message += f"\n\nYou can preview this HTML file at: {preview_url}"
|
# Check if index.html was rewritten and add 8080 server info (only in root workspace)
|
||||||
|
if file_path.lower() == 'index.html':
|
||||||
|
try:
|
||||||
|
website_link = self.sandbox.get_preview_link(8080)
|
||||||
|
website_url = website_link.url if hasattr(website_link, 'url') else str(website_link).split("url='")[1].split("'")[0]
|
||||||
|
message += f"\n\n[Auto-detected index.html - HTTP server available at: {website_url}]"
|
||||||
|
message += "\n[Note: Use the provided HTTP server URL above instead of starting a new server]"
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Failed to get website URL for index.html: {str(e)}")
|
||||||
|
|
||||||
return self.success_response(message)
|
return self.success_response(message)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|
|
@ -5,6 +5,7 @@ import React, {
|
||||||
useEffect,
|
useEffect,
|
||||||
useRef,
|
useRef,
|
||||||
useState,
|
useState,
|
||||||
|
useMemo,
|
||||||
} from 'react';
|
} from 'react';
|
||||||
import { useSearchParams } from 'next/navigation';
|
import { useSearchParams } from 'next/navigation';
|
||||||
import { BillingError } from '@/lib/api';
|
import { BillingError } from '@/lib/api';
|
||||||
|
@ -127,7 +128,10 @@ export default function ThreadPage({
|
||||||
? 'active'
|
? 'active'
|
||||||
: 'no_subscription';
|
: 'no_subscription';
|
||||||
|
|
||||||
useVncPreloader(project);
|
// Memoize project for VNC preloader to prevent re-preloading on every render
|
||||||
|
const memoizedProject = useMemo(() => project, [project?.id, project?.sandbox?.vnc_preview, project?.sandbox?.pass]);
|
||||||
|
|
||||||
|
useVncPreloader(memoizedProject);
|
||||||
|
|
||||||
|
|
||||||
const handleProjectRenamed = useCallback((newName: string) => {
|
const handleProjectRenamed = useCallback((newName: string) => {
|
||||||
|
|
|
@ -75,7 +75,7 @@ export function ToolCallSidePanel({
|
||||||
const [navigationMode, setNavigationMode] = React.useState<'live' | 'manual'>('live');
|
const [navigationMode, setNavigationMode] = React.useState<'live' | 'manual'>('live');
|
||||||
const [toolCallSnapshots, setToolCallSnapshots] = React.useState<ToolCallSnapshot[]>([]);
|
const [toolCallSnapshots, setToolCallSnapshots] = React.useState<ToolCallSnapshot[]>([]);
|
||||||
const [isInitialized, setIsInitialized] = React.useState(false);
|
const [isInitialized, setIsInitialized] = React.useState(false);
|
||||||
|
|
||||||
const isMobile = useIsMobile();
|
const isMobile = useIsMobile();
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
|
@ -85,23 +85,23 @@ export function ToolCallSidePanel({
|
||||||
index,
|
index,
|
||||||
timestamp: Date.now(),
|
timestamp: Date.now(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const hadSnapshots = toolCallSnapshots.length > 0;
|
const hadSnapshots = toolCallSnapshots.length > 0;
|
||||||
const hasNewSnapshots = newSnapshots.length > toolCallSnapshots.length;
|
const hasNewSnapshots = newSnapshots.length > toolCallSnapshots.length;
|
||||||
setToolCallSnapshots(newSnapshots);
|
setToolCallSnapshots(newSnapshots);
|
||||||
|
|
||||||
if (!isInitialized && newSnapshots.length > 0) {
|
if (!isInitialized && newSnapshots.length > 0) {
|
||||||
const completedCount = newSnapshots.filter(s =>
|
const completedCount = newSnapshots.filter(s =>
|
||||||
s.toolCall.toolResult?.content &&
|
s.toolCall.toolResult?.content &&
|
||||||
s.toolCall.toolResult.content !== 'STREAMING'
|
s.toolCall.toolResult.content !== 'STREAMING'
|
||||||
).length;
|
).length;
|
||||||
|
|
||||||
if (completedCount > 0) {
|
if (completedCount > 0) {
|
||||||
let lastCompletedIndex = -1;
|
let lastCompletedIndex = -1;
|
||||||
for (let i = newSnapshots.length - 1; i >= 0; i--) {
|
for (let i = newSnapshots.length - 1; i >= 0; i--) {
|
||||||
const snapshot = newSnapshots[i];
|
const snapshot = newSnapshots[i];
|
||||||
if (snapshot.toolCall.toolResult?.content &&
|
if (snapshot.toolCall.toolResult?.content &&
|
||||||
snapshot.toolCall.toolResult.content !== 'STREAMING') {
|
snapshot.toolCall.toolResult.content !== 'STREAMING') {
|
||||||
lastCompletedIndex = i;
|
lastCompletedIndex = i;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -115,21 +115,21 @@ export function ToolCallSidePanel({
|
||||||
const latestSnapshot = newSnapshots[newSnapshots.length - 1];
|
const latestSnapshot = newSnapshots[newSnapshots.length - 1];
|
||||||
const isLatestStreaming = latestSnapshot?.toolCall.toolResult?.content === 'STREAMING';
|
const isLatestStreaming = latestSnapshot?.toolCall.toolResult?.content === 'STREAMING';
|
||||||
if (isLatestStreaming) {
|
if (isLatestStreaming) {
|
||||||
let lastCompletedIndex = -1;
|
let lastCompletedIndex = -1;
|
||||||
for (let i = newSnapshots.length - 1; i >= 0; i--) {
|
for (let i = newSnapshots.length - 1; i >= 0; i--) {
|
||||||
const snapshot = newSnapshots[i];
|
const snapshot = newSnapshots[i];
|
||||||
if (snapshot.toolCall.toolResult?.content &&
|
if (snapshot.toolCall.toolResult?.content &&
|
||||||
snapshot.toolCall.toolResult.content !== 'STREAMING') {
|
snapshot.toolCall.toolResult.content !== 'STREAMING') {
|
||||||
lastCompletedIndex = i;
|
lastCompletedIndex = i;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (lastCompletedIndex >= 0) {
|
if (lastCompletedIndex >= 0) {
|
||||||
setInternalIndex(lastCompletedIndex);
|
setInternalIndex(lastCompletedIndex);
|
||||||
} else {
|
} else {
|
||||||
setInternalIndex(newSnapshots.length - 1);
|
setInternalIndex(newSnapshots.length - 1);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
setInternalIndex(newSnapshots.length - 1);
|
setInternalIndex(newSnapshots.length - 1);
|
||||||
}
|
}
|
||||||
} else if (hasNewSnapshots && navigationMode === 'manual') {
|
} else if (hasNewSnapshots && navigationMode === 'manual') {
|
||||||
|
@ -146,11 +146,11 @@ export function ToolCallSidePanel({
|
||||||
const currentSnapshot = toolCallSnapshots[safeInternalIndex];
|
const currentSnapshot = toolCallSnapshots[safeInternalIndex];
|
||||||
const currentToolCall = currentSnapshot?.toolCall;
|
const currentToolCall = currentSnapshot?.toolCall;
|
||||||
const totalCalls = toolCallSnapshots.length;
|
const totalCalls = toolCallSnapshots.length;
|
||||||
|
|
||||||
// Extract meaningful tool name, especially for MCP tools
|
// Extract meaningful tool name, especially for MCP tools
|
||||||
const extractToolName = (toolCall: any) => {
|
const extractToolName = (toolCall: any) => {
|
||||||
const rawName = toolCall?.assistantCall?.name || 'Tool Call';
|
const rawName = toolCall?.assistantCall?.name || 'Tool Call';
|
||||||
|
|
||||||
// Handle MCP tools specially
|
// Handle MCP tools specially
|
||||||
if (rawName === 'call-mcp-tool') {
|
if (rawName === 'call-mcp-tool') {
|
||||||
const assistantContent = toolCall?.assistantCall?.content;
|
const assistantContent = toolCall?.assistantCall?.content;
|
||||||
|
@ -169,21 +169,21 @@ export function ToolCallSidePanel({
|
||||||
}
|
}
|
||||||
return 'External Tool';
|
return 'External Tool';
|
||||||
}
|
}
|
||||||
|
|
||||||
// For all other tools, use the friendly name
|
// For all other tools, use the friendly name
|
||||||
return getUserFriendlyToolName(rawName);
|
return getUserFriendlyToolName(rawName);
|
||||||
};
|
};
|
||||||
|
|
||||||
const completedToolCalls = toolCallSnapshots.filter(snapshot =>
|
const completedToolCalls = toolCallSnapshots.filter(snapshot =>
|
||||||
snapshot.toolCall.toolResult?.content &&
|
snapshot.toolCall.toolResult?.content &&
|
||||||
snapshot.toolCall.toolResult.content !== 'STREAMING'
|
snapshot.toolCall.toolResult.content !== 'STREAMING'
|
||||||
);
|
);
|
||||||
const totalCompletedCalls = completedToolCalls.length;
|
const totalCompletedCalls = completedToolCalls.length;
|
||||||
|
|
||||||
let displayToolCall = currentToolCall;
|
let displayToolCall = currentToolCall;
|
||||||
let displayIndex = safeInternalIndex;
|
let displayIndex = safeInternalIndex;
|
||||||
let displayTotalCalls = totalCalls;
|
let displayTotalCalls = totalCalls;
|
||||||
|
|
||||||
const isCurrentToolStreaming = currentToolCall?.toolResult?.content === 'STREAMING';
|
const isCurrentToolStreaming = currentToolCall?.toolResult?.content === 'STREAMING';
|
||||||
if (isCurrentToolStreaming && totalCompletedCalls > 0) {
|
if (isCurrentToolStreaming && totalCompletedCalls > 0) {
|
||||||
const lastCompletedSnapshot = completedToolCalls[completedToolCalls.length - 1];
|
const lastCompletedSnapshot = completedToolCalls[completedToolCalls.length - 1];
|
||||||
|
@ -197,7 +197,7 @@ export function ToolCallSidePanel({
|
||||||
displayTotalCalls = totalCompletedCalls;
|
displayTotalCalls = totalCompletedCalls;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const currentToolName = displayToolCall?.assistantCall?.name || 'Tool Call';
|
const currentToolName = displayToolCall?.assistantCall?.name || 'Tool Call';
|
||||||
const CurrentToolIcon = getToolIcon(
|
const CurrentToolIcon = getToolIcon(
|
||||||
currentToolCall?.assistantCall?.name || 'unknown',
|
currentToolCall?.assistantCall?.name || 'unknown',
|
||||||
|
@ -207,19 +207,19 @@ export function ToolCallSidePanel({
|
||||||
|
|
||||||
const internalNavigate = React.useCallback((newIndex: number, source: string = 'internal') => {
|
const internalNavigate = React.useCallback((newIndex: number, source: string = 'internal') => {
|
||||||
if (newIndex < 0 || newIndex >= totalCalls) return;
|
if (newIndex < 0 || newIndex >= totalCalls) return;
|
||||||
|
|
||||||
const isNavigatingToLatest = newIndex === totalCalls - 1;
|
const isNavigatingToLatest = newIndex === totalCalls - 1;
|
||||||
|
|
||||||
console.log(`[INTERNAL_NAV] ${source}: ${internalIndex} -> ${newIndex}, mode will be: ${isNavigatingToLatest ? 'live' : 'manual'}`);
|
console.log(`[INTERNAL_NAV] ${source}: ${internalIndex} -> ${newIndex}, mode will be: ${isNavigatingToLatest ? 'live' : 'manual'}`);
|
||||||
|
|
||||||
setInternalIndex(newIndex);
|
setInternalIndex(newIndex);
|
||||||
|
|
||||||
if (isNavigatingToLatest) {
|
if (isNavigatingToLatest) {
|
||||||
setNavigationMode('live');
|
setNavigationMode('live');
|
||||||
} else {
|
} else {
|
||||||
setNavigationMode('manual');
|
setNavigationMode('manual');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (source === 'user_explicit') {
|
if (source === 'user_explicit') {
|
||||||
onNavigate(newIndex);
|
onNavigate(newIndex);
|
||||||
}
|
}
|
||||||
|
@ -242,7 +242,7 @@ export function ToolCallSidePanel({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [displayIndex, completedToolCalls, toolCallSnapshots, internalNavigate]);
|
}, [displayIndex, completedToolCalls, toolCallSnapshots, internalNavigate]);
|
||||||
|
|
||||||
const navigateToNext = React.useCallback(() => {
|
const navigateToNext = React.useCallback(() => {
|
||||||
if (displayIndex < displayTotalCalls - 1) {
|
if (displayIndex < displayTotalCalls - 1) {
|
||||||
const targetCompletedIndex = displayIndex + 1;
|
const targetCompletedIndex = displayIndex + 1;
|
||||||
|
@ -283,7 +283,7 @@ export function ToolCallSidePanel({
|
||||||
} else {
|
} else {
|
||||||
setNavigationMode('manual');
|
setNavigationMode('manual');
|
||||||
}
|
}
|
||||||
|
|
||||||
internalNavigate(actualIndex, 'user_explicit');
|
internalNavigate(actualIndex, 'user_explicit');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -482,7 +482,7 @@ export function ToolCallSidePanel({
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col h-full">
|
<div className="flex flex-col h-full">
|
||||||
<div className="pt-4 pl-4 pr-4">
|
<div className="pt-4 pl-4 pr-4">
|
||||||
|
@ -628,18 +628,24 @@ export function ToolCallSidePanel({
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
{isLiveMode && agentStatus === 'running' ? (
|
{showJumpToLive && (
|
||||||
|
<div className="flex cursor-pointer items-center gap-1.5 px-2 py-0.5 rounded-full bg-green-50 dark:bg-green-900/20 border border-green-200 dark:border-green-800" onClick={jumpToLive}>
|
||||||
|
<div className="w-1.5 h-1.5 bg-green-500 rounded-full animate-pulse"></div>
|
||||||
|
<span className="text-xs font-medium text-green-700 dark:text-green-400">Jump to Live</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{showJumpToLatest && (
|
||||||
|
<div className="flex cursor-pointer items-center gap-1.5 px-2 py-0.5 rounded-full bg-neutral-50 dark:bg-neutral-900/20 border border-neutral-200 dark:border-neutral-800" onClick={jumpToLatest}>
|
||||||
|
<div className="w-1.5 h-1.5 bg-neutral-500 rounded-full"></div>
|
||||||
|
<span className="text-xs font-medium text-neutral-700 dark:text-neutral-400">Jump to Latest</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{isLiveMode && agentStatus === 'running' && !showJumpToLive && (
|
||||||
<div className="flex items-center gap-1.5 px-2 py-0.5 rounded-full bg-green-50 dark:bg-green-900/20 border border-green-200 dark:border-green-800">
|
<div className="flex items-center gap-1.5 px-2 py-0.5 rounded-full bg-green-50 dark:bg-green-900/20 border border-green-200 dark:border-green-800">
|
||||||
<div className="w-1.5 h-1.5 bg-green-500 rounded-full animate-pulse"></div>
|
<div className="w-1.5 h-1.5 bg-green-500 rounded-full animate-pulse"></div>
|
||||||
<span className="text-xs font-medium text-green-700 dark:text-green-400">Live</span>
|
<span className="text-xs font-medium text-green-700 dark:text-green-400">Live</span>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
|
||||||
<div className="flex items-center gap-1.5 px-2 py-0.5 rounded-full bg-neutral-50 dark:bg-neutral-900/20 border border-neutral-200 dark:border-neutral-800">
|
|
||||||
<div className="w-1.5 h-1.5 bg-neutral-500 rounded-full"></div>
|
|
||||||
<span className="text-xs font-medium text-neutral-700 dark:text-neutral-400">Live</span>
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<span className="text-xs text-zinc-500 dark:text-zinc-400 flex-shrink-0">
|
<span className="text-xs text-zinc-500 dark:text-zinc-400 flex-shrink-0">
|
||||||
Step {displayIndex + 1} of {displayTotalCalls}
|
Step {displayIndex + 1} of {displayTotalCalls}
|
||||||
</span>
|
</span>
|
||||||
|
@ -672,7 +678,7 @@ export function ToolCallSidePanel({
|
||||||
<span className="text-xs font-medium text-neutral-700 dark:text-neutral-400">Live</span>
|
<span className="text-xs font-medium text-neutral-700 dark:text-neutral-400">Live</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<span className="text-xs text-zinc-500 dark:text-zinc-400">
|
<span className="text-xs text-zinc-500 dark:text-zinc-400">
|
||||||
{displayIndex + 1} / {displayTotalCalls}
|
{displayIndex + 1} / {displayTotalCalls}
|
||||||
{isCurrentToolStreaming && totalCompletedCalls > 0 && (
|
{isCurrentToolStreaming && totalCompletedCalls > 0 && (
|
||||||
|
@ -724,36 +730,8 @@ export function ToolCallSidePanel({
|
||||||
onValueChange={handleSliderChange}
|
onValueChange={handleSliderChange}
|
||||||
className="w-full [&>span:first-child]:h-1 [&>span:first-child]:bg-zinc-200 dark:[&>span:first-child]:bg-zinc-800 [&>span:first-child>span]:bg-zinc-500 dark:[&>span:first-child>span]:bg-zinc-400 [&>span:first-child>span]:h-1"
|
className="w-full [&>span:first-child]:h-1 [&>span:first-child]:bg-zinc-200 dark:[&>span:first-child]:bg-zinc-800 [&>span:first-child>span]:bg-zinc-500 dark:[&>span:first-child>span]:bg-zinc-400 [&>span:first-child>span]:h-1"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{showJumpToLive && (
|
|
||||||
<div className="absolute top-0 left-1/2 transform -translate-x-1/2 -translate-y-12 z-50">
|
|
||||||
<div className="relative">
|
|
||||||
<Button
|
|
||||||
onClick={jumpToLive}
|
|
||||||
size="sm"
|
|
||||||
className="h-8 px-3 bg-red-500 hover:bg-red-600 text-white shadow-lg dark:border-red-400 flex items-center gap-1.5"
|
|
||||||
>
|
|
||||||
<Radio className="h-3 w-3" />
|
|
||||||
<span className="text-xs font-medium">Jump to Live</span>
|
|
||||||
</Button>
|
|
||||||
<div className="absolute top-full left-1/2 transform -translate-x-1/2 w-0 h-0 border-l-4 border-r-4 border-t-4 border-transparent border-t-red-500"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{showJumpToLatest && (
|
|
||||||
<div className="absolute top-0 left-1/2 transform -translate-x-1/2 -translate-y-12 z-50">
|
|
||||||
<div className="relative">
|
|
||||||
<Button
|
|
||||||
onClick={jumpToLatest}
|
|
||||||
size="sm"
|
|
||||||
className="h-8 px-3 shadow-lg flex items-center gap-1.5"
|
|
||||||
>
|
|
||||||
<span className="text-xs font-medium">Jump to Latest</span>
|
|
||||||
</Button>
|
|
||||||
<div className="absolute top-full left-1/2 transform -translate-x-1/2 w-0 h-0 border-l-4 border-r-4 border-t-4 border-transparent border-t-primary"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
Loading…
Reference in New Issue