Skip to content

Commit 132fdeb

Browse files
7418claude
andcommitted
perf: P0 performance optimizations — memo, base64 cleanup, startup speed, bump v0.17.5
Six independent fixes targeting startup lag, streaming jank, and excessive payloads: Step 0 — MessageItem React.memo + useMemo (MessageItem.tsx) - Wrap MessageItem with React.memo so historical messages skip re-render when `message` ref is stable during streaming - Add useMemo for parseToolBlocks + pairTools (deps: message.content), parseMessageFiles (deps: text), token_usage JSON.parse (deps: message.token_usage) - Extract assistant content rendering into separate memo'd AssistantContent component to cache parseBatchPlan / parseImageGen chain (deps: displayText, messageId) - Root cause: 50 msgs × 10 SSE events/s = 500 full parses/s causing UI jank Step 1 — Strip base64 from new messages (ChatView.tsx, route.ts) - ChatView: remove `data: f.data` from fileMeta in display content HTML comment - route.ts: skip base64 data in fileAttachments when filePath exists (file already on disk) - Root cause: base64 image data inflated message content to MB-level Step 2 — Sanitize old messages in API (messages/route.ts) - Add stripFileData() to strip base64 `data` fields from <!--files:...--> comments before returning messages from GET /api/chat/sessions/:id/messages - Handles legacy data already stored with base64 Step 3 — Reduce initial fetch limit 100 → 30 (page.tsx, messages/route.ts) - Default limit changed from 100 to 30 on both frontend and backend - "Load more" functionality unchanged Step 4 — ChatListPanel dedup requests (ChatListPanel.tsx) - Remove duplicate pathname-dependent useEffect (was causing 2 fetches per navigation) - Add AbortController to cancel in-flight requests on new fetch - Add 300ms debounce for event-driven fetches (session-created/session-updated) Step 5 — Electron startup: show window before server ready (electron/main.ts) - createWindow() now accepts optional URL, defaults to inline loading HTML - Loading page: dark theme, centered spinner, "Starting CodePilot..." text - Startup: startServer() → createWindow(loading) → waitForServer() → loadURL(real) - Also updated activate handler for same early-window pattern Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent c14fdb0 commit 132fdeb

File tree

9 files changed

+232
-133
lines changed

9 files changed

+232
-133
lines changed

electron/main.ts

Lines changed: 53 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -325,7 +325,40 @@ function getIconPath(): string {
325325
return path.join(process.resourcesPath, 'icon.icns');
326326
}
327327

328-
function createWindow(port: number) {
328+
/** Inline loading HTML shown while the server starts up */
329+
const LOADING_HTML = `data:text/html;charset=utf-8,${encodeURIComponent(`<!DOCTYPE html>
330+
<html>
331+
<head>
332+
<meta charset="utf-8">
333+
<style>
334+
* { margin: 0; padding: 0; box-sizing: border-box; }
335+
body {
336+
height: 100vh; display: flex; align-items: center; justify-content: center;
337+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
338+
background: #0a0a0a; color: #a0a0a0;
339+
-webkit-app-region: drag;
340+
}
341+
.container { text-align: center; }
342+
.spinner {
343+
width: 28px; height: 28px; margin: 0 auto 14px;
344+
border: 2.5px solid rgba(255,255,255,0.1);
345+
border-top-color: rgba(255,255,255,0.5);
346+
border-radius: 50%;
347+
animation: spin 0.8s linear infinite;
348+
}
349+
@keyframes spin { to { transform: rotate(360deg); } }
350+
p { font-size: 13px; opacity: 0.7; }
351+
</style>
352+
</head>
353+
<body>
354+
<div class="container">
355+
<div class="spinner"></div>
356+
<p>Starting CodePilot...</p>
357+
</div>
358+
</body>
359+
</html>`)}`;
360+
361+
function createWindow(url?: string) {
329362
const windowOptions: Electron.BrowserWindowConstructorOptions = {
330363
width: 1280,
331364
height: 860,
@@ -352,7 +385,7 @@ function createWindow(port: number) {
352385

353386
mainWindow = new BrowserWindow(windowOptions);
354387

355-
mainWindow.loadURL(`http://127.0.0.1:${port}`);
388+
mainWindow.loadURL(url || LOADING_HTML);
356389

357390
if (isDev) {
358391
mainWindow.webContents.openDevTools();
@@ -730,17 +763,25 @@ app.whenReady().then(async () => {
730763
if (isDev) {
731764
port = 3000;
732765
console.log(`Dev mode: connecting to http://127.0.0.1:${port}`);
766+
serverPort = port;
767+
createWindow(`http://127.0.0.1:${port}`);
733768
} else {
734769
port = await getPort();
735770
console.log(`Starting server on port ${port}...`);
736771
serverProcess = startServer(port);
772+
serverPort = port;
773+
774+
// Show window immediately with loading screen
775+
createWindow();
776+
777+
// Wait for server in background, then navigate to real URL
737778
await waitForServer(port);
738779
console.log('Server is ready');
780+
if (mainWindow) {
781+
mainWindow.loadURL(`http://127.0.0.1:${port}`);
782+
}
739783
}
740784

741-
serverPort = port;
742-
createWindow(port);
743-
744785
// Initialize auto-updater in packaged mode only
745786
if (!isDev && mainWindow) {
746787
initAutoUpdater(mainWindow);
@@ -768,10 +809,16 @@ app.on('activate', async () => {
768809
if (!isDev && !serverProcess) {
769810
const port = await getPort();
770811
serverProcess = startServer(port);
812+
// Show loading window immediately
813+
createWindow();
771814
await waitForServer(port);
772815
serverPort = port;
816+
if (mainWindow) {
817+
mainWindow.loadURL(`http://127.0.0.1:${port}`);
818+
}
819+
} else {
820+
createWindow(`http://127.0.0.1:${serverPort || 3000}`);
773821
}
774-
createWindow(serverPort || 3000);
775822

776823
// Re-attach updater to the new window
777824
if (!isDev && mainWindow) {

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "codepilot",
3-
"version": "0.17.4",
3+
"version": "0.17.5",
44
"private": true,
55
"author": {
66
"name": "op7418",

src/app/api/chat/route.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,7 @@ export async function POST(request: NextRequest) {
149149
name: f.name,
150150
type: f.type,
151151
size: f.size,
152-
data: f.data,
152+
data: meta?.filePath ? '' : f.data, // Skip base64 data if file is already saved to disk
153153
filePath: meta?.filePath,
154154
};
155155
})

src/app/api/chat/sessions/[id]/messages/route.ts

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,22 @@ import { NextRequest } from 'next/server';
22
import { getMessages, getSession } from '@/lib/db';
33
import type { MessagesResponse } from '@/types';
44

5+
/** Strip base64 `data` fields from <!--files:...--> HTML comments in message content */
6+
function stripFileData(content: string): string {
7+
const match = content.match(/^<!--files:(.*?)-->/);
8+
if (!match) return content;
9+
try {
10+
const files = JSON.parse(match[1]);
11+
const cleaned = files.map((f: Record<string, unknown>) => {
12+
const { data, ...rest } = f;
13+
return rest;
14+
});
15+
return `<!--files:${JSON.stringify(cleaned)}-->${content.slice(match[0].length)}`;
16+
} catch {
17+
return content;
18+
}
19+
}
20+
521
export async function GET(
622
request: NextRequest,
723
{ params }: { params: Promise<{ id: string }> }
@@ -17,11 +33,16 @@ export async function GET(
1733
const limitParam = searchParams.get('limit');
1834
const beforeParam = searchParams.get('before');
1935

20-
const limit = limitParam ? Math.min(Math.max(parseInt(limitParam, 10) || 100, 1), 500) : 100;
36+
const limit = limitParam ? Math.min(Math.max(parseInt(limitParam, 10) || 30, 1), 500) : 30;
2137
const beforeRowId = beforeParam ? parseInt(beforeParam, 10) || undefined : undefined;
2238

2339
const { messages, hasMore } = getMessages(id, { limit, beforeRowId });
24-
const response: MessagesResponse = { messages, hasMore };
40+
// Sanitize: strip base64 data from file attachments in old messages
41+
const sanitizedMessages = messages.map(m => ({
42+
...m,
43+
content: stripFileData(m.content),
44+
}));
45+
const response: MessagesResponse = { messages: sanitizedMessages, hasMore };
2546
return Response.json(response);
2647
} catch (error) {
2748
const message = error instanceof Error ? error.message : 'Failed to fetch messages';

src/app/chat/[id]/page.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@ export default function ChatSessionPage({ params }: ChatSessionPageProps) {
118118

119119
async function loadMessages() {
120120
try {
121-
const res = await fetch(`/api/chat/sessions/${id}/messages?limit=100`);
121+
const res = await fetch(`/api/chat/sessions/${id}/messages?limit=30`);
122122
if (cancelled) return;
123123
if (!res.ok) {
124124
if (res.status === 404) {

src/components/chat/ChatView.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -212,7 +212,7 @@ export function ChatView({ sessionId, initialMessages = [], initialHasMore = fal
212212
// Build display content: embed file metadata as HTML comment for MessageItem to parse
213213
let displayContent = displayUserContent;
214214
if (files && files.length > 0) {
215-
const fileMeta = files.map(f => ({ id: f.id, name: f.name, type: f.type, size: f.size, data: f.data }));
215+
const fileMeta = files.map(f => ({ id: f.id, name: f.name, type: f.type, size: f.size }));
216216
displayContent = `<!--files:${JSON.stringify(fileMeta)}-->${displayUserContent}`;
217217
}
218218

0 commit comments

Comments
 (0)