Merge branch 'main' into mcp-5a-custom-1

This commit is contained in:
Soumyadas15 2025-06-02 16:34:49 +05:30
commit f65cc3eba9
13 changed files with 554 additions and 87 deletions

View File

@ -1514,7 +1514,7 @@ class ResponseProcessor:
# This allows the LLM to see the tool result in subsequent interactions
result_message = {
"role": result_role,
"content": structured_result
"content": json.dumps(structured_result)
}
message_obj = await self.add_message(
thread_id=thread_id,
@ -1619,6 +1619,9 @@ class ResponseProcessor:
# return summary
summary_output = result.output if hasattr(result, 'output') else str(result)
success_status = structured_result["tool_execution"]["result"]["success"]
# Create a more comprehensive summary for the LLM
if xml_tag_name:
status = "completed successfully" if structured_result_v1["tool_execution"]["result"]["success"] else "failed"
summary = f"Tool '{xml_tag_name}' {status}. Output: {summary_output}"

View File

@ -6,7 +6,7 @@ from fastapi import FastAPI, UploadFile, File, HTTPException, APIRouter, Form, D
from fastapi.responses import Response
from pydantic import BaseModel
from sandbox.sandbox import get_or_start_sandbox
from sandbox.sandbox import get_or_start_sandbox, delete_sandbox
from utils.logger import logger
from utils.auth_utils import get_optional_user_id
from services.supabase import DBConnection
@ -305,6 +305,28 @@ async def delete_file(
logger.error(f"Error deleting file in sandbox {sandbox_id}: {str(e)}")
raise HTTPException(status_code=500, detail=str(e))
@router.delete("/sandboxes/{sandbox_id}")
async def delete_sandbox_route(
sandbox_id: str,
request: Request = None,
user_id: Optional[str] = Depends(get_optional_user_id)
):
"""Delete an entire sandbox"""
logger.info(f"Received sandbox delete request for sandbox {sandbox_id}, user_id: {user_id}")
client = await db.client
# Verify the user has access to this sandbox
await verify_sandbox_access(client, sandbox_id, user_id)
try:
# Delete the sandbox using the sandbox module function
await delete_sandbox(sandbox_id)
return {"status": "success", "deleted": True, "sandbox_id": sandbox_id}
except Exception as e:
logger.error(f"Error deleting sandbox {sandbox_id}: {str(e)}")
raise HTTPException(status_code=500, detail=str(e))
# Should happen on server-side fully
@router.post("/project/{project_id}/sandbox/ensure-active")
async def ensure_project_sandbox_active(

View File

@ -125,3 +125,20 @@ def create_sandbox(password: str, project_id: str = None):
logger.debug(f"Sandbox environment successfully initialized")
return sandbox
async def delete_sandbox(sandbox_id: str):
"""Delete a sandbox by its ID."""
logger.info(f"Deleting sandbox with ID: {sandbox_id}")
try:
# Get the sandbox
sandbox = daytona.get_current_sandbox(sandbox_id)
# Delete the sandbox
daytona.remove(sandbox)
logger.info(f"Successfully deleted sandbox {sandbox_id}")
return True
except Exception as e:
logger.error(f"Error deleting sandbox {sandbox_id}: {str(e)}")
raise e

View File

@ -1,13 +1,30 @@
'use client';
import React from 'react';
import React, { useState, useEffect } from 'react';
import { motion } from 'framer-motion';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import {
BarChart3,
Bot,
Briefcase,
Settings,
Sparkles,
RefreshCw,
TrendingUp,
Users,
Shield,
Zap,
Target,
Brain,
Globe,
Heart,
PenTool,
Code,
Camera,
Calendar,
DollarSign,
Rocket,
} from 'lucide-react';
type PromptExample = {
@ -16,14 +33,14 @@ type PromptExample = {
icon: React.ReactNode;
};
const prompts: PromptExample[] = [
const allPrompts: PromptExample[] = [
{
title: 'Market research dashboard',
query: 'Create a comprehensive market research dashboard analyzing industry trends, customer segments, and competitive landscape. Include data visualization and actionable recommendations.',
icon: <BarChart3 className="text-green-700 dark:text-green-400" size={16} />,
},
{
title: 'Recommendation engine development',
title: 'Recommendation engine',
query: 'Develop a recommendation engine for personalized product suggestions. Include collaborative filtering, content-based filtering, and hybrid approaches with evaluation metrics.',
icon: <Bot className="text-blue-700 dark:text-blue-400" size={16} />,
},
@ -37,32 +54,159 @@ const prompts: PromptExample[] = [
query: 'Create an automated data pipeline for ETL processes. Include data validation, error handling, monitoring, and scalable architecture design.',
icon: <Settings className="text-purple-700 dark:text-purple-400" size={16} />,
},
{
title: 'Productivity system',
query: 'Design a comprehensive personal productivity system including task management, goal tracking, habit formation, and time blocking. Create templates and workflows for daily, weekly, and monthly planning.',
icon: <Target className="text-orange-700 dark:text-orange-400" size={16} />,
},
{
title: 'Content marketing plan',
query: 'Develop a 6-month content marketing strategy including blog posts, social media, email campaigns, and SEO optimization. Include content calendar and performance metrics.',
icon: <PenTool className="text-indigo-700 dark:text-indigo-400" size={16} />,
},
{
title: 'Portfolio analysis',
query: 'Create a personal investment portfolio analysis tool with risk assessment, diversification recommendations, and performance tracking against market benchmarks.',
icon: <DollarSign className="text-emerald-700 dark:text-emerald-400" size={16} />,
},
{
title: 'Customer journey map',
query: 'Map the complete customer journey from awareness to advocacy. Include touchpoints, pain points, emotions, and optimization opportunities at each stage.',
icon: <Users className="text-cyan-700 dark:text-cyan-400" size={16} />,
},
{
title: 'A/B testing framework',
query: 'Design a comprehensive A/B testing framework including hypothesis formation, statistical significance calculations, and result interpretation guidelines.',
icon: <TrendingUp className="text-teal-700 dark:text-teal-400" size={16} />,
},
{
title: 'Code review automation',
query: 'Create an automated code review system that checks for security vulnerabilities, performance issues, and coding standards. Include integration with CI/CD pipelines.',
icon: <Code className="text-violet-700 dark:text-violet-400" size={16} />,
},
{
title: 'Risk assessment matrix',
query: 'Develop a comprehensive risk assessment framework for business operations including risk identification, probability analysis, impact evaluation, and mitigation strategies.',
icon: <Shield className="text-red-700 dark:text-red-400" size={16} />,
},
{
title: 'Learning path generator',
query: 'Create a personalized learning path generator that adapts to individual goals, current skill level, and preferred learning style. Include progress tracking and resource recommendations.',
icon: <Brain className="text-pink-700 dark:text-pink-400" size={16} />,
},
{
title: 'Social media automation',
query: 'Design a social media automation system including content scheduling, engagement tracking, hashtag optimization, and performance analytics across multiple platforms.',
icon: <Globe className="text-blue-600 dark:text-blue-300" size={16} />,
},
{
title: 'Health tracking dashboard',
query: 'Build a comprehensive health tracking dashboard integrating fitness data, nutrition logging, sleep patterns, and medical records with actionable insights and goal setting.',
icon: <Heart className="text-red-600 dark:text-red-300" size={16} />,
},
{
title: 'Project automation',
query: 'Create an intelligent project management system with automatic task assignment, deadline tracking, resource allocation, and team communication integration.',
icon: <Calendar className="text-amber-700 dark:text-amber-400" size={16} />,
},
{
title: 'Sales funnel optimizer',
query: 'Analyze and optimize the entire sales funnel from lead generation to conversion. Include lead scoring, nurture sequences, and conversion rate optimization strategies.',
icon: <Zap className="text-yellow-600 dark:text-yellow-300" size={16} />,
},
{
title: 'Startup pitch deck',
query: 'Generate a compelling startup pitch deck including problem statement, solution overview, market analysis, business model, financial projections, and funding requirements.',
icon: <Rocket className="text-orange-600 dark:text-orange-300" size={16} />,
},
{
title: 'Photography workflow',
query: 'Design an end-to-end photography workflow including shoot planning, file organization, editing presets, client delivery, and portfolio management systems.',
icon: <Camera className="text-slate-700 dark:text-slate-400" size={16} />,
},
{
title: 'Supply chain analysis',
query: 'Create a supply chain optimization analysis including vendor evaluation, cost reduction opportunities, risk mitigation, and inventory management strategies.',
icon: <Briefcase className="text-stone-700 dark:text-stone-400" size={16} />,
},
{
title: 'UX research framework',
query: 'Develop a comprehensive UX research framework including user interviews, usability testing, persona development, and data-driven design recommendations.',
icon: <Sparkles className="text-fuchsia-700 dark:text-fuchsia-400" size={16} />,
},
];
// Function to get random prompts
const getRandomPrompts = (count: number = 3): PromptExample[] => {
const shuffled = [...allPrompts].sort(() => 0.5 - Math.random());
return shuffled.slice(0, count);
};
export const Examples = ({
onSelectPrompt,
}: {
onSelectPrompt?: (query: string) => void;
}) => {
return (
<div className="w-full max-w-3xl mx-auto">
<div className="grid grid-cols-2 md:grid-cols-2 xl:grid-cols-4 gap-4">
{prompts.map((prompt, index) => (
<Card
key={index}
className="group cursor-pointer h-full shadow-none transition-all bg-sidebar hover:bg-neutral-100 dark:hover:bg-neutral-800/60"
onClick={() => onSelectPrompt && onSelectPrompt(prompt.query)}
>
<CardHeader className="px-4">
<div className="flex items-center gap-2">
{prompt.icon}
</div>
<CardTitle className="font-normal group-hover:text-foreground transition-all text-muted-foreground text-sm line-clamp-3">
{prompt.title}
</CardTitle>
</CardHeader>
const [displayedPrompts, setDisplayedPrompts] = useState<PromptExample[]>([]);
const [isRefreshing, setIsRefreshing] = useState(false);
</Card>
// Initialize with random prompts on mount
useEffect(() => {
setDisplayedPrompts(getRandomPrompts(3));
}, []);
const handleRefresh = () => {
setIsRefreshing(true);
setDisplayedPrompts(getRandomPrompts(3));
setTimeout(() => setIsRefreshing(false), 500);
};
return (
<div className="w-full max-w-3xl mx-auto px-4">
<div className="flex justify-between items-center mb-3">
<span className="text-xs text-muted-foreground font-medium">Quick starts</span>
<Button
variant="ghost"
size="sm"
onClick={handleRefresh}
className="h-6 px-2 text-xs text-muted-foreground hover:text-foreground"
>
<motion.div
animate={{ rotate: isRefreshing ? 360 : 0 }}
transition={{ duration: 0.5, ease: "easeInOut" }}
>
<RefreshCw size={10} />
</motion.div>
</Button>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-3">
{displayedPrompts.map((prompt, index) => (
<motion.div
key={`${prompt.title}-${index}`}
initial={{ opacity: 0, scale: 0.8 }}
animate={{ opacity: 1, scale: 1 }}
transition={{
duration: 0.3,
delay: index * 0.05,
ease: "easeOut"
}}
>
<Card
className="group cursor-pointer h-full shadow-none transition-all bg-sidebar hover:bg-neutral-100 dark:hover:bg-neutral-800/60 p-0 justify-center"
onClick={() => onSelectPrompt && onSelectPrompt(prompt.query)}
>
<CardHeader className="p-2 grid-rows-1">
<div className="flex items-start justify-center gap-1.5">
<div className="flex-shrink-0 mt-0.5">
{React.cloneElement(prompt.icon as React.ReactElement, { size: 14 })}
</div>
<CardTitle className="font-normal group-hover:text-foreground transition-all text-muted-foreground text-xs leading-relaxed line-clamp-3">
{prompt.title}
</CardTitle>
</div>
</CardHeader>
</Card>
</motion.div>
))}
</div>
</div>

View File

@ -0,0 +1,140 @@
'use client';
import { useEffect, useState } from 'react';
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog';
import { Button } from '@/components/ui/button';
import { PricingSection } from '@/components/home/sections/pricing-section';
import { isLocalMode } from '@/lib/config';
import {
getSubscription,
createPortalSession,
SubscriptionStatus,
} from '@/lib/api';
import { useAuth } from '@/components/AuthProvider';
import { Skeleton } from '@/components/ui/skeleton';
import { X } from 'lucide-react';
interface BillingModalProps {
open: boolean;
onOpenChange: (open: boolean) => void;
returnUrl?: string;
}
export function BillingModal({ open, onOpenChange, returnUrl = window?.location?.href || '/' }: BillingModalProps) {
const { session, isLoading: authLoading } = useAuth();
const [subscriptionData, setSubscriptionData] = useState<SubscriptionStatus | null>(null);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [isManaging, setIsManaging] = useState(false);
useEffect(() => {
async function fetchSubscription() {
if (!open || authLoading || !session) return;
try {
setIsLoading(true);
const data = await getSubscription();
setSubscriptionData(data);
setError(null);
} catch (err) {
console.error('Failed to get subscription:', err);
setError(err instanceof Error ? err.message : 'Failed to load subscription data');
} finally {
setIsLoading(false);
}
}
fetchSubscription();
}, [open, session, authLoading]);
const handleManageSubscription = async () => {
try {
setIsManaging(true);
const { url } = await createPortalSession({ return_url: returnUrl });
window.location.href = url;
} catch (err) {
console.error('Failed to create portal session:', err);
setError(err instanceof Error ? err.message : 'Failed to create portal session');
} finally {
setIsManaging(false);
}
};
// Local mode content
if (isLocalMode()) {
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-2xl max-h-[80vh] overflow-y-auto">
<DialogHeader>
<DialogTitle>Billing & Subscription</DialogTitle>
</DialogHeader>
<div className="p-4 bg-muted/30 border border-border rounded-lg text-center">
<p className="text-sm text-muted-foreground">
Running in local development mode - billing features are disabled
</p>
<p className="text-xs text-muted-foreground mt-2">
All premium features are available in this environment
</p>
</div>
</DialogContent>
</Dialog>
);
}
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-4xl max-h-[80vh] overflow-y-auto">
<DialogHeader>
<DialogTitle>Upgrade Your Plan</DialogTitle>
</DialogHeader>
{isLoading || authLoading ? (
<div className="space-y-4">
<Skeleton className="h-20 w-full" />
<Skeleton className="h-40 w-full" />
<Skeleton className="h-10 w-full" />
</div>
) : error ? (
<div className="p-4 bg-destructive/10 border border-destructive/20 rounded-lg text-center">
<p className="text-sm text-destructive">Error loading billing status: {error}</p>
</div>
) : (
<>
{subscriptionData && (
<div className="mb-6">
<div className="rounded-lg border bg-background p-4">
<div className="flex justify-between items-center">
<span className="text-sm font-medium text-foreground/90">
Agent Usage This Month
</span>
<span className="text-sm font-medium">
{subscriptionData.current_usage?.toFixed(2) || '0'} /{' '}
{subscriptionData.minutes_limit || '0'} minutes
</span>
</div>
</div>
</div>
)}
<PricingSection returnUrl={returnUrl} showTitleAndTabs={false} />
{subscriptionData && (
<Button
onClick={handleManageSubscription}
disabled={isManaging}
className="w-full bg-primary hover:bg-primary/90 shadow-md hover:shadow-lg transition-all mt-4"
>
{isManaging ? 'Loading...' : 'Manage Subscription'}
</Button>
)}
</>
)}
</DialogContent>
</Dialog>
);
}

View File

@ -225,10 +225,16 @@ export function NavAgents() {
// Store threadToDelete in a local variable since it might be cleared
const deletedThread = { ...threadToDelete };
// Get sandbox ID from projects data
const thread = combinedThreads.find(t => t.threadId === threadId);
const project = projects.find(p => p.id === thread?.projectId);
const sandboxId = project?.sandbox?.id;
// Log operation start
console.log('DELETION - Starting thread deletion process', {
threadId: deletedThread.id,
isCurrentThread: isActive,
sandboxId
});
// Use the centralized deletion system with completion callback
@ -236,9 +242,9 @@ export function NavAgents() {
threadId,
isActive,
async () => {
// Delete the thread using the mutation
// Delete the thread using the mutation with sandbox ID
deleteThreadMutation(
{ threadId },
{ threadId, sandboxId },
{
onSuccess: () => {
// Invalidate queries to refresh the list
@ -282,6 +288,13 @@ export function NavAgents() {
deleteMultipleThreadsMutation(
{
threadIds: threadIdsToDelete,
threadSandboxMap: Object.fromEntries(
threadIdsToDelete.map(threadId => {
const thread = combinedThreads.find(t => t.threadId === threadId);
const project = projects.find(p => p.id === thread?.projectId);
return [threadId, project?.sandbox?.id || ''];
}).filter(([, sandboxId]) => sandboxId)
),
onProgress: handleDeletionProgress
},
{

View File

@ -27,6 +27,7 @@ import {
MODELS // Import the centralized MODELS constant
} from './_use-model-selection';
import { PaywallDialog } from '@/components/payment/paywall-dialog';
import { BillingModal } from '@/components/billing/billing-modal';
import { cn } from '@/lib/utils';
import { useRouter } from 'next/navigation';
import { isLocalMode } from '@/lib/config';
@ -56,6 +57,7 @@ export const ModelSelector: React.FC<ModelSelectorProps> = ({
refreshCustomModels,
}) => {
const [paywallOpen, setPaywallOpen] = useState(false);
const [billingModalOpen, setBillingModalOpen] = useState(false);
const [lockedModel, setLockedModel] = useState<string | null>(null);
const [isOpen, setIsOpen] = useState(false);
const [searchQuery, setSearchQuery] = useState('');
@ -195,7 +197,7 @@ export const ModelSelector: React.FC<ModelSelectorProps> = ({
};
const handleUpgradeClick = () => {
router.push('/settings/billing');
setBillingModalOpen(true);
};
const closeDialog = () => {
@ -756,6 +758,13 @@ export const ModelSelector: React.FC<ModelSelectorProps> = ({
mode={dialogMode}
/>
{/* Billing Modal */}
<BillingModal
open={billingModalOpen}
onOpenChange={setBillingModalOpen}
returnUrl={typeof window !== 'undefined' ? window.location.href : '/'}
/>
{paywallOpen && (
<PaywallDialog
open={true}

View File

@ -203,7 +203,34 @@ export function ToolCallSidePanel({
currentToolCall?.assistantCall?.name || 'unknown',
);
const isStreaming = displayToolCall?.toolResult?.content === 'STREAMING';
const isSuccess = displayToolCall?.toolResult?.isSuccess ?? true;
// Extract actual success value from tool content with fallbacks
const getActualSuccess = (toolCall: any): boolean => {
const content = toolCall?.toolResult?.content;
if (!content) return toolCall?.toolResult?.isSuccess ?? true;
const safeParse = (data: any) => {
try { return typeof data === 'string' ? JSON.parse(data) : data; }
catch { return null; }
};
const parsed = safeParse(content);
if (!parsed) return toolCall?.toolResult?.isSuccess ?? true;
if (parsed.content) {
const inner = safeParse(parsed.content);
if (inner?.tool_execution?.result?.success !== undefined) {
return inner.tool_execution.result.success;
}
}
const success = parsed.tool_execution?.result?.success ??
parsed.result?.success ??
parsed.success;
return success !== undefined ? success : (toolCall?.toolResult?.isSuccess ?? true);
};
const isSuccess = isStreaming ? true : getActualSuccess(displayToolCall);
const internalNavigate = React.useCallback((newIndex: number, source: string = 'internal') => {
if (newIndex < 0 || newIndex >= totalCalls) return;
@ -520,7 +547,7 @@ export function ToolCallSidePanel({
toolContent={displayToolCall.toolResult?.content}
assistantTimestamp={displayToolCall.assistantCall.timestamp}
toolTimestamp={displayToolCall.toolResult?.timestamp}
isSuccess={isStreaming ? true : (displayToolCall.toolResult?.isSuccess ?? true)}
isSuccess={isSuccess}
isStreaming={isStreaming}
project={project}
messages={messages}

View File

@ -39,25 +39,25 @@ export function GenericToolView({
// Use the new parser for backwards compatibility
const { toolResult } = extractToolData(content);
if (toolResult) {
// Format the structured content nicely
const formatted: any = {
tool: toolResult.xmlTagName || toolResult.functionName,
};
if (toolResult.arguments && Object.keys(toolResult.arguments).length > 0) {
formatted.parameters = toolResult.arguments;
}
if (toolResult.toolOutput) {
formatted.output = toolResult.toolOutput;
}
if (toolResult.isSuccess !== undefined) {
formatted.success = toolResult.isSuccess;
}
return JSON.stringify(formatted, null, 2);
}
@ -68,18 +68,18 @@ export function GenericToolView({
const formatted: any = {
tool: content.tool_name || content.xml_tag_name || 'unknown',
};
if (content.parameters && Object.keys(content.parameters).length > 0) {
formatted.parameters = content.parameters;
}
if (content.result) {
formatted.result = content.result;
}
return JSON.stringify(formatted, null, 2);
}
// Check if it has a content field that might contain the structured data (legacy)
if ('content' in content && typeof content.content === 'object') {
const innerContent = content.content;
@ -87,26 +87,26 @@ export function GenericToolView({
const formatted: any = {
tool: innerContent.tool_name || innerContent.xml_tag_name || 'unknown',
};
if (innerContent.parameters && Object.keys(innerContent.parameters).length > 0) {
formatted.parameters = innerContent.parameters;
}
if (innerContent.result) {
formatted.result = innerContent.result;
}
return JSON.stringify(formatted, null, 2);
}
}
// Fall back to old format handling
if (content.content && typeof content.content === 'string') {
return content.content;
}
return JSON.stringify(content, null, 2);
}
if (typeof content === 'string') {
try {
const parsedJson = JSON.parse(content);
@ -142,13 +142,13 @@ export function GenericToolView({
</CardTitle>
</div>
</div>
{!isStreaming && (
<Badge
variant="secondary"
<Badge
variant="secondary"
className={
isSuccess
? "bg-gradient-to-b from-emerald-200 to-emerald-100 text-emerald-700 dark:from-emerald-800/50 dark:to-emerald-900/60 dark:text-emerald-300"
isSuccess
? "bg-gradient-to-b from-emerald-200 to-emerald-100 text-emerald-700 dark:from-emerald-800/50 dark:to-emerald-900/60 dark:text-emerald-300"
: "bg-gradient-to-b from-rose-200 to-rose-100 text-rose-700 dark:from-rose-800/50 dark:to-rose-900/60 dark:text-rose-300"
}
>
@ -165,7 +165,7 @@ export function GenericToolView({
<CardContent className="p-0 h-full flex-1 overflow-hidden relative">
{isStreaming ? (
<LoadingState
<LoadingState
icon={Wrench}
iconColor="text-orange-500 dark:text-orange-400"
bgColor="bg-gradient-to-b from-orange-100 to-orange-50 shadow-inner dark:from-orange-800/40 dark:to-orange-900/60 dark:shadow-orange-950/20"
@ -223,7 +223,7 @@ export function GenericToolView({
</div>
)}
</CardContent>
<div className="px-4 py-2 h-10 bg-gradient-to-r from-zinc-50/90 to-zinc-100/90 dark:from-zinc-900/90 dark:to-zinc-800/90 backdrop-blur-sm border-t border-zinc-200 dark:border-zinc-800 flex justify-between items-center gap-4">
<div className="h-full flex items-center gap-2 text-sm text-zinc-500 dark:text-zinc-400">
{!isStreaming && (formattedAssistantContent || formattedToolContent) && (
@ -233,7 +233,7 @@ export function GenericToolView({
</Badge>
)}
</div>
<div className="text-xs text-zinc-500 dark:text-zinc-400 flex items-center gap-2">
<Clock className="h-3.5 w-3.5" />
{toolTimestamp && !isStreaming

View File

@ -37,8 +37,8 @@ const UnifiedDiffView: React.FC<{ lineDiff: LineDiff[] }> = ({ lineDiff }) => (
<table className="w-full border-collapse">
<tbody>
{lineDiff.map((line, i) => (
<tr
key={i}
<tr
key={i}
className={cn(
"hover:bg-zinc-50 dark:hover:bg-zinc-900",
line.type === 'removed' && "bg-red-50 dark:bg-red-950/30",
@ -78,9 +78,9 @@ const SplitDiffView: React.FC<{ lineDiff: LineDiff[] }> = ({ lineDiff }) => (
<tbody>
{lineDiff.map((line, i) => (
<tr key={i}>
<td
<td
className={cn(
"p-2 align-top",
"p-2 align-top",
line.type === 'removed' ? 'bg-red-50 dark:bg-red-950/30 text-red-700 dark:text-red-400' : '',
line.oldLine === null ? 'bg-zinc-100 dark:bg-zinc-900' : ''
)}
@ -90,7 +90,7 @@ const SplitDiffView: React.FC<{ lineDiff: LineDiff[] }> = ({ lineDiff }) => (
<div className="w-8 text-right pr-2 select-none text-xs text-zinc-500 dark:text-zinc-400">
{line.lineNumber}
</div>
{line.type === 'removed' &&
{line.type === 'removed' &&
<Minus className="h-3.5 w-3.5 text-red-500 mt-0.5 mr-2 flex-shrink-0" />
}
<div className="overflow-x-auto">
@ -99,7 +99,7 @@ const SplitDiffView: React.FC<{ lineDiff: LineDiff[] }> = ({ lineDiff }) => (
</div>
) : null}
</td>
<td
<td
className={cn(
"p-2 align-top",
line.type === 'added' ? 'bg-emerald-50 dark:bg-emerald-950/30 text-emerald-700 dark:text-emerald-400' : '',
@ -111,7 +111,7 @@ const SplitDiffView: React.FC<{ lineDiff: LineDiff[] }> = ({ lineDiff }) => (
<div className="w-8 text-right pr-2 select-none text-xs text-zinc-500 dark:text-zinc-400">
{line.lineNumber}
</div>
{line.type === 'added' &&
{line.type === 'added' &&
<Plus className="h-3.5 w-3.5 text-emerald-500 mt-0.5 mr-2 flex-shrink-0" />
}
<div className="overflow-x-auto">
@ -152,7 +152,7 @@ export function StrReplaceToolView({
}: ToolViewProps): JSX.Element {
const [expanded, setExpanded] = useState<boolean>(true);
const [viewMode, setViewMode] = useState<'unified' | 'split'>('unified');
let filePath: string | null = null;
let oldStr: string | null = null;
let newStr: string | null = null;
@ -198,7 +198,7 @@ export function StrReplaceToolView({
if (!filePath) {
filePath = extractFilePath(assistantContent) || extractFilePath(toolContent);
}
if (!oldStr || !newStr) {
const assistantStrReplace = extractStrReplaceContent(assistantContent);
const toolStrReplace = extractStrReplaceContent(toolContent);
@ -211,7 +211,7 @@ export function StrReplaceToolView({
// Generate diff data (only if we have both strings)
const lineDiff = oldStr && newStr ? generateLineDiff(oldStr, newStr) : [];
const charDiff = oldStr && newStr ? generateCharDiff(oldStr, newStr) : [];
// Calculate stats on changes
const stats: DiffStats = calculateDiffStats(lineDiff);
@ -230,13 +230,13 @@ export function StrReplaceToolView({
{toolTitle}
</CardTitle>
</div>
{!isStreaming && (
<Badge
variant="secondary"
<Badge
variant="secondary"
className={
actualIsSuccess
? "bg-gradient-to-b from-emerald-200 to-emerald-100 text-emerald-700 dark:from-emerald-800/50 dark:to-emerald-900/60 dark:text-emerald-300"
actualIsSuccess
? "bg-gradient-to-b from-emerald-200 to-emerald-100 text-emerald-700 dark:from-emerald-800/50 dark:to-emerald-900/60 dark:text-emerald-300"
: "bg-gradient-to-b from-rose-200 to-rose-100 text-rose-700 dark:from-rose-800/50 dark:to-rose-900/60 dark:text-rose-300"
}
>
@ -260,7 +260,7 @@ export function StrReplaceToolView({
<CardContent className="p-0 h-full flex-1 overflow-hidden relative">
{isStreaming ? (
<LoadingState
<LoadingState
icon={FileDiff}
iconColor="text-purple-500 dark:text-purple-400"
bgColor="bg-gradient-to-b from-purple-100 to-purple-50 shadow-inner dark:from-purple-800/40 dark:to-purple-900/60 dark:shadow-purple-950/20"
@ -282,7 +282,7 @@ export function StrReplaceToolView({
{filePath || 'Unknown file'}
</code>
</div>
<div className="flex items-center gap-2">
<div className="flex items-center text-xs text-zinc-500 dark:text-zinc-400 gap-3">
<div className="flex items-center">
@ -294,7 +294,7 @@ export function StrReplaceToolView({
<span>{stats.deletions}</span>
</div>
</div>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
@ -314,7 +314,7 @@ export function StrReplaceToolView({
</TooltipProvider>
</div>
</div>
{expanded && (
<div>
<Tabs value={viewMode} onValueChange={(v) => setViewMode(v as 'unified' | 'split')} className="w-auto">
@ -324,11 +324,11 @@ export function StrReplaceToolView({
<TabsTrigger value="split" className="text-xs h-6 px-2">Split</TabsTrigger>
</TabsList>
</div>
<TabsContent value="unified" className="m-0 pb-4">
<UnifiedDiffView lineDiff={lineDiff} />
</TabsContent>
<TabsContent value="split" className="m-0">
<SplitDiffView lineDiff={lineDiff} />
</TabsContent>
@ -340,7 +340,7 @@ export function StrReplaceToolView({
</ScrollArea>
)}
</CardContent>
<div className="px-4 py-2 h-10 bg-gradient-to-r from-zinc-50/90 to-zinc-100/90 dark:from-zinc-900/90 dark:to-zinc-800/90 backdrop-blur-sm border-t border-zinc-200 dark:border-zinc-800 flex justify-between items-center">
<div className="h-full flex items-center gap-2 text-xs text-zinc-500 dark:text-zinc-400">
{!isStreaming && (
@ -365,7 +365,7 @@ export function StrReplaceToolView({
</div>
)}
</div>
<div className="text-xs text-zinc-500 dark:text-zinc-400">
{actualToolTimestamp && !isStreaming
? formatTimestamp(actualToolTimestamp)

View File

@ -27,7 +27,32 @@ export interface ExtractedData {
export const extractFromNewFormat = (content: any): ExtractedData => {
if (!content || typeof content !== 'object') return { filePath: null, oldStr: null, newStr: null };
if (!content) {
return { filePath: null, oldStr: null, newStr: null };
}
if (typeof content === 'string') {
// Only try to parse if it looks like JSON
const trimmed = content.trim();
if (trimmed.startsWith('{') || trimmed.startsWith('[')) {
try {
console.debug('StrReplaceToolView: Attempting to parse JSON string:', content.substring(0, 100) + '...');
const parsed = JSON.parse(content);
console.debug('StrReplaceToolView: Successfully parsed JSON:', parsed);
return extractFromNewFormat(parsed);
} catch (error) {
console.error('StrReplaceToolView: JSON parse error:', error, 'Content:', content.substring(0, 200));
return { filePath: null, oldStr: null, newStr: null };
}
} else {
console.debug('StrReplaceToolView: String content does not look like JSON, skipping parse');
return { filePath: null, oldStr: null, newStr: null };
}
}
if (typeof content !== 'object') {
return { filePath: null, oldStr: null, newStr: null };
}
if ('tool_execution' in content && typeof content.tool_execution === 'object') {
const toolExecution = content.tool_execution;
@ -49,7 +74,13 @@ export const extractFromNewFormat = (content: any): ExtractedData => {
};
}
if ('role' in content && 'content' in content && typeof content.content === 'string') {
console.debug('StrReplaceToolView: Found role/content structure with string content, parsing...');
return extractFromNewFormat(content.content);
}
if ('role' in content && 'content' in content && typeof content.content === 'object') {
console.debug('StrReplaceToolView: Found role/content structure with object content');
return extractFromNewFormat(content.content);
}

View File

@ -33,12 +33,13 @@ export const useThreads = createQueryHook(
interface DeleteThreadVariables {
threadId: string;
sandboxId?: string;
isNavigateAway?: boolean;
}
export const useDeleteThread = createMutationHook(
async ({ threadId }: DeleteThreadVariables) => {
return await deleteThread(threadId);
async ({ threadId, sandboxId }: DeleteThreadVariables) => {
return await deleteThread(threadId, sandboxId);
},
{
onSuccess: () => {
@ -48,16 +49,18 @@ export const useDeleteThread = createMutationHook(
interface DeleteMultipleThreadsVariables {
threadIds: string[];
threadSandboxMap?: Record<string, string>;
onProgress?: (completed: number, total: number) => void;
}
export const useDeleteMultipleThreads = createMutationHook(
async ({ threadIds, onProgress }: DeleteMultipleThreadsVariables) => {
async ({ threadIds, threadSandboxMap, onProgress }: DeleteMultipleThreadsVariables) => {
let completedCount = 0;
const results = await Promise.all(
threadIds.map(async (threadId) => {
try {
const result = await deleteThread(threadId);
const sandboxId = threadSandboxMap?.[threadId];
const result = await deleteThread(threadId, sandboxId);
completedCount++;
onProgress?.(completedCount, threadIds.length);
return { success: true, threadId };

View File

@ -74,41 +74,99 @@ export const toggleThreadPublicStatus = async (
return updateThread(threadId, { is_public: isPublic });
};
export const deleteThread = async (threadId: string): Promise<void> => {
const deleteSandbox = async (sandboxId: string): Promise<void> => {
try {
const supabase = createClient();
const {
data: { session },
} = await supabase.auth.getSession();
const headers: Record<string, string> = {
'Content-Type': 'application/json',
};
if (session?.access_token) {
headers['Authorization'] = `Bearer ${session.access_token}`;
}
const response = await fetch(`${API_URL}/sandboxes/${sandboxId}`, {
method: 'DELETE',
headers,
});
if (!response.ok) {
console.warn('Failed to delete sandbox, continuing with thread deletion');
}
} catch (error) {
console.warn('Error deleting sandbox, continuing with thread deletion:', error);
}
};
export const deleteThread = async (threadId: string, sandboxId?: string): Promise<void> => {
try {
const supabase = createClient();
// If sandbox ID is provided, delete it directly
if (sandboxId) {
await deleteSandbox(sandboxId);
} else {
// Otherwise, get the thread to find its project and sandbox
const { data: thread, error: threadError } = await supabase
.from('threads')
.select('project_id')
.eq('thread_id', threadId)
.single();
if (threadError) {
console.error('Error fetching thread:', threadError);
throw new Error(`Error fetching thread: ${threadError.message}`);
}
// If thread has a project, get sandbox ID and delete it
if (thread?.project_id) {
const { data: project } = await supabase
.from('projects')
.select('sandbox')
.eq('project_id', thread.project_id)
.single();
if (project?.sandbox?.id) {
await deleteSandbox(project.sandbox.id);
}
}
}
console.log(`Deleting all agent runs for thread ${threadId}`);
const { error: agentRunsError } = await supabase
.from('agent_runs')
.delete()
.eq('thread_id', threadId);
if (agentRunsError) {
console.error('Error deleting agent runs:', agentRunsError);
throw new Error(`Error deleting agent runs: ${agentRunsError.message}`);
}
console.log(`Deleting all messages for thread ${threadId}`);
const { error: messagesError } = await supabase
.from('messages')
.delete()
.eq('thread_id', threadId);
if (messagesError) {
console.error('Error deleting messages:', messagesError);
throw new Error(`Error deleting messages: ${messagesError.message}`);
}
console.log(`Deleting thread ${threadId}`);
const { error: threadError } = await supabase
const { error: threadError2 } = await supabase
.from('threads')
.delete()
.eq('thread_id', threadId);
if (threadError) {
console.error('Error deleting thread:', threadError);
throw new Error(`Error deleting thread: ${threadError.message}`);
if (threadError2) {
console.error('Error deleting thread:', threadError2);
throw new Error(`Error deleting thread: ${threadError2.message}`);
}
console.log(