diff --git a/backend/sandbox/api.py b/backend/sandbox/api.py index 33effa6f..390d44c9 100644 --- a/backend/sandbox/api.py +++ b/backend/sandbox/api.py @@ -53,6 +53,10 @@ async def verify_sandbox_access(client, sandbox_id: str, user_id: str): raise HTTPException(status_code=404, detail="Sandbox not found") project_data = project_result.data[0] + + if project_data.get('is_public'): + return project_data + account_id = project_data.get('account_id') # Verify account membership diff --git a/backend/supabase/migrations/20250416133920_agentpress_schema.sql b/backend/supabase/migrations/20250416133920_agentpress_schema.sql index c0156dd6..f5c92b6c 100644 --- a/backend/supabase/migrations/20250416133920_agentpress_schema.sql +++ b/backend/supabase/migrations/20250416133920_agentpress_schema.sql @@ -6,6 +6,7 @@ CREATE TABLE projects ( description TEXT, account_id UUID NOT NULL REFERENCES basejump.accounts(id) ON DELETE CASCADE, sandbox JSONB DEFAULT '{}'::jsonb, + is_public BOOLEAN DEFAULT FALSE, created_at TIMESTAMP WITH TIME ZONE DEFAULT TIMEZONE('utc'::text, NOW()) NOT NULL, updated_at TIMESTAMP WITH TIME ZONE DEFAULT TIMEZONE('utc'::text, NOW()) NOT NULL ); @@ -96,7 +97,10 @@ ALTER TABLE projects ENABLE ROW LEVEL SECURITY; -- Project policies CREATE POLICY project_select_policy ON projects FOR SELECT - USING (basejump.has_role_on_account(account_id) = true); + USING ( + is_public = TRUE OR + basejump.has_role_on_account(account_id) = true OR + ); CREATE POLICY project_insert_policy ON projects FOR INSERT @@ -274,6 +278,7 @@ CREATE POLICY message_delete_policy ON messages -- Grant permissions to roles GRANT ALL PRIVILEGES ON TABLE projects TO authenticated, service_role; +GRANT SELECT ON TABLE projects TO anon; GRANT SELECT ON TABLE threads TO authenticated, anon, service_role; GRANT SELECT ON TABLE messages TO authenticated, anon, service_role; GRANT ALL PRIVILEGES ON TABLE agent_runs TO authenticated, service_role; diff --git a/frontend/src/app/share/[threadId]/page.tsx b/frontend/src/app/share/[threadId]/page.tsx index 2d1c349f..456a4fc7 100644 --- a/frontend/src/app/share/[threadId]/page.tsx +++ b/frontend/src/app/share/[threadId]/page.tsx @@ -95,6 +95,8 @@ function renderMarkdownContent(content: string, handleToolClick: (assistantMessa let lastIndex = 0; const contentParts: React.ReactNode[] = []; let match; + // Generate a unique timestamp for this render to avoid key conflicts + const timestamp = Date.now(); // If no XML tags found, just return the full content as markdown if (!content.match(xmlRegex)) { @@ -106,7 +108,7 @@ function renderMarkdownContent(content: string, handleToolClick: (assistantMessa if (match.index > lastIndex) { const textBeforeTag = content.substring(lastIndex, match.index); contentParts.push( - {textBeforeTag} + {textBeforeTag} ); } @@ -114,7 +116,7 @@ function renderMarkdownContent(content: string, handleToolClick: (assistantMessa const toolName = match[1] || match[2]; const IconComponent = getToolIcon(toolName); const paramDisplay = extractPrimaryParam(toolName, rawXml); - const toolCallKey = `tool-${match.index}`; + const toolCallKey = `tool-${match.index}-${timestamp}`; if (toolName === 'ask') { // Extract attachments from the XML attributes @@ -129,7 +131,7 @@ function renderMarkdownContent(content: string, handleToolClick: (assistantMessa // Render tag content with attachment UI contentParts.push( - + {askContent} {attachments.length > 0 && ( @@ -151,7 +153,7 @@ function renderMarkdownContent(content: string, handleToolClick: (assistantMessa return ( fileViewerHandler?.(attachment)} className="group inline-flex items-center gap-2 rounded-md border bg-muted/5 px-2.5 py-1.5 text-sm transition-colors hover:bg-muted/10" > @@ -192,7 +194,7 @@ function renderMarkdownContent(content: string, handleToolClick: (assistantMessa // Add text after the last tag if (lastIndex < content.length) { contentParts.push( - {content.substring(lastIndex)} + {content.substring(lastIndex)} ); } @@ -294,10 +296,13 @@ export default function ThreadPage({ params }: { params: Promise } } setMessages(prev => { + // First check if the message already exists const messageExists = prev.some(m => m.message_id === message.message_id); if (messageExists) { + // If it exists, update it instead of adding a new one return prev.map(m => m.message_id === message.message_id ? message : m); } else { + // If it's a new message, add it to the end return [...prev, message]; } }); @@ -1250,7 +1255,7 @@ export default function ThreadPage({ params }: { params: Promise } visibleMessages.forEach((message, index) => { const messageType = message.type; - const key = message.message_id || `msg-${index}`; + const key = message.message_id ? `${message.message_id}-${index}` : `msg-${index}`; if (messageType === 'user') { if (currentGroup) { @@ -1327,7 +1332,7 @@ export default function ThreadPage({ params }: { params: Promise } group.messages.forEach((message, msgIndex) => { if (message.type === 'assistant') { const parsedContent = safeJsonParse(message.content, {}); - const msgKey = message.message_id || `submsg-assistant-${msgIndex}`; + const msgKey = message.message_id ? `${message.message_id}-${msgIndex}` : `submsg-assistant-${msgIndex}`; if (!parsedContent.content) return; @@ -1483,6 +1488,27 @@ export default function ThreadPage({ params }: { params: Promise } > + + { + setIsPlaying(false); + setCurrentMessageIndex(messages.length); + setVisibleMessages(messages); + setToolPlaybackIndex(toolCalls.length - 1); + setStreamingText(""); + setIsStreamingText(false); + setCurrentToolCall(null); + if (toolCalls.length > 0) { + setCurrentToolIndex(toolCalls.length - 1); + setIsSidePanelOpen(true); + } + }} + className="text-xs" + > + Skip to end + )} @@ -1514,33 +1540,14 @@ export default function ThreadPage({ params }: { params: Promise } renderToolResult={toolViewResult} /> - {sandboxId && ( - - )} - - {/* Show a fallback modal when sandbox is not available */} - {!sandboxId && fileViewerOpen && ( - - - File Unavailable - - The file viewer is not available for this shared thread. - - setFileViewerOpen(false)} - className="w-full" - > - Close - - - - )} + {/* Show FileViewerModal regardless of sandboxId availability */} + ); } diff --git a/frontend/src/components/thread/tool-views/FileOperationToolView.tsx b/frontend/src/components/thread/tool-views/FileOperationToolView.tsx index b3306650..27154aed 100644 --- a/frontend/src/components/thread/tool-views/FileOperationToolView.tsx +++ b/frontend/src/components/thread/tool-views/FileOperationToolView.tsx @@ -144,7 +144,11 @@ export function FileOperationToolView({ ? `${project.sandbox.sandbox_url}/${processedFilePath}` : undefined; - console.log('HTML Preview URL:', htmlPreviewUrl); + // Only log HTML preview URL when it exists + if (htmlPreviewUrl) { + console.log('HTML Preview URL:', htmlPreviewUrl); + } + // Add state for view mode toggle (code or preview) const [viewMode, setViewMode] = useState<'code' | 'preview'>(isHtml || isMarkdown || isCsv ? 'preview' : 'code');
- The file viewer is not available for this shared thread. -