diff --git a/backend/agentpress/response_processor.py b/backend/agentpress/response_processor.py
index 5a44b4cb..8b7af0d7 100644
--- a/backend/agentpress/response_processor.py
+++ b/backend/agentpress/response_processor.py
@@ -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}"
diff --git a/backend/sandbox/api.py b/backend/sandbox/api.py
index 5e654a4c..f937361d 100644
--- a/backend/sandbox/api.py
+++ b/backend/sandbox/api.py
@@ -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(
diff --git a/backend/sandbox/sandbox.py b/backend/sandbox/sandbox.py
index 5e7bc9b2..6c9691ea 100644
--- a/backend/sandbox/sandbox.py
+++ b/backend/sandbox/sandbox.py
@@ -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
+
diff --git a/frontend/src/app/(dashboard)/dashboard/_components/suggestions/examples.tsx b/frontend/src/app/(dashboard)/dashboard/_components/suggestions/examples.tsx
index 26e8ae2a..0ad0ffd5 100644
--- a/frontend/src/app/(dashboard)/dashboard/_components/suggestions/examples.tsx
+++ b/frontend/src/app/(dashboard)/dashboard/_components/suggestions/examples.tsx
@@ -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: ,
},
{
- 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: ,
},
@@ -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: ,
},
+ {
+ 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: ,
+ },
+ {
+ 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: ,
+ },
+ {
+ title: 'Portfolio analysis',
+ query: 'Create a personal investment portfolio analysis tool with risk assessment, diversification recommendations, and performance tracking against market benchmarks.',
+ icon: ,
+ },
+ {
+ 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: ,
+ },
+ {
+ title: 'A/B testing framework',
+ query: 'Design a comprehensive A/B testing framework including hypothesis formation, statistical significance calculations, and result interpretation guidelines.',
+ icon: ,
+ },
+ {
+ 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:
,
+ },
+ {
+ 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: ,
+ },
+ {
+ 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: ,
+ },
+ {
+ 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: ,
+ },
+ {
+ 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: ,
+ },
+ {
+ title: 'Project automation',
+ query: 'Create an intelligent project management system with automatic task assignment, deadline tracking, resource allocation, and team communication integration.',
+ icon: ,
+ },
+ {
+ 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: ,
+ },
+ {
+ 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: ,
+ },
+ {
+ 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: ,
+ },
+ {
+ title: 'Supply chain analysis',
+ query: 'Create a supply chain optimization analysis including vendor evaluation, cost reduction opportunities, risk mitigation, and inventory management strategies.',
+ icon: ,
+ },
+ {
+ title: 'UX research framework',
+ query: 'Develop a comprehensive UX research framework including user interviews, usability testing, persona development, and data-driven design recommendations.',
+ icon: ,
+ },
];
+// 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 (
-
-
- {prompts.map((prompt, index) => (
-
onSelectPrompt && onSelectPrompt(prompt.query)}
- >
-
-
- {prompt.icon}
-
-
- {prompt.title}
-
-
+ const [displayedPrompts, setDisplayedPrompts] = useState([]);
+ const [isRefreshing, setIsRefreshing] = useState(false);
-
+ // Initialize with random prompts on mount
+ useEffect(() => {
+ setDisplayedPrompts(getRandomPrompts(3));
+ }, []);
+
+ const handleRefresh = () => {
+ setIsRefreshing(true);
+ setDisplayedPrompts(getRandomPrompts(3));
+ setTimeout(() => setIsRefreshing(false), 500);
+ };
+
+ return (
+
+
+ Quick starts
+
+
+
+ {displayedPrompts.map((prompt, index) => (
+
+ onSelectPrompt && onSelectPrompt(prompt.query)}
+ >
+
+
+
+ {React.cloneElement(prompt.icon as React.ReactElement, { size: 14 })}
+
+
+ {prompt.title}
+
+
+
+
+
))}
diff --git a/frontend/src/components/billing/billing-modal.tsx b/frontend/src/components/billing/billing-modal.tsx
new file mode 100644
index 00000000..839e102a
--- /dev/null
+++ b/frontend/src/components/billing/billing-modal.tsx
@@ -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
(null);
+ const [isLoading, setIsLoading] = useState(true);
+ const [error, setError] = useState(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 (
+
+ );
+ }
+
+ return (
+
+ );
+}
\ No newline at end of file
diff --git a/frontend/src/components/sidebar/nav-agents.tsx b/frontend/src/components/sidebar/nav-agents.tsx
index 6011f43f..3c0b25e4 100644
--- a/frontend/src/components/sidebar/nav-agents.tsx
+++ b/frontend/src/components/sidebar/nav-agents.tsx
@@ -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
},
{
diff --git a/frontend/src/components/thread/chat-input/model-selector.tsx b/frontend/src/components/thread/chat-input/model-selector.tsx
index 29c0ac4a..45308556 100644
--- a/frontend/src/components/thread/chat-input/model-selector.tsx
+++ b/frontend/src/components/thread/chat-input/model-selector.tsx
@@ -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 = ({
refreshCustomModels,
}) => {
const [paywallOpen, setPaywallOpen] = useState(false);
+ const [billingModalOpen, setBillingModalOpen] = useState(false);
const [lockedModel, setLockedModel] = useState(null);
const [isOpen, setIsOpen] = useState(false);
const [searchQuery, setSearchQuery] = useState('');
@@ -195,7 +197,7 @@ export const ModelSelector: React.FC = ({
};
const handleUpgradeClick = () => {
- router.push('/settings/billing');
+ setBillingModalOpen(true);
};
const closeDialog = () => {
@@ -756,6 +758,13 @@ export const ModelSelector: React.FC = ({
mode={dialogMode}
/>
+ {/* Billing Modal */}
+
+
{paywallOpen && (
{
+ 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}
diff --git a/frontend/src/components/thread/tool-views/GenericToolView.tsx b/frontend/src/components/thread/tool-views/GenericToolView.tsx
index 45152444..52da784f 100644
--- a/frontend/src/components/thread/tool-views/GenericToolView.tsx
+++ b/frontend/src/components/thread/tool-views/GenericToolView.tsx
@@ -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({
-
+
{!isStreaming && (
-
@@ -165,7 +165,7 @@ export function GenericToolView({
{isStreaming ? (
-
)}
-
+
{!isStreaming && (formattedAssistantContent || formattedToolContent) && (
@@ -233,7 +233,7 @@ export function GenericToolView({
)}
-
+
{toolTimestamp && !isStreaming
diff --git a/frontend/src/components/thread/tool-views/str-replace/StrReplaceToolView.tsx b/frontend/src/components/thread/tool-views/str-replace/StrReplaceToolView.tsx
index c1686abd..a2767047 100644
--- a/frontend/src/components/thread/tool-views/str-replace/StrReplaceToolView.tsx
+++ b/frontend/src/components/thread/tool-views/str-replace/StrReplaceToolView.tsx
@@ -37,8 +37,8 @@ const UnifiedDiffView: React.FC<{ lineDiff: LineDiff[] }> = ({ lineDiff }) => (
{lineDiff.map((line, i) => (
- = ({ lineDiff }) => (
{lineDiff.map((line, i) => (
- = ({ lineDiff }) => (
{line.lineNumber}
- {line.type === 'removed' &&
+ {line.type === 'removed' &&
}
@@ -99,7 +99,7 @@ const SplitDiffView: React.FC<{ lineDiff: LineDiff[] }> = ({ lineDiff }) => (
) : null}
|
- = ({ lineDiff }) => (
{line.lineNumber}
- {line.type === 'added' &&
+ {line.type === 'added' &&
}
@@ -152,7 +152,7 @@ export function StrReplaceToolView({
}: ToolViewProps): JSX.Element {
const [expanded, setExpanded] = useState(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}
-
+
{!isStreaming && (
-
@@ -260,7 +260,7 @@ export function StrReplaceToolView({
{isStreaming ? (
-
-
+
@@ -294,7 +294,7 @@ export function StrReplaceToolView({
{stats.deletions}
-
+
@@ -314,7 +314,7 @@ export function StrReplaceToolView({
-
+
{expanded && (
setViewMode(v as 'unified' | 'split')} className="w-auto">
@@ -324,11 +324,11 @@ export function StrReplaceToolView({
Split
-
+
-
+
@@ -340,7 +340,7 @@ export function StrReplaceToolView({
)}
-
+
{!isStreaming && (
@@ -365,7 +365,7 @@ export function StrReplaceToolView({
)}
-
+
{actualToolTimestamp && !isStreaming
? formatTimestamp(actualToolTimestamp)
diff --git a/frontend/src/components/thread/tool-views/str-replace/_utils.ts b/frontend/src/components/thread/tool-views/str-replace/_utils.ts
index 199cc97a..6917a115 100644
--- a/frontend/src/components/thread/tool-views/str-replace/_utils.ts
+++ b/frontend/src/components/thread/tool-views/str-replace/_utils.ts
@@ -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);
}
diff --git a/frontend/src/hooks/react-query/sidebar/use-sidebar.ts b/frontend/src/hooks/react-query/sidebar/use-sidebar.ts
index 5eec10e1..19c12739 100644
--- a/frontend/src/hooks/react-query/sidebar/use-sidebar.ts
+++ b/frontend/src/hooks/react-query/sidebar/use-sidebar.ts
@@ -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;
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 };
diff --git a/frontend/src/hooks/react-query/threads/utils.ts b/frontend/src/hooks/react-query/threads/utils.ts
index 9bbc7d38..47bb49d6 100644
--- a/frontend/src/hooks/react-query/threads/utils.ts
+++ b/frontend/src/hooks/react-query/threads/utils.ts
@@ -74,41 +74,99 @@ export const toggleThreadPublicStatus = async (
return updateThread(threadId, { is_public: isPublic });
};
-export const deleteThread = async (threadId: string): Promise => {
+const deleteSandbox = async (sandboxId: string): Promise => {
+ try {
+ const supabase = createClient();
+ const {
+ data: { session },
+ } = await supabase.auth.getSession();
+
+ const headers: Record = {
+ '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 => {
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(
|