Add handling big files
6 files changed,  +109, -14
M bin/lcr.js
+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 () => {
M package.json
+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",
A scripts/build-ws.js
+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');
M src/lib/components/DiffView.svelte
+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>
M src/lib/types/diff.ts
+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 {
M src/routes/api/diff/+server.ts
+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) {