From 1b294fe1c27771a4cb6f134dafd5df06eaf3dfc7 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Thu, 22 Jan 2026 10:34:25 +0000
Subject: [PATCH 1/4] Initial plan
From 5b39ba276ed076ddedc9343d0b7cb0f1532dec34 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Thu, 22 Jan 2026 10:37:27 +0000
Subject: [PATCH 2/4] Add React.memo to components, pagination to API, and fix
memory leaks
Co-authored-by: kaifcoder <57701861+kaifcoder@users.noreply.github.com>
---
src/app/api/sessions/route.ts | 41 ++++++++++++++++++++++++++++-------
src/components/ChatPanel.tsx | 21 ++++++++++--------
src/components/FileTree.tsx | 37 ++++++++++++++++---------------
src/lib/agent-memory.ts | 13 ++++++++---
4 files changed, 74 insertions(+), 38 deletions(-)
diff --git a/src/app/api/sessions/route.ts b/src/app/api/sessions/route.ts
index 250e734..5959b2f 100644
--- a/src/app/api/sessions/route.ts
+++ b/src/app/api/sessions/route.ts
@@ -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(
diff --git a/src/components/ChatPanel.tsx b/src/components/ChatPanel.tsx
index 06916aa..bcbfa0a 100644
--- a/src/components/ChatPanel.tsx
+++ b/src/components/ChatPanel.tsx
@@ -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";
@@ -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,
@@ -157,12 +157,15 @@ export function ChatPanel({
✗ Error
)}
- {toolCall.args && Object.keys(toolCall.args).length > 0 && (
-
- Args:{' '}
- {JSON.stringify(toolCall.args, null, 2).slice(0, 100)}{JSON.stringify(toolCall.args).length > 100 ? '...' : ''}
-
- )}
+ {toolCall.args && Object.keys(toolCall.args).length > 0 && (() => {
+ const argsStr = JSON.stringify(toolCall.args, null, 2);
+ return (
+
+ Args:{' '}
+ {argsStr.slice(0, 100)}{argsStr.length > 100 ? '...' : ''}
+
+ );
+ })()}
{toolCall.result && (
Result:{' '}
@@ -280,4 +283,4 @@ export function ChatPanel({
)}
);
-}
+});
diff --git a/src/components/FileTree.tsx b/src/components/FileTree.tsx
index 9368e02..846f2e9 100644
--- a/src/components/FileTree.tsx
+++ b/src/components/FileTree.tsx
@@ -1,4 +1,4 @@
-import { useState, useEffect } from "react";
+import { useState, useEffect, memo, useMemo } from "react";
import { cn } from "@/lib/utils";
import {
ChevronRight,
@@ -18,22 +18,20 @@ export type FileNode = {
children?: FileNode[];
};
+// Memoized file icon lookup
+const fileIconMap: Record = {
+ 'ts': FileCode,
+ 'tsx': FileCode,
+ 'js': FileCode,
+ 'jsx': FileCode,
+ 'json': FileJson,
+ 'md': FileText,
+ 'txt': FileText,
+};
+
function getFileIcon(fileName: string) {
const ext = fileName.split('.').pop()?.toLowerCase();
- switch (ext) {
- case 'ts':
- case 'tsx':
- case 'js':
- case 'jsx':
- return FileCode;
- case 'json':
- return FileJson;
- case 'md':
- case 'txt':
- return FileText;
- default:
- return File;
- }
+ return ext ? (fileIconMap[ext] || File) : File;
}
export interface FileTreeProps {
@@ -44,7 +42,7 @@ export interface FileTreeProps {
streamingFile?: string | null;
}
-export function FileTree({ nodes, selected, onSelect, level = 0, streamingFile }: Readonly) {
+export const FileTree = memo(function FileTree({ nodes, selected, onSelect, level = 0, streamingFile }: Readonly) {
// Initialize with all folders expanded by default
const [open, setOpen] = useState<{ [key: string]: boolean }>(() => {
const initialOpen: { [key: string]: boolean } = {};
@@ -86,7 +84,10 @@ export function FileTree({ nodes, selected, onSelect, level = 0, streamingFile }
return (
{nodes.map((node) => {
- const FileIconComponent = node.type === "file" ? getFileIcon(node.name) : null;
+ const FileIconComponent = useMemo(() =>
+ node.type === "file" ? getFileIcon(node.name) : null,
+ [node.type, node.name]
+ );
return (
@@ -151,4 +152,4 @@ export function FileTree({ nodes, selected, onSelect, level = 0, streamingFile }
})}
);
-}
+});
diff --git a/src/lib/agent-memory.ts b/src/lib/agent-memory.ts
index 0cf57d5..d5b775a 100644
--- a/src/lib/agent-memory.ts
+++ b/src/lib/agent-memory.ts
@@ -218,6 +218,8 @@ export type WorkSummary = {
};
const MAX_MESSAGES = 40; // cap per session to avoid unbounded memory
+const MAX_FILES_TRACKED = 50; // cap on files tracked per session
+const MAX_COMPONENTS_TRACKED = 30; // cap on components tracked per session
export async function getSessionMessages(sessionId: string): Promise
{
const memory = await getSessionMemory(sessionId, 'messages');
@@ -258,11 +260,16 @@ export async function updateWorkSummary(sessionId: string, summary: Partial
Date: Thu, 22 Jan 2026 10:39:24 +0000
Subject: [PATCH 3/4] Optimize SSE handler, regex patterns, async operations,
and database queries
Co-authored-by: kaifcoder <57701861+kaifcoder@users.noreply.github.com>
---
src/app/api/stream/route.ts | 171 ++++++++++++---------------------
src/app/chat/[id]/page.tsx | 16 +--
src/lib/nextjs-coding-agent.ts | 30 +++---
src/trpc/routers/session.ts | 47 ++++++---
4 files changed, 116 insertions(+), 148 deletions(-)
diff --git a/src/app/api/stream/route.ts b/src/app/api/stream/route.ts
index 5f1e18e..4f2272a 100644
--- a/src/app/api/stream/route.ts
+++ b/src/app/api/stream/route.ts
@@ -18,6 +18,11 @@ interface AgentEventData {
isNew?: boolean;
}
+// Helper function to create SSE messages
+function createSSEMessage(type: string, data: Record): 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');
@@ -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; 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() }]);
}
@@ -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,
+ }));
}
};
@@ -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
diff --git a/src/app/chat/[id]/page.tsx b/src/app/chat/[id]/page.tsx
index f97ed18..65b2525 100644
--- a/src/app/chat/[id]/page.tsx
+++ b/src/app/chat/[id]/page.tsx
@@ -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',
@@ -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);
@@ -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());
diff --git a/src/lib/nextjs-coding-agent.ts b/src/lib/nextjs-coding-agent.ts
index ae900d7..0a9f7e3 100644
--- a/src/lib/nextjs-coding-agent.ts
+++ b/src/lib/nextjs-coding-agent.ts
@@ -150,27 +150,27 @@ async function* typewriterEffect(content: string): AsyncGenerator pattern.test(content));
+ const startsWithThinking = THINKING_PATTERNS.some(pattern => pattern.test(content));
if (startsWithThinking) {
// Look for clear transition markers between thinking and answer
- const transitionPatterns = [
- /\n\n(?:Here's|Here is|Now,|So,|Therefore,|Based on this,|The answer is|To summarize)/i,
- /\n\n(?:Answer:|Response:|Solution:)/i,
- /\n\n---+\n/, // Markdown separator
- ];
-
- for (const pattern of transitionPatterns) {
+ for (const pattern of TRANSITION_PATTERNS) {
const parts = content.split(pattern);
if (parts.length >= 2) {
const reasoning = parts[0].trim();
diff --git a/src/trpc/routers/session.ts b/src/trpc/routers/session.ts
index 912f2fb..bf17509 100644
--- a/src/trpc/routers/session.ts
+++ b/src/trpc/routers/session.ts
@@ -92,28 +92,43 @@ export const sessionRouter = createTRPCRouter({
deleteSession: baseProcedure
.input(z.object({ id: z.string() }))
.mutation(async ({ ctx, input }) => {
- // Get session to retrieve sandboxId
- const session = await ctx.prisma.session.findUnique({
- where: { id: input.id },
+ // Use transaction to fetch and delete in one operation
+ const session = await ctx.prisma.$transaction(async (tx) => {
+ // Get session to retrieve sandboxId
+ const sess = await tx.session.findUnique({
+ where: { id: input.id },
+ select: { id: true, sandboxId: true },
+ });
+
+ if (!sess) {
+ throw new Error('Session not found');
+ }
+
+ // Delete session in same transaction
+ await tx.session.delete({
+ where: { id: input.id },
+ });
+
+ return sess;
});
- // Kill sandbox if it exists
+ // Kill sandbox if it exists (outside transaction to avoid blocking)
if (session?.sandboxId) {
- try {
- const sandbox = await getSandbox(session.sandboxId);
- if (sandbox) {
- await sandbox.kill();
+ // Fire and forget - don't await to avoid blocking response
+ getSandbox(session.sandboxId)
+ .then(sandbox => {
+ if (sandbox) {
+ return sandbox.kill();
+ }
+ })
+ .then(() => {
console.log(`✅ Killed sandbox ${session.sandboxId} for session ${input.id}`);
- }
- } catch (error) {
- console.error(`Failed to kill sandbox ${session.sandboxId}:`, error);
- // Continue with session deletion even if sandbox kill fails
- }
+ })
+ .catch((error) => {
+ console.error(`Failed to kill sandbox ${session.sandboxId}:`, error);
+ });
}
- await ctx.prisma.session.delete({
- where: { id: input.id },
- });
return { success: true };
}),
From 687fc3be26f4dc931509e8e9f513222a2f9130f0 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Thu, 22 Jan 2026 10:42:40 +0000
Subject: [PATCH 4/4] Address code review feedback: simplify IIFE and remove
unnecessary useMemo
Co-authored-by: kaifcoder <57701861+kaifcoder@users.noreply.github.com>
---
src/components/ChatPanel.tsx | 25 ++++++++++++++-----------
src/components/FileTree.tsx | 7 ++-----
2 files changed, 16 insertions(+), 16 deletions(-)
diff --git a/src/components/ChatPanel.tsx b/src/components/ChatPanel.tsx
index bcbfa0a..e1cb1c1 100644
--- a/src/components/ChatPanel.tsx
+++ b/src/components/ChatPanel.tsx
@@ -141,7 +141,12 @@ export const ChatPanel = memo(function ChatPanel({
🔧
Tool Executions
- {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 (
@@ -157,15 +162,12 @@ export const ChatPanel = memo(function ChatPanel({
✗ Error
)}
- {toolCall.args && Object.keys(toolCall.args).length > 0 && (() => {
- const argsStr = JSON.stringify(toolCall.args, null, 2);
- return (
-
- Args:{' '}
- {argsStr.slice(0, 100)}{argsStr.length > 100 ? '...' : ''}
-
- );
- })()}
+ {argsStr && (
+
+ Args:{' '}
+ {argsStr.slice(0, 100)}{argsStr.length > 100 ? '...' : ''}
+
+ )}
{toolCall.result && (
Result:{' '}
@@ -173,7 +175,8 @@ export const ChatPanel = memo(function ChatPanel({
)}
- ))}
+ );
+ })}
)}
diff --git a/src/components/FileTree.tsx b/src/components/FileTree.tsx
index 846f2e9..a01fd4d 100644
--- a/src/components/FileTree.tsx
+++ b/src/components/FileTree.tsx
@@ -1,4 +1,4 @@
-import { useState, useEffect, memo, useMemo } from "react";
+import { useState, useEffect, memo } from "react";
import { cn } from "@/lib/utils";
import {
ChevronRight,
@@ -84,10 +84,7 @@ export const FileTree = memo(function FileTree({ nodes, selected, onSelect, leve
return (
{nodes.map((node) => {
- const FileIconComponent = useMemo(() =>
- node.type === "file" ? getFileIcon(node.name) : null,
- [node.type, node.name]
- );
+ const FileIconComponent = node.type === "file" ? getFileIcon(node.name) : null;
return (