Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 33 additions & 8 deletions src/app/api/sessions/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,41 @@ import { PrismaClient } from '@/generated/prisma'

const prisma = new PrismaClient()

// GET /api/sessions - List all sessions
export async function GET() {
// GET /api/sessions - List all sessions with pagination
export async function GET(request: Request) {
try {
const sessions = await prisma.session.findMany({
orderBy: {
createdAt: 'desc'
}
})
const { searchParams } = new URL(request.url);
const page = parseInt(searchParams.get('page') || '1', 10);
const limit = Math.min(parseInt(searchParams.get('limit') || '50', 10), 100); // Max 100 per page
const skip = (page - 1) * limit;

const [sessions, totalCount] = await Promise.all([
prisma.session.findMany({
select: {
id: true,
sandboxId: true,
shareToken: true,
createdAt: true,
updatedAt: true,
},
orderBy: {
createdAt: 'desc'
},
skip,
take: limit,
}),
prisma.session.count(),
]);

return NextResponse.json(sessions)
return NextResponse.json({
sessions,
pagination: {
page,
limit,
totalCount,
totalPages: Math.ceil(totalCount / limit),
}
});
} catch (error) {
console.error('[API] Failed to fetch sessions:', error)
return NextResponse.json(
Expand Down
171 changes: 61 additions & 110 deletions src/app/api/stream/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,11 @@ interface AgentEventData {
isNew?: boolean;
}

// Helper function to create SSE messages
function createSSEMessage(type: string, data: Record<string, unknown>): string {
return `data: ${JSON.stringify({ type, data })}\n\n`;
}

export async function GET(request: NextRequest) {
const { searchParams } = new URL(request.url);
const sessionId = searchParams.get('sessionId');
Expand Down Expand Up @@ -47,88 +52,61 @@ export async function GET(request: NextRequest) {
};

// Initial connection message
send(`data: ${JSON.stringify({ type: 'connected', sessionId })}

`);
send(createSSEMessage('connected', { sessionId }));

// Event handlers for different types of updates
const handleAgentUpdate = (data: AgentEventData) => {
if (data.sessionId === sessionId) {
send(`data: ${JSON.stringify({
type: 'status',
data: {
sessionId: data.sessionId,
status: data.status,
message: data.message,
hasSandbox: data.hasSandbox,
}
})}

`);
send(createSSEMessage('status', {
sessionId: data.sessionId,
status: data.status,
message: data.message,
hasSandbox: data.hasSandbox,
}));
}
};

const handlePartialContent = (data: AgentEventData) => {
if (data.sessionId === sessionId) {
send(`data: ${JSON.stringify({
type: 'partial',
data: {
sessionId: data.sessionId,
content: data.content,
fullContent: data.fullContent,
}
})}

`);
send(createSSEMessage('partial', {
sessionId: data.sessionId,
content: data.content,
fullContent: data.fullContent,
}));
}
};

const handleToolUsed = (data: AgentEventData & { args?: Record<string, unknown>; result?: string; status?: string }) => {
if (data.sessionId === sessionId) {
send(`data: ${JSON.stringify({
type: 'tool',
data: {
sessionId: data.sessionId,
tool: data.tool,
args: data.args,
result: data.result,
status: data.status,
}
})}

`);
send(createSSEMessage('tool', {
sessionId: data.sessionId,
tool: data.tool,
args: data.args,
result: data.result,
status: data.status,
}));
}
};

const handleSandboxStatus = (data: AgentEventData) => {
if (data.sessionId === sessionId) {
send(`data: ${JSON.stringify({
type: 'sandbox',
data: {
sessionId: data.sessionId,
sandboxId: data.sandboxId,
sandboxUrl: data.sandboxUrl,
isNew: data.isNew,
}
})}

`);
send(createSSEMessage('sandbox', {
sessionId: data.sessionId,
sandboxId: data.sandboxId,
sandboxUrl: data.sandboxUrl,
isNew: data.isNew,
}));
}
};

const handleComplete = (data: AgentEventData) => {
if (data.sessionId === sessionId) {
send(`data: ${JSON.stringify({
type: 'complete',
data: {
sessionId: data.sessionId,
response: data.response,
sandboxUrl: data.sandboxUrl,
hasSandbox: data.hasSandbox,
}
})}

`);
send(createSSEMessage('complete', {
sessionId: data.sessionId,
response: data.response,
sandboxUrl: data.sandboxUrl,
hasSandbox: data.hasSandbox,
}));
if (data.response) {
appendSessionMessages(sessionId, [{ role: 'ai', content: String(data.response), ts: Date.now() }]);
}
Expand All @@ -137,75 +115,50 @@ export async function GET(request: NextRequest) {

const handleError = (data: AgentEventData) => {
if (data.sessionId === sessionId) {
send(`data: ${JSON.stringify({
type: 'error',
data: {
sessionId: data.sessionId,
error: data.error,
}
})}

`);
send(createSSEMessage('error', {
sessionId: data.sessionId,
error: data.error,
}));
}
};

const handleReasoning = (data: AgentEventData & { reasoning?: string }) => {
if (data.sessionId === sessionId) {
send(`data: ${JSON.stringify({
type: 'reasoning',
data: {
sessionId: data.sessionId,
reasoning: data.reasoning,
}
})}

`);
send(createSSEMessage('reasoning', {
sessionId: data.sessionId,
reasoning: data.reasoning,
}));
}
};

const handleFileUpdate = (data: AgentEventData & { filePath?: string; content?: string; action?: 'start' | 'update' | 'complete' }) => {
if (data.sessionId === sessionId) {
send(`data: ${JSON.stringify({
type: 'file_update',
data: {
sessionId: data.sessionId,
filePath: data.filePath,
content: data.content,
action: data.action,
}
})}

`);
send(createSSEMessage('file_update', {
sessionId: data.sessionId,
filePath: data.filePath,
content: data.content,
action: data.action,
}));
}
};

const handleCodePatch = (data: AgentEventData & { filePath?: string; content?: string; action?: 'start' | 'patch' | 'complete' }) => {
if (data.sessionId === sessionId) {
send(`data: ${JSON.stringify({
type: 'code_patch',
data: {
sessionId: data.sessionId,
filePath: data.filePath,
content: data.content,
action: data.action,
}
})}

`);
send(createSSEMessage('code_patch', {
sessionId: data.sessionId,
filePath: data.filePath,
content: data.content,
action: data.action,
}));
}
};

const handleFileTreeSync = (data: AgentEventData & { fileTree?: unknown }) => {
if (data.sessionId === sessionId) {
send(`data: ${JSON.stringify({
type: 'file_tree_sync',
data: {
sessionId: data.sessionId,
fileTree: data.fileTree,
}
})}

`);
send(createSSEMessage('file_tree_sync', {
sessionId: data.sessionId,
fileTree: data.fileTree,
}));
}
};

Expand All @@ -223,9 +176,7 @@ export async function GET(request: NextRequest) {

// Keep connection alive with periodic heartbeat
const heartbeat = setInterval(() => {
send(`data: ${JSON.stringify({ type: 'heartbeat', timestamp: Date.now() })}

`);
send(createSSEMessage('heartbeat', { timestamp: Date.now() }));
}, 30000);

// Cleanup when client disconnects
Expand Down
16 changes: 9 additions & 7 deletions src/app/chat/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -184,10 +184,11 @@ function Page({ params }: PageProps) {
setSandboxId(session.sandboxId);
console.log('[DB] Loaded sandboxId:', session.sandboxId);

// Auto-sync filesystem when sandbox is loaded
// Block code tab until sync completes
// Auto-sync filesystem when sandbox is loaded - non-blocking async
setIsSyncingFilesystem(true);
setTimeout(async () => {
// Use async IIFE instead of setTimeout for non-blocking operation
(async () => {
await new Promise(resolve => setTimeout(resolve, 1000)); // Wait 1s for sandbox to be ready
try {
const response = await fetch('/api/sync-filesystem', {
method: 'POST',
Expand All @@ -208,7 +209,7 @@ function Page({ params }: PageProps) {
// Sync complete, unblock code tab
setIsSyncingFilesystem(false);
}
}, 1000); // Wait 1s for sandbox to be ready
})();
}
if (session.sandboxUrl) {
setSandboxUrl(session.sandboxUrl);
Expand All @@ -218,15 +219,16 @@ function Page({ params }: PageProps) {
setIsCheckingExpiration(true);
const createdTime = new Date(session.sandboxCreatedAt).getTime();
setSandboxCreatedAt(createdTime);
// Check if already expired
// Check if already expired - non-blocking async
const elapsed = Date.now() - createdTime;
setTimeout(() => {
(async () => {
await new Promise(resolve => setTimeout(resolve, 500)); // Small delay to show loader
if (elapsed >= SANDBOX_EXPIRY_MS) {
setIsSandboxExpired(true);
console.log('[DB] Sandbox already expired');
}
setIsCheckingExpiration(false);
}, 500); // Small delay to show loader
})();
} else {
// Fallback: if no timestamp in DB, use current time (new sandbox, not expired)
setSandboxCreatedAt(Date.now());
Expand Down
20 changes: 13 additions & 7 deletions src/components/ChatPanel.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"use client";

import { Button } from "@/components/ui/button";
import { Dispatch, SetStateAction, useRef, useEffect } from "react";
import { Dispatch, SetStateAction, useRef, useEffect, memo, useMemo } from "react";
import { motion, AnimatePresence } from "framer-motion";
import { LucideSend, Bot, User } from "lucide-react";
import { Textarea } from "@/components/ui/textarea";
Expand Down Expand Up @@ -33,7 +33,7 @@ interface ChatPanelProps {
readOnly?: boolean; // For shared sessions - disable sending messages
}

export function ChatPanel({
export const ChatPanel = memo(function ChatPanel({
messages,
message,
setMessage,
Expand Down Expand Up @@ -141,7 +141,12 @@ export function ChatPanel({
<span>🔧</span>
<span>Tool Executions</span>
</div>
{msg.toolCalls.map((toolCall, idx) => (
{msg.toolCalls.map((toolCall, idx) => {
const argsStr = toolCall.args && Object.keys(toolCall.args).length > 0
? JSON.stringify(toolCall.args, null, 2)
: null;

return (
<div key={`${msg.id}-tool-${idx}`} className="ml-6 space-y-1 text-xs border-l-2 border-purple-200 dark:border-purple-800 pl-3">
<div className="flex items-center gap-2">
<code className="bg-purple-100 dark:bg-purple-900/30 px-2 py-0.5 rounded font-mono">
Expand All @@ -157,10 +162,10 @@ export function ChatPanel({
<span className="text-red-500">✗ Error</span>
)}
</div>
{toolCall.args && Object.keys(toolCall.args).length > 0 && (
{argsStr && (
<div className="text-muted-foreground">
<span className="font-medium">Args:</span>{' '}
<span className="font-mono">{JSON.stringify(toolCall.args, null, 2).slice(0, 100)}{JSON.stringify(toolCall.args).length > 100 ? '...' : ''}</span>
<span className="font-mono">{argsStr.slice(0, 100)}{argsStr.length > 100 ? '...' : ''}</span>
</div>
)}
{toolCall.result && (
Expand All @@ -170,7 +175,8 @@ export function ChatPanel({
</div>
)}
</div>
))}
);
})}
</div>
)}

Expand Down Expand Up @@ -280,4 +286,4 @@ export function ChatPanel({
)}
</div>
);
}
});
Loading