6 files changed,
+109,
-14
+4,
-10
1@@ -36,16 +36,10 @@ const server = createServer(handler);
2
3 // Attach WebSocket for terminal
4 try {
5- const wsHandler = await import(join(buildPath, 'server', 'chunks', 'ws-handler.js'));
6- wsHandler.attachWebSocket(server);
7-} catch {
8- // ws-handler may be bundled differently — try alternative import
9- try {
10- const { attachWebSocket } = await import('../src/lib/server/ws-handler.ts');
11- attachWebSocket(server);
12- } catch (err) {
13- console.warn(' Warning: Could not attach terminal WebSocket:', err.message);
14- }
15+ const { attachWebSocket } = await import(join(buildPath, 'ws-handler.js'));
16+ attachWebSocket(server);
17+} catch (err) {
18+ console.warn(' Warning: Terminal not available:', err.message);
19 }
20
21 server.listen(port, '127.0.0.1', async () => {
+1,
-1
1@@ -18,7 +18,7 @@
2 "scripts": {
3 "prepublishOnly": "npm run build",
4 "dev": "vite dev",
5- "build": "vite build",
6+ "build": "vite build && node scripts/build-ws.js",
7 "preview": "vite preview",
8 "prepare": "svelte-kit sync || echo ''",
9 "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
+24,
-0
1@@ -0,0 +1,24 @@
2+// Build ws-handler + pty-session as a standalone bundle for the CLI binary
3+import { build } from 'vite';
4+import { resolve, dirname } from 'path';
5+import { fileURLToPath } from 'url';
6+
7+const __dirname = dirname(fileURLToPath(import.meta.url));
8+const root = resolve(__dirname, '..');
9+
10+// Use esbuild directly via vite's dependency
11+const esbuild = await import('esbuild');
12+await esbuild.build({
13+ entryPoints: [resolve(root, 'src/lib/server/ws-handler.ts')],
14+ bundle: true,
15+ outfile: resolve(root, 'build/ws-handler.js'),
16+ platform: 'node',
17+ format: 'esm',
18+ target: 'node18',
19+ external: ['node-pty', 'ws', 'simple-git', 'shiki'],
20+ banner: {
21+ js: '// Auto-generated — do not edit'
22+ }
23+});
24+
25+console.log(' ✔ Built build/ws-handler.js');
+33,
-1
1@@ -3,7 +3,7 @@
2 import type { ReviewThread } from '$lib/types/index.ts';
3 import { viewMode, commentingLine } from '$lib/stores/ui.ts';
4 import { threadsByFile } from '$lib/stores/review.ts';
5- import { resetHunk } from '$lib/stores/files.ts';
6+ import { resetHunk, diffFiles } from '$lib/stores/files.ts';
7 import CommentEditor from './CommentEditor.svelte';
8 import CommentThread from './CommentThread.svelte';
9
10@@ -12,6 +12,23 @@
11 }
12
13 let { file }: Props = $props();
14+ let loadingFull = $state(false);
15+
16+ async function loadFullFile() {
17+ loadingFull = true;
18+ try {
19+ const res = await fetch(`/api/diff?file=${encodeURIComponent(file.path)}&full=1`);
20+ const data = await res.json();
21+ if (data.files?.[0]) {
22+ // Replace this file in the diffFiles store
23+ diffFiles.update((files) =>
24+ files.map((f) => f.path === file.path ? data.files[0] : f)
25+ );
26+ }
27+ } finally {
28+ loadingFull = false;
29+ }
30+ }
31
32 // Track expanded context lines per hunk boundary
33 let expandedLines = $state<Map<string, DiffLine[]>>(new Map());
34@@ -335,6 +352,21 @@
35 {/if}
36 {/if}
37 {/each}
38+
39+ {#if file.truncated}
40+ <div class="bg-panel border-t border-border px-4 py-3 text-center">
41+ <p class="text-sm text-muted mb-2">
42+ Large file: showing {file.hunks.reduce((s, h) => s + h.lines.length, 0).toLocaleString()} of {(file.totalLines || 0).toLocaleString()} changed lines
43+ </p>
44+ <button
45+ class="text-sm px-4 py-1.5 rounded bg-accent-blue/20 text-accent-blue hover:bg-accent-blue/30 transition-colors disabled:opacity-50"
46+ onclick={loadFullFile}
47+ disabled={loadingFull}
48+ >
49+ {loadingFull ? '⏳ Loading...' : 'Load full file (may be slow)'}
50+ </button>
51+ </div>
52+ {/if}
53 </div>
54
55 <script lang="ts" module>
+2,
-0
1@@ -13,6 +13,8 @@ export interface DiffFile {
2 oldPath?: string;
3 hunks: DiffHunk[];
4 language: string;
5+ totalLines?: number;
6+ truncated?: boolean;
7 }
8
9 export interface DiffHunk {
+45,
-2
1@@ -49,15 +49,58 @@ async function applyHighlighting(files: DiffFile[]): Promise<void> {
2 }
3 }
4
5+const MAX_LINES_PER_FILE = 1500;
6+const MAX_LINES_FOR_HIGHLIGHTING = 5000;
7+
8+function truncateLargeFiles(files: DiffFile[]): void {
9+ for (const file of files) {
10+ let totalLines = 0;
11+ for (const hunk of file.hunks) {
12+ totalLines += hunk.lines.length;
13+ }
14+ file.totalLines = totalLines;
15+
16+ if (totalLines > MAX_LINES_PER_FILE) {
17+ file.truncated = true;
18+ // Keep only first N lines across hunks
19+ let remaining = MAX_LINES_PER_FILE;
20+ for (const hunk of file.hunks) {
21+ if (remaining <= 0) {
22+ hunk.lines = [];
23+ } else if (hunk.lines.length > remaining) {
24+ hunk.lines = hunk.lines.slice(0, remaining);
25+ }
26+ remaining -= hunk.lines.length;
27+ }
28+ // Remove empty hunks
29+ file.hunks = file.hunks.filter((h) => h.lines.length > 0);
30+ }
31+ }
32+}
33+
34 export const GET: RequestHandler = async ({ url }) => {
35 try {
36 const filePath = url.searchParams.get('file') || undefined;
37 const mode = (url.searchParams.get('mode') || 'full') as DiffMode;
38+ const skipTruncation = url.searchParams.get('full') === '1';
39 const raw = await getDiffByMode(mode, filePath);
40 const parsed = parseDiff(raw);
41
42- // Apply syntax highlighting
43- await applyHighlighting(parsed);
44+ // Truncate large files before highlighting (unless full load requested)
45+ if (!skipTruncation) {
46+ truncateLargeFiles(parsed);
47+ } else {
48+ // Still count total lines for display
49+ for (const file of parsed) {
50+ let totalLines = 0;
51+ for (const hunk of file.hunks) totalLines += hunk.lines.length;
52+ file.totalLines = totalLines;
53+ }
54+ }
55+
56+ // Skip highlighting for very large files
57+ const filesToHighlight = parsed.filter((f) => (f.totalLines || 0) <= MAX_LINES_FOR_HIGHLIGHTING);
58+ await applyHighlighting(filesToHighlight);
59
60 return json({ files: parsed, raw });
61 } catch (err) {