v1
49 files changed,  +7228, -0
A .claude/launch.json
+17, -0
 1@@ -0,0 +1,17 @@
 2+{
 3+  "version": "0.0.1",
 4+  "configurations": [
 5+    {
 6+      "name": "dev",
 7+      "runtimeExecutable": "npm",
 8+      "runtimeArgs": ["run", "dev"],
 9+      "port": 5173
10+    },
11+    {
12+      "name": "dev-ios",
13+      "runtimeExecutable": "/bin/sh",
14+      "runtimeArgs": ["-c", "LCR_REPO_DIR=/Users/alex/Projects/nebenan-iOS VITE_LCR_WS_PORT=6176 npm run dev -- --port 5176"],
15+      "port": 5176
16+    }
17+  ]
18+}
A .gitignore
+26, -0
 1@@ -0,0 +1,26 @@
 2+node_modules
 3+
 4+# Output
 5+.output
 6+.vercel
 7+.netlify
 8+.wrangler
 9+/.svelte-kit
10+/build
11+
12+# OS
13+.DS_Store
14+Thumbs.db
15+
16+# LCR exports
17+.lcr-review.*
18+
19+# Env
20+.env
21+.env.*
22+!.env.example
23+!.env.test
24+
25+# Vite
26+vite.config.js.timestamp-*
27+vite.config.ts.timestamp-*
A .npmrc
+1, -0
1@@ -0,0 +1 @@
2+engine-strict=true
A .vscode/extensions.json
+6, -0
1@@ -0,0 +1,6 @@
2+{
3+	"recommendations": [
4+		"svelte.svelte-vscode",
5+		"bradlc.vscode-tailwindcss"
6+	]
7+}
A .vscode/settings.json
+5, -0
1@@ -0,0 +1,5 @@
2+{
3+	"files.associations": {
4+		"*.css": "tailwindcss"
5+	}
6+}
A README.md
+196, -0
  1@@ -0,0 +1,196 @@
  2+# Local Code Review (lcr)
  3+
  4+Local code review tool for AI-assisted development. Review uncommitted changes with a GitHub PR-like interface — inline comments, suggestions, questions — then approve & commit or send feedback to AI for another round.
  5+
  6+## Why?
  7+
  8+When developing with AI (vibe-coding), AI makes many changes at once. There's no good way to review those changes and give structured feedback before committing. Diff tools show changes but don't let you write comments. Chat interfaces take one comment at a time. This tool gives you the full PR review experience locally:
  9+
 10+- **See diffs** like a GitHub PR (unified or split view, syntax highlighted)
 11+- **Write inline comments** on any line — comments, suggestions, questions
 12+- **Approve individual files** to stage them for commit
 13+- **Toggle between full diff and unstaged-only** to focus on what still needs review
 14+- **Submit a structured review** with all your feedback at once
 15+- **Send to AI** — export review as JSON/Markdown, copy to clipboard, or send directly to Claude Code via built-in terminal
 16+- **Outdated detection** — comments are automatically flagged when the underlying code changes
 17+- **Approve & commit** when you're satisfied
 18+
 19+## Install
 20+
 21+```sh
 22+npm install -g local-code-review
 23+```
 24+
 25+Requires Node.js 18+.
 26+
 27+## Quick Start
 28+
 29+```sh
 30+cd your-project
 31+lcr
 32+```
 33+
 34+Opens a web UI at `http://localhost:5678` showing all uncommitted changes.
 35+
 36+## Usage
 37+
 38+### Reviewing a project
 39+
 40+Run from inside any git repository:
 41+
 42+```sh
 43+lcr                    # Review current directory
 44+lcr /path/to/project   # Review a different project
 45+```
 46+
 47+### Running multiple instances
 48+
 49+Each instance needs its own port:
 50+
 51+```sh
 52+lcr                          # First project on default port 5678
 53+PORT=5679 lcr /path/to/other # Second project on port 5679
 54+```
 55+
 56+### Review flow
 57+
 58+1. Browse changed files in the sidebar
 59+2. Click the `+` button on any line to add a comment
 60+3. Choose comment type: **comment**, **suggestion**, or **question**
 61+4. Add comments across multiple files
 62+5. Approve individual files (checkbox in sidebar) — approved files are staged for git
 63+6. When done, click **Review** and choose:
 64+   - **Request Changes** — sends your review to Claude Code for another iteration
 65+   - **Approve & Commit** — opens commit dialog with approved/all file options
 66+
 67+### Diff modes
 68+
 69+- **Full Diff** — shows all uncommitted changes (staged + unstaged)
 70+- **Needs Review** — shows only unstaged changes, so you can focus on what hasn't been approved yet
 71+
 72+### Export options
 73+
 74+Click **Export** in the toolbar:
 75+
 76+- **Send to Claude Code** — sends review to Claude via the built-in terminal
 77+- **Copy to Clipboard** — paste into any AI chat
 78+- **Export as JSON** — saves `.lcr-review.json` in the project root
 79+- **Export as Markdown** — saves `.lcr-review.md` in the project root
 80+
 81+### Built-in terminal
 82+
 83+The tool includes an embedded terminal that runs an interactive Claude session in your project directory. When you click "Request Changes" or "Send to Claude Code", your review is sent directly to this terminal session. Claude can then make changes, and you can review the next round without leaving the tool.
 84+
 85+The terminal uses a WebSocket connection on a separate port (main port + 1000 by default).
 86+
 87+### Outdated comments
 88+
 89+When AI makes changes after you've submitted a review, comments on lines that changed are automatically marked as **Outdated** with a yellow badge. This happens whenever the diff is refreshed, so you can see which of your comments have already been addressed.
 90+
 91+### File approval
 92+
 93+Click the checkbox next to any file in the sidebar to approve it. Approved files are:
 94+- Staged in git (`git add`)
 95+- Shown with a strikethrough in the sidebar
 96+- Excluded from the "Needs Review" diff mode
 97+
 98+If an approved file is modified again, its approval status is reset.
 99+
100+### Keyboard shortcuts
101+
102+| Key | Action |
103+|-----|--------|
104+| `j` / `k` | Navigate files |
105+| `b` | Toggle sidebar |
106+| `Cmd/Ctrl+Enter` | Submit comment / commit |
107+| `Escape` | Cancel comment |
108+
109+## Configuration
110+
111+### Environment variables
112+
113+| Variable | Default | Description |
114+|----------|---------|-------------|
115+| `PORT` | `5678` | HTTP server port |
116+| `LCR_REPO_DIR` | Current directory | Path to the git repository to review |
117+| `VITE_LCR_WS_PORT` | `PORT + 1000` | WebSocket port for the terminal (dev mode only) |
118+
119+### Claude Code integration
120+
121+The "Send to Claude Code" feature requires [Claude Code](https://docs.anthropic.com/en/docs/claude-code) CLI to be installed. The tool uses whatever model your Claude Code is configured with. To change the default model:
122+
123+```sh
124+claude config set model claude-sonnet-4-6
125+# or
126+claude config set model claude-opus-4-6
127+```
128+
129+## Development
130+
131+### Setup
132+
133+```sh
134+git clone <repo>
135+cd local-code-review
136+npm install
137+npm run dev
138+```
139+
140+The dev server starts at `http://localhost:5173` by default.
141+
142+### Testing with another project
143+
144+Point `LCR_REPO_DIR` at a different git repository:
145+
146+```sh
147+LCR_REPO_DIR=/path/to/your/project npm run dev
148+```
149+
150+To run multiple dev instances simultaneously, each needs its own port and WebSocket port:
151+
152+```sh
153+# Instance 1 (default ports: 5173 + WS on 6173)
154+LCR_REPO_DIR=/path/to/project-a npm run dev
155+
156+# Instance 2 (custom ports: 5174 + WS on 6174)
157+LCR_REPO_DIR=/path/to/project-b VITE_LCR_WS_PORT=6174 npm run dev -- --port 5174
158+```
159+
160+### Building
161+
162+```sh
163+npm run build
164+```
165+
166+Test the production build locally:
167+
168+```sh
169+node bin/lcr.js /path/to/project
170+```
171+
172+### Project structure
173+
174+```
175+src/
176+  lib/
177+    components/    # Svelte UI components (DiffView, FileTree, Terminal, etc.)
178+    server/        # Server-side code (git operations, review store, terminal)
179+    stores/        # Svelte stores (files, review, UI state)
180+    types/         # TypeScript interfaces
181+  routes/
182+    api/           # REST API endpoints (diff, comments, export, commit, etc.)
183+    +page.svelte   # Main page
184+bin/
185+  lcr.js           # CLI entry point
186+```
187+
188+## Notes
189+
190+- Comments are stored in memory and reset when the server restarts
191+- The `.lcr-review.json` and `.lcr-review.md` export files are created in the project root — add them to `.gitignore` if needed
192+- Syntax highlighting is powered by [Shiki](https://shiki.matsu.io/) (same engine as VS Code)
193+- The terminal feature requires `node-pty` which needs a C++ compiler to build (usually available by default on macOS/Linux)
194+
195+## License
196+
197+MIT
A bin/lcr.js
+84, -0
 1@@ -0,0 +1,84 @@
 2+#!/usr/bin/env node
 3+
 4+import { existsSync } from 'fs';
 5+import { resolve, join } from 'path';
 6+import { createServer } from 'http';
 7+import { fileURLToPath } from 'url';
 8+
 9+const args = process.argv.slice(2);
10+const repoDir = resolve(args[0] || process.cwd());
11+
12+// Check if directory is a git repo
13+if (!existsSync(join(repoDir, '.git'))) {
14+	console.error('Error: Not a git repository. Run this command from within a git repo.');
15+	process.exit(1);
16+}
17+
18+// Set repo dir for the server
19+process.env.LCR_REPO_DIR = repoDir;
20+
21+// Find the build directory
22+const __dirname = fileURLToPath(new URL('.', import.meta.url));
23+const buildPath = join(__dirname, '..', 'build');
24+
25+if (!existsSync(join(buildPath, 'handler.js'))) {
26+	console.error('Error: Build not found. Run `npm run build` first.');
27+	process.exit(1);
28+}
29+
30+// Import the SvelteKit handler
31+const { handler } = await import(join(buildPath, 'handler.js'));
32+
33+const DEFAULT_PORT = 5678;
34+const port = parseInt(process.env.PORT || String(DEFAULT_PORT), 10);
35+
36+const server = createServer(handler);
37+
38+// Attach WebSocket for terminal
39+try {
40+	const wsHandler = await import(join(buildPath, 'server', 'chunks', 'ws-handler.js'));
41+	wsHandler.attachWebSocket(server);
42+} catch {
43+	// ws-handler may be bundled differently — try alternative import
44+	try {
45+		const { attachWebSocket } = await import('../src/lib/server/ws-handler.ts');
46+		attachWebSocket(server);
47+	} catch (err) {
48+		console.warn('  Warning: Could not attach terminal WebSocket:', err.message);
49+	}
50+}
51+
52+server.listen(port, '127.0.0.1', async () => {
53+	const url = `http://localhost:${port}`;
54+	console.log(`\n  Local Code Review`);
55+	console.log(`  Reviewing: ${repoDir}`);
56+	console.log(`  Server:    ${url}\n`);
57+
58+	// Open browser
59+	try {
60+		const open = await import('open');
61+		await open.default(url);
62+	} catch {
63+		console.log(`  Open ${url} in your browser`);
64+	}
65+});
66+
67+server.on('error', (err) => {
68+	if (err.code === 'EADDRINUSE') {
69+		console.error(`Port ${port} is in use. Try: PORT=${port + 1} lcr`);
70+		process.exit(1);
71+	}
72+	throw err;
73+});
74+
75+// Graceful shutdown
76+process.on('SIGINT', () => {
77+	console.log('\n  Shutting down...');
78+	server.close();
79+	process.exit(0);
80+});
81+
82+process.on('SIGTERM', () => {
83+	server.close();
84+	process.exit(0);
85+});
A package-lock.json
+3396, -0
   1@@ -0,0 +1,3396 @@
   2+{
   3+	"name": "local-code-review",
   4+	"version": "0.1.0",
   5+	"lockfileVersion": 3,
   6+	"requires": true,
   7+	"packages": {
   8+		"": {
   9+			"name": "local-code-review",
  10+			"version": "0.1.0",
  11+			"license": "MIT",
  12+			"dependencies": {
  13+				"@xterm/addon-fit": "^0.11.0",
  14+				"@xterm/xterm": "^6.0.0",
  15+				"diff2html": "^3.4.56",
  16+				"node-pty": "^1.2.0-beta.12",
  17+				"open": "^11.0.0",
  18+				"shiki": "^4.0.2",
  19+				"simple-git": "^3.33.0",
  20+				"ws": "^8.20.0"
  21+			},
  22+			"bin": {
  23+				"lcr": "bin/lcr.js",
  24+				"local-code-review": "bin/lcr.js"
  25+			},
  26+			"devDependencies": {
  27+				"@sveltejs/adapter-node": "^5.5.2",
  28+				"@sveltejs/kit": "^2.50.2",
  29+				"@sveltejs/vite-plugin-svelte": "^6.2.4",
  30+				"@tailwindcss/vite": "^4.1.18",
  31+				"@types/ws": "^8.18.1",
  32+				"svelte": "^5.54.0",
  33+				"svelte-check": "^4.4.2",
  34+				"tailwindcss": "^4.1.18",
  35+				"typescript": "^5.9.3",
  36+				"vite": "^7.3.1"
  37+			},
  38+			"engines": {
  39+				"node": ">=18"
  40+			}
  41+		},
  42+		"node_modules/@esbuild/aix-ppc64": {
  43+			"version": "0.27.4",
  44+			"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.4.tgz",
  45+			"integrity": "sha512-cQPwL2mp2nSmHHJlCyoXgHGhbEPMrEEU5xhkcy3Hs/O7nGZqEpZ2sUtLaL9MORLtDfRvVl2/3PAuEkYZH0Ty8Q==",
  46+			"cpu": [
  47+				"ppc64"
  48+			],
  49+			"dev": true,
  50+			"license": "MIT",
  51+			"optional": true,
  52+			"os": [
  53+				"aix"
  54+			],
  55+			"engines": {
  56+				"node": ">=18"
  57+			}
  58+		},
  59+		"node_modules/@esbuild/android-arm": {
  60+			"version": "0.27.4",
  61+			"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.4.tgz",
  62+			"integrity": "sha512-X9bUgvxiC8CHAGKYufLIHGXPJWnr0OCdR0anD2e21vdvgCI8lIfqFbnoeOz7lBjdrAGUhqLZLcQo6MLhTO2DKQ==",
  63+			"cpu": [
  64+				"arm"
  65+			],
  66+			"dev": true,
  67+			"license": "MIT",
  68+			"optional": true,
  69+			"os": [
  70+				"android"
  71+			],
  72+			"engines": {
  73+				"node": ">=18"
  74+			}
  75+		},
  76+		"node_modules/@esbuild/android-arm64": {
  77+			"version": "0.27.4",
  78+			"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.4.tgz",
  79+			"integrity": "sha512-gdLscB7v75wRfu7QSm/zg6Rx29VLdy9eTr2t44sfTW7CxwAtQghZ4ZnqHk3/ogz7xao0QAgrkradbBzcqFPasw==",
  80+			"cpu": [
  81+				"arm64"
  82+			],
  83+			"dev": true,
  84+			"license": "MIT",
  85+			"optional": true,
  86+			"os": [
  87+				"android"
  88+			],
  89+			"engines": {
  90+				"node": ">=18"
  91+			}
  92+		},
  93+		"node_modules/@esbuild/android-x64": {
  94+			"version": "0.27.4",
  95+			"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.4.tgz",
  96+			"integrity": "sha512-PzPFnBNVF292sfpfhiyiXCGSn9HZg5BcAz+ivBuSsl6Rk4ga1oEXAamhOXRFyMcjwr2DVtm40G65N3GLeH1Lvw==",
  97+			"cpu": [
  98+				"x64"
  99+			],
 100+			"dev": true,
 101+			"license": "MIT",
 102+			"optional": true,
 103+			"os": [
 104+				"android"
 105+			],
 106+			"engines": {
 107+				"node": ">=18"
 108+			}
 109+		},
 110+		"node_modules/@esbuild/darwin-arm64": {
 111+			"version": "0.27.4",
 112+			"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.4.tgz",
 113+			"integrity": "sha512-b7xaGIwdJlht8ZFCvMkpDN6uiSmnxxK56N2GDTMYPr2/gzvfdQN8rTfBsvVKmIVY/X7EM+/hJKEIbbHs9oA4tQ==",
 114+			"cpu": [
 115+				"arm64"
 116+			],
 117+			"dev": true,
 118+			"license": "MIT",
 119+			"optional": true,
 120+			"os": [
 121+				"darwin"
 122+			],
 123+			"engines": {
 124+				"node": ">=18"
 125+			}
 126+		},
 127+		"node_modules/@esbuild/darwin-x64": {
 128+			"version": "0.27.4",
 129+			"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.4.tgz",
 130+			"integrity": "sha512-sR+OiKLwd15nmCdqpXMnuJ9W2kpy0KigzqScqHI3Hqwr7IXxBp3Yva+yJwoqh7rE8V77tdoheRYataNKL4QrPw==",
 131+			"cpu": [
 132+				"x64"
 133+			],
 134+			"dev": true,
 135+			"license": "MIT",
 136+			"optional": true,
 137+			"os": [
 138+				"darwin"
 139+			],
 140+			"engines": {
 141+				"node": ">=18"
 142+			}
 143+		},
 144+		"node_modules/@esbuild/freebsd-arm64": {
 145+			"version": "0.27.4",
 146+			"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.4.tgz",
 147+			"integrity": "sha512-jnfpKe+p79tCnm4GVav68A7tUFeKQwQyLgESwEAUzyxk/TJr4QdGog9sqWNcUbr/bZt/O/HXouspuQDd9JxFSw==",
 148+			"cpu": [
 149+				"arm64"
 150+			],
 151+			"dev": true,
 152+			"license": "MIT",
 153+			"optional": true,
 154+			"os": [
 155+				"freebsd"
 156+			],
 157+			"engines": {
 158+				"node": ">=18"
 159+			}
 160+		},
 161+		"node_modules/@esbuild/freebsd-x64": {
 162+			"version": "0.27.4",
 163+			"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.4.tgz",
 164+			"integrity": "sha512-2kb4ceA/CpfUrIcTUl1wrP/9ad9Atrp5J94Lq69w7UwOMolPIGrfLSvAKJp0RTvkPPyn6CIWrNy13kyLikZRZQ==",
 165+			"cpu": [
 166+				"x64"
 167+			],
 168+			"dev": true,
 169+			"license": "MIT",
 170+			"optional": true,
 171+			"os": [
 172+				"freebsd"
 173+			],
 174+			"engines": {
 175+				"node": ">=18"
 176+			}
 177+		},
 178+		"node_modules/@esbuild/linux-arm": {
 179+			"version": "0.27.4",
 180+			"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.4.tgz",
 181+			"integrity": "sha512-aBYgcIxX/wd5n2ys0yESGeYMGF+pv6g0DhZr3G1ZG4jMfruU9Tl1i2Z+Wnj9/KjGz1lTLCcorqE2viePZqj4Eg==",
 182+			"cpu": [
 183+				"arm"
 184+			],
 185+			"dev": true,
 186+			"license": "MIT",
 187+			"optional": true,
 188+			"os": [
 189+				"linux"
 190+			],
 191+			"engines": {
 192+				"node": ">=18"
 193+			}
 194+		},
 195+		"node_modules/@esbuild/linux-arm64": {
 196+			"version": "0.27.4",
 197+			"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.4.tgz",
 198+			"integrity": "sha512-7nQOttdzVGth1iz57kxg9uCz57dxQLHWxopL6mYuYthohPKEK0vU0C3O21CcBK6KDlkYVcnDXY099HcCDXd9dA==",
 199+			"cpu": [
 200+				"arm64"
 201+			],
 202+			"dev": true,
 203+			"license": "MIT",
 204+			"optional": true,
 205+			"os": [
 206+				"linux"
 207+			],
 208+			"engines": {
 209+				"node": ">=18"
 210+			}
 211+		},
 212+		"node_modules/@esbuild/linux-ia32": {
 213+			"version": "0.27.4",
 214+			"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.4.tgz",
 215+			"integrity": "sha512-oPtixtAIzgvzYcKBQM/qZ3R+9TEUd1aNJQu0HhGyqtx6oS7qTpvjheIWBbes4+qu1bNlo2V4cbkISr8q6gRBFA==",
 216+			"cpu": [
 217+				"ia32"
 218+			],
 219+			"dev": true,
 220+			"license": "MIT",
 221+			"optional": true,
 222+			"os": [
 223+				"linux"
 224+			],
 225+			"engines": {
 226+				"node": ">=18"
 227+			}
 228+		},
 229+		"node_modules/@esbuild/linux-loong64": {
 230+			"version": "0.27.4",
 231+			"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.4.tgz",
 232+			"integrity": "sha512-8mL/vh8qeCoRcFH2nM8wm5uJP+ZcVYGGayMavi8GmRJjuI3g1v6Z7Ni0JJKAJW+m0EtUuARb6Lmp4hMjzCBWzA==",
 233+			"cpu": [
 234+				"loong64"
 235+			],
 236+			"dev": true,
 237+			"license": "MIT",
 238+			"optional": true,
 239+			"os": [
 240+				"linux"
 241+			],
 242+			"engines": {
 243+				"node": ">=18"
 244+			}
 245+		},
 246+		"node_modules/@esbuild/linux-mips64el": {
 247+			"version": "0.27.4",
 248+			"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.4.tgz",
 249+			"integrity": "sha512-1RdrWFFiiLIW7LQq9Q2NES+HiD4NyT8Itj9AUeCl0IVCA459WnPhREKgwrpaIfTOe+/2rdntisegiPWn/r/aAw==",
 250+			"cpu": [
 251+				"mips64el"
 252+			],
 253+			"dev": true,
 254+			"license": "MIT",
 255+			"optional": true,
 256+			"os": [
 257+				"linux"
 258+			],
 259+			"engines": {
 260+				"node": ">=18"
 261+			}
 262+		},
 263+		"node_modules/@esbuild/linux-ppc64": {
 264+			"version": "0.27.4",
 265+			"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.4.tgz",
 266+			"integrity": "sha512-tLCwNG47l3sd9lpfyx9LAGEGItCUeRCWeAx6x2Jmbav65nAwoPXfewtAdtbtit/pJFLUWOhpv0FpS6GQAmPrHA==",
 267+			"cpu": [
 268+				"ppc64"
 269+			],
 270+			"dev": true,
 271+			"license": "MIT",
 272+			"optional": true,
 273+			"os": [
 274+				"linux"
 275+			],
 276+			"engines": {
 277+				"node": ">=18"
 278+			}
 279+		},
 280+		"node_modules/@esbuild/linux-riscv64": {
 281+			"version": "0.27.4",
 282+			"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.4.tgz",
 283+			"integrity": "sha512-BnASypppbUWyqjd1KIpU4AUBiIhVr6YlHx/cnPgqEkNoVOhHg+YiSVxM1RLfiy4t9cAulbRGTNCKOcqHrEQLIw==",
 284+			"cpu": [
 285+				"riscv64"
 286+			],
 287+			"dev": true,
 288+			"license": "MIT",
 289+			"optional": true,
 290+			"os": [
 291+				"linux"
 292+			],
 293+			"engines": {
 294+				"node": ">=18"
 295+			}
 296+		},
 297+		"node_modules/@esbuild/linux-s390x": {
 298+			"version": "0.27.4",
 299+			"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.4.tgz",
 300+			"integrity": "sha512-+eUqgb/Z7vxVLezG8bVB9SfBie89gMueS+I0xYh2tJdw3vqA/0ImZJ2ROeWwVJN59ihBeZ7Tu92dF/5dy5FttA==",
 301+			"cpu": [
 302+				"s390x"
 303+			],
 304+			"dev": true,
 305+			"license": "MIT",
 306+			"optional": true,
 307+			"os": [
 308+				"linux"
 309+			],
 310+			"engines": {
 311+				"node": ">=18"
 312+			}
 313+		},
 314+		"node_modules/@esbuild/linux-x64": {
 315+			"version": "0.27.4",
 316+			"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.4.tgz",
 317+			"integrity": "sha512-S5qOXrKV8BQEzJPVxAwnryi2+Iq5pB40gTEIT69BQONqR7JH1EPIcQ/Uiv9mCnn05jff9umq/5nqzxlqTOg9NA==",
 318+			"cpu": [
 319+				"x64"
 320+			],
 321+			"dev": true,
 322+			"license": "MIT",
 323+			"optional": true,
 324+			"os": [
 325+				"linux"
 326+			],
 327+			"engines": {
 328+				"node": ">=18"
 329+			}
 330+		},
 331+		"node_modules/@esbuild/netbsd-arm64": {
 332+			"version": "0.27.4",
 333+			"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.4.tgz",
 334+			"integrity": "sha512-xHT8X4sb0GS8qTqiwzHqpY00C95DPAq7nAwX35Ie/s+LO9830hrMd3oX0ZMKLvy7vsonee73x0lmcdOVXFzd6Q==",
 335+			"cpu": [
 336+				"arm64"
 337+			],
 338+			"dev": true,
 339+			"license": "MIT",
 340+			"optional": true,
 341+			"os": [
 342+				"netbsd"
 343+			],
 344+			"engines": {
 345+				"node": ">=18"
 346+			}
 347+		},
 348+		"node_modules/@esbuild/netbsd-x64": {
 349+			"version": "0.27.4",
 350+			"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.4.tgz",
 351+			"integrity": "sha512-RugOvOdXfdyi5Tyv40kgQnI0byv66BFgAqjdgtAKqHoZTbTF2QqfQrFwa7cHEORJf6X2ht+l9ABLMP0dnKYsgg==",
 352+			"cpu": [
 353+				"x64"
 354+			],
 355+			"dev": true,
 356+			"license": "MIT",
 357+			"optional": true,
 358+			"os": [
 359+				"netbsd"
 360+			],
 361+			"engines": {
 362+				"node": ">=18"
 363+			}
 364+		},
 365+		"node_modules/@esbuild/openbsd-arm64": {
 366+			"version": "0.27.4",
 367+			"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.4.tgz",
 368+			"integrity": "sha512-2MyL3IAaTX+1/qP0O1SwskwcwCoOI4kV2IBX1xYnDDqthmq5ArrW94qSIKCAuRraMgPOmG0RDTA74mzYNQA9ow==",
 369+			"cpu": [
 370+				"arm64"
 371+			],
 372+			"dev": true,
 373+			"license": "MIT",
 374+			"optional": true,
 375+			"os": [
 376+				"openbsd"
 377+			],
 378+			"engines": {
 379+				"node": ">=18"
 380+			}
 381+		},
 382+		"node_modules/@esbuild/openbsd-x64": {
 383+			"version": "0.27.4",
 384+			"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.4.tgz",
 385+			"integrity": "sha512-u8fg/jQ5aQDfsnIV6+KwLOf1CmJnfu1ShpwqdwC0uA7ZPwFws55Ngc12vBdeUdnuWoQYx/SOQLGDcdlfXhYmXQ==",
 386+			"cpu": [
 387+				"x64"
 388+			],
 389+			"dev": true,
 390+			"license": "MIT",
 391+			"optional": true,
 392+			"os": [
 393+				"openbsd"
 394+			],
 395+			"engines": {
 396+				"node": ">=18"
 397+			}
 398+		},
 399+		"node_modules/@esbuild/openharmony-arm64": {
 400+			"version": "0.27.4",
 401+			"resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.4.tgz",
 402+			"integrity": "sha512-JkTZrl6VbyO8lDQO3yv26nNr2RM2yZzNrNHEsj9bm6dOwwu9OYN28CjzZkH57bh4w0I2F7IodpQvUAEd1mbWXg==",
 403+			"cpu": [
 404+				"arm64"
 405+			],
 406+			"dev": true,
 407+			"license": "MIT",
 408+			"optional": true,
 409+			"os": [
 410+				"openharmony"
 411+			],
 412+			"engines": {
 413+				"node": ">=18"
 414+			}
 415+		},
 416+		"node_modules/@esbuild/sunos-x64": {
 417+			"version": "0.27.4",
 418+			"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.4.tgz",
 419+			"integrity": "sha512-/gOzgaewZJfeJTlsWhvUEmUG4tWEY2Spp5M20INYRg2ZKl9QPO3QEEgPeRtLjEWSW8FilRNacPOg8R1uaYkA6g==",
 420+			"cpu": [
 421+				"x64"
 422+			],
 423+			"dev": true,
 424+			"license": "MIT",
 425+			"optional": true,
 426+			"os": [
 427+				"sunos"
 428+			],
 429+			"engines": {
 430+				"node": ">=18"
 431+			}
 432+		},
 433+		"node_modules/@esbuild/win32-arm64": {
 434+			"version": "0.27.4",
 435+			"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.4.tgz",
 436+			"integrity": "sha512-Z9SExBg2y32smoDQdf1HRwHRt6vAHLXcxD2uGgO/v2jK7Y718Ix4ndsbNMU/+1Qiem9OiOdaqitioZwxivhXYg==",
 437+			"cpu": [
 438+				"arm64"
 439+			],
 440+			"dev": true,
 441+			"license": "MIT",
 442+			"optional": true,
 443+			"os": [
 444+				"win32"
 445+			],
 446+			"engines": {
 447+				"node": ">=18"
 448+			}
 449+		},
 450+		"node_modules/@esbuild/win32-ia32": {
 451+			"version": "0.27.4",
 452+			"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.4.tgz",
 453+			"integrity": "sha512-DAyGLS0Jz5G5iixEbMHi5KdiApqHBWMGzTtMiJ72ZOLhbu/bzxgAe8Ue8CTS3n3HbIUHQz/L51yMdGMeoxXNJw==",
 454+			"cpu": [
 455+				"ia32"
 456+			],
 457+			"dev": true,
 458+			"license": "MIT",
 459+			"optional": true,
 460+			"os": [
 461+				"win32"
 462+			],
 463+			"engines": {
 464+				"node": ">=18"
 465+			}
 466+		},
 467+		"node_modules/@esbuild/win32-x64": {
 468+			"version": "0.27.4",
 469+			"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.4.tgz",
 470+			"integrity": "sha512-+knoa0BDoeXgkNvvV1vvbZX4+hizelrkwmGJBdT17t8FNPwG2lKemmuMZlmaNQ3ws3DKKCxpb4zRZEIp3UxFCg==",
 471+			"cpu": [
 472+				"x64"
 473+			],
 474+			"dev": true,
 475+			"license": "MIT",
 476+			"optional": true,
 477+			"os": [
 478+				"win32"
 479+			],
 480+			"engines": {
 481+				"node": ">=18"
 482+			}
 483+		},
 484+		"node_modules/@jridgewell/gen-mapping": {
 485+			"version": "0.3.13",
 486+			"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
 487+			"integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
 488+			"dev": true,
 489+			"license": "MIT",
 490+			"dependencies": {
 491+				"@jridgewell/sourcemap-codec": "^1.5.0",
 492+				"@jridgewell/trace-mapping": "^0.3.24"
 493+			}
 494+		},
 495+		"node_modules/@jridgewell/remapping": {
 496+			"version": "2.3.5",
 497+			"resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
 498+			"integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
 499+			"dev": true,
 500+			"license": "MIT",
 501+			"dependencies": {
 502+				"@jridgewell/gen-mapping": "^0.3.5",
 503+				"@jridgewell/trace-mapping": "^0.3.24"
 504+			}
 505+		},
 506+		"node_modules/@jridgewell/resolve-uri": {
 507+			"version": "3.1.2",
 508+			"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
 509+			"integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
 510+			"dev": true,
 511+			"license": "MIT",
 512+			"engines": {
 513+				"node": ">=6.0.0"
 514+			}
 515+		},
 516+		"node_modules/@jridgewell/sourcemap-codec": {
 517+			"version": "1.5.5",
 518+			"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
 519+			"integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
 520+			"dev": true,
 521+			"license": "MIT"
 522+		},
 523+		"node_modules/@jridgewell/trace-mapping": {
 524+			"version": "0.3.31",
 525+			"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
 526+			"integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
 527+			"dev": true,
 528+			"license": "MIT",
 529+			"dependencies": {
 530+				"@jridgewell/resolve-uri": "^3.1.0",
 531+				"@jridgewell/sourcemap-codec": "^1.4.14"
 532+			}
 533+		},
 534+		"node_modules/@kwsites/file-exists": {
 535+			"version": "1.1.1",
 536+			"resolved": "https://registry.npmjs.org/@kwsites/file-exists/-/file-exists-1.1.1.tgz",
 537+			"integrity": "sha512-m9/5YGR18lIwxSFDwfE3oA7bWuq9kdau6ugN4H2rJeyhFQZcG9AgSHkQtSD15a8WvTgfz9aikZMrKPHvbpqFiw==",
 538+			"license": "MIT",
 539+			"dependencies": {
 540+				"debug": "^4.1.1"
 541+			}
 542+		},
 543+		"node_modules/@kwsites/promise-deferred": {
 544+			"version": "1.1.1",
 545+			"resolved": "https://registry.npmjs.org/@kwsites/promise-deferred/-/promise-deferred-1.1.1.tgz",
 546+			"integrity": "sha512-GaHYm+c0O9MjZRu0ongGBRbinu8gVAMd2UZjji6jVmqKtZluZnptXGWhz1E8j8D2HJ3f/yMxKAUC0b+57wncIw==",
 547+			"license": "MIT"
 548+		},
 549+		"node_modules/@polka/url": {
 550+			"version": "1.0.0-next.29",
 551+			"resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz",
 552+			"integrity": "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==",
 553+			"dev": true,
 554+			"license": "MIT"
 555+		},
 556+		"node_modules/@profoundlogic/hogan": {
 557+			"version": "3.0.4",
 558+			"resolved": "https://registry.npmjs.org/@profoundlogic/hogan/-/hogan-3.0.4.tgz",
 559+			"integrity": "sha512-pmNVGuooS30Mm7YbZd5T7E5zYVO6D5Ct91sn4T39mUvMUc3sCGridcnhAufL1/Bz2QzAtzEn0agNrdk3+5yWzw==",
 560+			"license": "Apache-2.0",
 561+			"dependencies": {
 562+				"nopt": "1.0.10"
 563+			},
 564+			"bin": {
 565+				"hulk": "bin/hulk"
 566+			}
 567+		},
 568+		"node_modules/@rollup/plugin-commonjs": {
 569+			"version": "29.0.2",
 570+			"resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-29.0.2.tgz",
 571+			"integrity": "sha512-S/ggWH1LU7jTyi9DxZOKyxpVd4hF/OZ0JrEbeLjXk/DFXwRny0tjD2c992zOUYQobLrVkRVMDdmHP16HKP7GRg==",
 572+			"dev": true,
 573+			"license": "MIT",
 574+			"dependencies": {
 575+				"@rollup/pluginutils": "^5.0.1",
 576+				"commondir": "^1.0.1",
 577+				"estree-walker": "^2.0.2",
 578+				"fdir": "^6.2.0",
 579+				"is-reference": "1.2.1",
 580+				"magic-string": "^0.30.3",
 581+				"picomatch": "^4.0.2"
 582+			},
 583+			"engines": {
 584+				"node": ">=16.0.0 || 14 >= 14.17"
 585+			},
 586+			"peerDependencies": {
 587+				"rollup": "^2.68.0||^3.0.0||^4.0.0"
 588+			},
 589+			"peerDependenciesMeta": {
 590+				"rollup": {
 591+					"optional": true
 592+				}
 593+			}
 594+		},
 595+		"node_modules/@rollup/plugin-json": {
 596+			"version": "6.1.0",
 597+			"resolved": "https://registry.npmjs.org/@rollup/plugin-json/-/plugin-json-6.1.0.tgz",
 598+			"integrity": "sha512-EGI2te5ENk1coGeADSIwZ7G2Q8CJS2sF120T7jLw4xFw9n7wIOXHo+kIYRAoVpJAN+kmqZSoO3Fp4JtoNF4ReA==",
 599+			"dev": true,
 600+			"license": "MIT",
 601+			"dependencies": {
 602+				"@rollup/pluginutils": "^5.1.0"
 603+			},
 604+			"engines": {
 605+				"node": ">=14.0.0"
 606+			},
 607+			"peerDependencies": {
 608+				"rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0"
 609+			},
 610+			"peerDependenciesMeta": {
 611+				"rollup": {
 612+					"optional": true
 613+				}
 614+			}
 615+		},
 616+		"node_modules/@rollup/plugin-node-resolve": {
 617+			"version": "16.0.3",
 618+			"resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-16.0.3.tgz",
 619+			"integrity": "sha512-lUYM3UBGuM93CnMPG1YocWu7X802BrNF3jW2zny5gQyLQgRFJhV1Sq0Zi74+dh/6NBx1DxFC4b4GXg9wUCG5Qg==",
 620+			"dev": true,
 621+			"license": "MIT",
 622+			"dependencies": {
 623+				"@rollup/pluginutils": "^5.0.1",
 624+				"@types/resolve": "1.20.2",
 625+				"deepmerge": "^4.2.2",
 626+				"is-module": "^1.0.0",
 627+				"resolve": "^1.22.1"
 628+			},
 629+			"engines": {
 630+				"node": ">=14.0.0"
 631+			},
 632+			"peerDependencies": {
 633+				"rollup": "^2.78.0||^3.0.0||^4.0.0"
 634+			},
 635+			"peerDependenciesMeta": {
 636+				"rollup": {
 637+					"optional": true
 638+				}
 639+			}
 640+		},
 641+		"node_modules/@rollup/pluginutils": {
 642+			"version": "5.3.0",
 643+			"resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.3.0.tgz",
 644+			"integrity": "sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==",
 645+			"dev": true,
 646+			"license": "MIT",
 647+			"dependencies": {
 648+				"@types/estree": "^1.0.0",
 649+				"estree-walker": "^2.0.2",
 650+				"picomatch": "^4.0.2"
 651+			},
 652+			"engines": {
 653+				"node": ">=14.0.0"
 654+			},
 655+			"peerDependencies": {
 656+				"rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0"
 657+			},
 658+			"peerDependenciesMeta": {
 659+				"rollup": {
 660+					"optional": true
 661+				}
 662+			}
 663+		},
 664+		"node_modules/@rollup/rollup-android-arm-eabi": {
 665+			"version": "4.60.0",
 666+			"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.0.tgz",
 667+			"integrity": "sha512-WOhNW9K8bR3kf4zLxbfg6Pxu2ybOUbB2AjMDHSQx86LIF4rH4Ft7vmMwNt0loO0eonglSNy4cpD3MKXXKQu0/A==",
 668+			"cpu": [
 669+				"arm"
 670+			],
 671+			"dev": true,
 672+			"license": "MIT",
 673+			"optional": true,
 674+			"os": [
 675+				"android"
 676+			]
 677+		},
 678+		"node_modules/@rollup/rollup-android-arm64": {
 679+			"version": "4.60.0",
 680+			"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.0.tgz",
 681+			"integrity": "sha512-u6JHLll5QKRvjciE78bQXDmqRqNs5M/3GVqZeMwvmjaNODJih/WIrJlFVEihvV0MiYFmd+ZyPr9wxOVbPAG2Iw==",
 682+			"cpu": [
 683+				"arm64"
 684+			],
 685+			"dev": true,
 686+			"license": "MIT",
 687+			"optional": true,
 688+			"os": [
 689+				"android"
 690+			]
 691+		},
 692+		"node_modules/@rollup/rollup-darwin-arm64": {
 693+			"version": "4.60.0",
 694+			"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.0.tgz",
 695+			"integrity": "sha512-qEF7CsKKzSRc20Ciu2Zw1wRrBz4g56F7r/vRwY430UPp/nt1x21Q/fpJ9N5l47WWvJlkNCPJz3QRVw008fi7yA==",
 696+			"cpu": [
 697+				"arm64"
 698+			],
 699+			"dev": true,
 700+			"license": "MIT",
 701+			"optional": true,
 702+			"os": [
 703+				"darwin"
 704+			]
 705+		},
 706+		"node_modules/@rollup/rollup-darwin-x64": {
 707+			"version": "4.60.0",
 708+			"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.0.tgz",
 709+			"integrity": "sha512-WADYozJ4QCnXCH4wPB+3FuGmDPoFseVCUrANmA5LWwGmC6FL14BWC7pcq+FstOZv3baGX65tZ378uT6WG8ynTw==",
 710+			"cpu": [
 711+				"x64"
 712+			],
 713+			"dev": true,
 714+			"license": "MIT",
 715+			"optional": true,
 716+			"os": [
 717+				"darwin"
 718+			]
 719+		},
 720+		"node_modules/@rollup/rollup-freebsd-arm64": {
 721+			"version": "4.60.0",
 722+			"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.0.tgz",
 723+			"integrity": "sha512-6b8wGHJlDrGeSE3aH5mGNHBjA0TTkxdoNHik5EkvPHCt351XnigA4pS7Wsj/Eo9Y8RBU6f35cjN9SYmCFBtzxw==",
 724+			"cpu": [
 725+				"arm64"
 726+			],
 727+			"dev": true,
 728+			"license": "MIT",
 729+			"optional": true,
 730+			"os": [
 731+				"freebsd"
 732+			]
 733+		},
 734+		"node_modules/@rollup/rollup-freebsd-x64": {
 735+			"version": "4.60.0",
 736+			"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.0.tgz",
 737+			"integrity": "sha512-h25Ga0t4jaylMB8M/JKAyrvvfxGRjnPQIR8lnCayyzEjEOx2EJIlIiMbhpWxDRKGKF8jbNH01NnN663dH638mA==",
 738+			"cpu": [
 739+				"x64"
 740+			],
 741+			"dev": true,
 742+			"license": "MIT",
 743+			"optional": true,
 744+			"os": [
 745+				"freebsd"
 746+			]
 747+		},
 748+		"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
 749+			"version": "4.60.0",
 750+			"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.0.tgz",
 751+			"integrity": "sha512-RzeBwv0B3qtVBWtcuABtSuCzToo2IEAIQrcyB/b2zMvBWVbjo8bZDjACUpnaafaxhTw2W+imQbP2BD1usasK4g==",
 752+			"cpu": [
 753+				"arm"
 754+			],
 755+			"dev": true,
 756+			"license": "MIT",
 757+			"optional": true,
 758+			"os": [
 759+				"linux"
 760+			]
 761+		},
 762+		"node_modules/@rollup/rollup-linux-arm-musleabihf": {
 763+			"version": "4.60.0",
 764+			"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.0.tgz",
 765+			"integrity": "sha512-Sf7zusNI2CIU1HLzuu9Tc5YGAHEZs5Lu7N1ssJG4Tkw6e0MEsN7NdjUDDfGNHy2IU+ENyWT+L2obgWiguWibWQ==",
 766+			"cpu": [
 767+				"arm"
 768+			],
 769+			"dev": true,
 770+			"license": "MIT",
 771+			"optional": true,
 772+			"os": [
 773+				"linux"
 774+			]
 775+		},
 776+		"node_modules/@rollup/rollup-linux-arm64-gnu": {
 777+			"version": "4.60.0",
 778+			"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.0.tgz",
 779+			"integrity": "sha512-DX2x7CMcrJzsE91q7/O02IJQ5/aLkVtYFryqCjduJhUfGKG6yJV8hxaw8pZa93lLEpPTP/ohdN4wFz7yp/ry9A==",
 780+			"cpu": [
 781+				"arm64"
 782+			],
 783+			"dev": true,
 784+			"license": "MIT",
 785+			"optional": true,
 786+			"os": [
 787+				"linux"
 788+			]
 789+		},
 790+		"node_modules/@rollup/rollup-linux-arm64-musl": {
 791+			"version": "4.60.0",
 792+			"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.0.tgz",
 793+			"integrity": "sha512-09EL+yFVbJZlhcQfShpswwRZ0Rg+z/CsSELFCnPt3iK+iqwGsI4zht3secj5vLEs957QvFFXnzAT0FFPIxSrkQ==",
 794+			"cpu": [
 795+				"arm64"
 796+			],
 797+			"dev": true,
 798+			"license": "MIT",
 799+			"optional": true,
 800+			"os": [
 801+				"linux"
 802+			]
 803+		},
 804+		"node_modules/@rollup/rollup-linux-loong64-gnu": {
 805+			"version": "4.60.0",
 806+			"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.0.tgz",
 807+			"integrity": "sha512-i9IcCMPr3EXm8EQg5jnja0Zyc1iFxJjZWlb4wr7U2Wx/GrddOuEafxRdMPRYVaXjgbhvqalp6np07hN1w9kAKw==",
 808+			"cpu": [
 809+				"loong64"
 810+			],
 811+			"dev": true,
 812+			"license": "MIT",
 813+			"optional": true,
 814+			"os": [
 815+				"linux"
 816+			]
 817+		},
 818+		"node_modules/@rollup/rollup-linux-loong64-musl": {
 819+			"version": "4.60.0",
 820+			"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.0.tgz",
 821+			"integrity": "sha512-DGzdJK9kyJ+B78MCkWeGnpXJ91tK/iKA6HwHxF4TAlPIY7GXEvMe8hBFRgdrR9Ly4qebR/7gfUs9y2IoaVEyog==",
 822+			"cpu": [
 823+				"loong64"
 824+			],
 825+			"dev": true,
 826+			"license": "MIT",
 827+			"optional": true,
 828+			"os": [
 829+				"linux"
 830+			]
 831+		},
 832+		"node_modules/@rollup/rollup-linux-ppc64-gnu": {
 833+			"version": "4.60.0",
 834+			"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.0.tgz",
 835+			"integrity": "sha512-RwpnLsqC8qbS8z1H1AxBA1H6qknR4YpPR9w2XX0vo2Sz10miu57PkNcnHVaZkbqyw/kUWfKMI73jhmfi9BRMUQ==",
 836+			"cpu": [
 837+				"ppc64"
 838+			],
 839+			"dev": true,
 840+			"license": "MIT",
 841+			"optional": true,
 842+			"os": [
 843+				"linux"
 844+			]
 845+		},
 846+		"node_modules/@rollup/rollup-linux-ppc64-musl": {
 847+			"version": "4.60.0",
 848+			"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.0.tgz",
 849+			"integrity": "sha512-Z8pPf54Ly3aqtdWC3G4rFigZgNvd+qJlOE52fmko3KST9SoGfAdSRCwyoyG05q1HrrAblLbk1/PSIV+80/pxLg==",
 850+			"cpu": [
 851+				"ppc64"
 852+			],
 853+			"dev": true,
 854+			"license": "MIT",
 855+			"optional": true,
 856+			"os": [
 857+				"linux"
 858+			]
 859+		},
 860+		"node_modules/@rollup/rollup-linux-riscv64-gnu": {
 861+			"version": "4.60.0",
 862+			"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.0.tgz",
 863+			"integrity": "sha512-3a3qQustp3COCGvnP4SvrMHnPQ9d1vzCakQVRTliaz8cIp/wULGjiGpbcqrkv0WrHTEp8bQD/B3HBjzujVWLOA==",
 864+			"cpu": [
 865+				"riscv64"
 866+			],
 867+			"dev": true,
 868+			"license": "MIT",
 869+			"optional": true,
 870+			"os": [
 871+				"linux"
 872+			]
 873+		},
 874+		"node_modules/@rollup/rollup-linux-riscv64-musl": {
 875+			"version": "4.60.0",
 876+			"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.0.tgz",
 877+			"integrity": "sha512-pjZDsVH/1VsghMJ2/kAaxt6dL0psT6ZexQVrijczOf+PeP2BUqTHYejk3l6TlPRydggINOeNRhvpLa0AYpCWSQ==",
 878+			"cpu": [
 879+				"riscv64"
 880+			],
 881+			"dev": true,
 882+			"license": "MIT",
 883+			"optional": true,
 884+			"os": [
 885+				"linux"
 886+			]
 887+		},
 888+		"node_modules/@rollup/rollup-linux-s390x-gnu": {
 889+			"version": "4.60.0",
 890+			"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.0.tgz",
 891+			"integrity": "sha512-3ObQs0BhvPgiUVZrN7gqCSvmFuMWvWvsjG5ayJ3Lraqv+2KhOsp+pUbigqbeWqueGIsnn+09HBw27rJ+gYK4VQ==",
 892+			"cpu": [
 893+				"s390x"
 894+			],
 895+			"dev": true,
 896+			"license": "MIT",
 897+			"optional": true,
 898+			"os": [
 899+				"linux"
 900+			]
 901+		},
 902+		"node_modules/@rollup/rollup-linux-x64-gnu": {
 903+			"version": "4.60.0",
 904+			"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.0.tgz",
 905+			"integrity": "sha512-EtylprDtQPdS5rXvAayrNDYoJhIz1/vzN2fEubo3yLE7tfAw+948dO0g4M0vkTVFhKojnF+n6C8bDNe+gDRdTg==",
 906+			"cpu": [
 907+				"x64"
 908+			],
 909+			"dev": true,
 910+			"license": "MIT",
 911+			"optional": true,
 912+			"os": [
 913+				"linux"
 914+			]
 915+		},
 916+		"node_modules/@rollup/rollup-linux-x64-musl": {
 917+			"version": "4.60.0",
 918+			"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.0.tgz",
 919+			"integrity": "sha512-k09oiRCi/bHU9UVFqD17r3eJR9bn03TyKraCrlz5ULFJGdJGi7VOmm9jl44vOJvRJ6P7WuBi/s2A97LxxHGIdw==",
 920+			"cpu": [
 921+				"x64"
 922+			],
 923+			"dev": true,
 924+			"license": "MIT",
 925+			"optional": true,
 926+			"os": [
 927+				"linux"
 928+			]
 929+		},
 930+		"node_modules/@rollup/rollup-openbsd-x64": {
 931+			"version": "4.60.0",
 932+			"resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.0.tgz",
 933+			"integrity": "sha512-1o/0/pIhozoSaDJoDcec+IVLbnRtQmHwPV730+AOD29lHEEo4F5BEUB24H0OBdhbBBDwIOSuf7vgg0Ywxdfiiw==",
 934+			"cpu": [
 935+				"x64"
 936+			],
 937+			"dev": true,
 938+			"license": "MIT",
 939+			"optional": true,
 940+			"os": [
 941+				"openbsd"
 942+			]
 943+		},
 944+		"node_modules/@rollup/rollup-openharmony-arm64": {
 945+			"version": "4.60.0",
 946+			"resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.0.tgz",
 947+			"integrity": "sha512-pESDkos/PDzYwtyzB5p/UoNU/8fJo68vcXM9ZW2V0kjYayj1KaaUfi1NmTUTUpMn4UhU4gTuK8gIaFO4UGuMbA==",
 948+			"cpu": [
 949+				"arm64"
 950+			],
 951+			"dev": true,
 952+			"license": "MIT",
 953+			"optional": true,
 954+			"os": [
 955+				"openharmony"
 956+			]
 957+		},
 958+		"node_modules/@rollup/rollup-win32-arm64-msvc": {
 959+			"version": "4.60.0",
 960+			"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.0.tgz",
 961+			"integrity": "sha512-hj1wFStD7B1YBeYmvY+lWXZ7ey73YGPcViMShYikqKT1GtstIKQAtfUI6yrzPjAy/O7pO0VLXGmUVWXQMaYgTQ==",
 962+			"cpu": [
 963+				"arm64"
 964+			],
 965+			"dev": true,
 966+			"license": "MIT",
 967+			"optional": true,
 968+			"os": [
 969+				"win32"
 970+			]
 971+		},
 972+		"node_modules/@rollup/rollup-win32-ia32-msvc": {
 973+			"version": "4.60.0",
 974+			"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.0.tgz",
 975+			"integrity": "sha512-SyaIPFoxmUPlNDq5EHkTbiKzmSEmq/gOYFI/3HHJ8iS/v1mbugVa7dXUzcJGQfoytp9DJFLhHH4U3/eTy2Bq4w==",
 976+			"cpu": [
 977+				"ia32"
 978+			],
 979+			"dev": true,
 980+			"license": "MIT",
 981+			"optional": true,
 982+			"os": [
 983+				"win32"
 984+			]
 985+		},
 986+		"node_modules/@rollup/rollup-win32-x64-gnu": {
 987+			"version": "4.60.0",
 988+			"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.0.tgz",
 989+			"integrity": "sha512-RdcryEfzZr+lAr5kRm2ucN9aVlCCa2QNq4hXelZxb8GG0NJSazq44Z3PCCc8wISRuCVnGs0lQJVX5Vp6fKA+IA==",
 990+			"cpu": [
 991+				"x64"
 992+			],
 993+			"dev": true,
 994+			"license": "MIT",
 995+			"optional": true,
 996+			"os": [
 997+				"win32"
 998+			]
 999+		},
1000+		"node_modules/@rollup/rollup-win32-x64-msvc": {
1001+			"version": "4.60.0",
1002+			"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.0.tgz",
1003+			"integrity": "sha512-PrsWNQ8BuE00O3Xsx3ALh2Df8fAj9+cvvX9AIA6o4KpATR98c9mud4XtDWVvsEuyia5U4tVSTKygawyJkjm60w==",
1004+			"cpu": [
1005+				"x64"
1006+			],
1007+			"dev": true,
1008+			"license": "MIT",
1009+			"optional": true,
1010+			"os": [
1011+				"win32"
1012+			]
1013+		},
1014+		"node_modules/@shikijs/core": {
1015+			"version": "4.0.2",
1016+			"resolved": "https://registry.npmjs.org/@shikijs/core/-/core-4.0.2.tgz",
1017+			"integrity": "sha512-hxT0YF4ExEqB8G/qFdtJvpmHXBYJ2lWW7qTHDarVkIudPFE6iCIrqdgWxGn5s+ppkGXI0aEGlibI0PAyzP3zlw==",
1018+			"license": "MIT",
1019+			"dependencies": {
1020+				"@shikijs/primitive": "4.0.2",
1021+				"@shikijs/types": "4.0.2",
1022+				"@shikijs/vscode-textmate": "^10.0.2",
1023+				"@types/hast": "^3.0.4",
1024+				"hast-util-to-html": "^9.0.5"
1025+			},
1026+			"engines": {
1027+				"node": ">=20"
1028+			}
1029+		},
1030+		"node_modules/@shikijs/engine-javascript": {
1031+			"version": "4.0.2",
1032+			"resolved": "https://registry.npmjs.org/@shikijs/engine-javascript/-/engine-javascript-4.0.2.tgz",
1033+			"integrity": "sha512-7PW0Nm49DcoUIQEXlJhNNBHyoGMjalRETTCcjMqEaMoJRLljy1Bi/EGV3/qLBgLKQejdspiiYuHGQW6dX94Nag==",
1034+			"license": "MIT",
1035+			"dependencies": {
1036+				"@shikijs/types": "4.0.2",
1037+				"@shikijs/vscode-textmate": "^10.0.2",
1038+				"oniguruma-to-es": "^4.3.4"
1039+			},
1040+			"engines": {
1041+				"node": ">=20"
1042+			}
1043+		},
1044+		"node_modules/@shikijs/engine-oniguruma": {
1045+			"version": "4.0.2",
1046+			"resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-4.0.2.tgz",
1047+			"integrity": "sha512-UpCB9Y2sUKlS9z8juFSKz7ZtysmeXCgnRF0dlhXBkmQnek7lAToPte8DkxmEYGNTMii72zU/lyXiCB6StuZeJg==",
1048+			"license": "MIT",
1049+			"dependencies": {
1050+				"@shikijs/types": "4.0.2",
1051+				"@shikijs/vscode-textmate": "^10.0.2"
1052+			},
1053+			"engines": {
1054+				"node": ">=20"
1055+			}
1056+		},
1057+		"node_modules/@shikijs/langs": {
1058+			"version": "4.0.2",
1059+			"resolved": "https://registry.npmjs.org/@shikijs/langs/-/langs-4.0.2.tgz",
1060+			"integrity": "sha512-KaXby5dvoeuZzN0rYQiPMjFoUrz4hgwIE+D6Du9owcHcl6/g16/yT5BQxSW5cGt2MZBz6Hl0YuRqf12omRfUUg==",
1061+			"license": "MIT",
1062+			"dependencies": {
1063+				"@shikijs/types": "4.0.2"
1064+			},
1065+			"engines": {
1066+				"node": ">=20"
1067+			}
1068+		},
1069+		"node_modules/@shikijs/primitive": {
1070+			"version": "4.0.2",
1071+			"resolved": "https://registry.npmjs.org/@shikijs/primitive/-/primitive-4.0.2.tgz",
1072+			"integrity": "sha512-M6UMPrSa3fN5ayeJwFVl9qWofl273wtK1VG8ySDZ1mQBfhCpdd8nEx7nPZ/tk7k+TYcpqBZzj/AnwxT9lO+HJw==",
1073+			"license": "MIT",
1074+			"dependencies": {
1075+				"@shikijs/types": "4.0.2",
1076+				"@shikijs/vscode-textmate": "^10.0.2",
1077+				"@types/hast": "^3.0.4"
1078+			},
1079+			"engines": {
1080+				"node": ">=20"
1081+			}
1082+		},
1083+		"node_modules/@shikijs/themes": {
1084+			"version": "4.0.2",
1085+			"resolved": "https://registry.npmjs.org/@shikijs/themes/-/themes-4.0.2.tgz",
1086+			"integrity": "sha512-mjCafwt8lJJaVSsQvNVrJumbnnj1RI8jbUKrPKgE6E3OvQKxnuRoBaYC51H4IGHePsGN/QtALglWBU7DoKDFnA==",
1087+			"license": "MIT",
1088+			"dependencies": {
1089+				"@shikijs/types": "4.0.2"
1090+			},
1091+			"engines": {
1092+				"node": ">=20"
1093+			}
1094+		},
1095+		"node_modules/@shikijs/types": {
1096+			"version": "4.0.2",
1097+			"resolved": "https://registry.npmjs.org/@shikijs/types/-/types-4.0.2.tgz",
1098+			"integrity": "sha512-qzbeRooUTPnLE+sHD/Z8DStmaDgnbbc/pMrU203950aRqjX/6AFHeDYT+j00y2lPdz0ywJKx7o/7qnqTivtlXg==",
1099+			"license": "MIT",
1100+			"dependencies": {
1101+				"@shikijs/vscode-textmate": "^10.0.2",
1102+				"@types/hast": "^3.0.4"
1103+			},
1104+			"engines": {
1105+				"node": ">=20"
1106+			}
1107+		},
1108+		"node_modules/@shikijs/vscode-textmate": {
1109+			"version": "10.0.2",
1110+			"resolved": "https://registry.npmjs.org/@shikijs/vscode-textmate/-/vscode-textmate-10.0.2.tgz",
1111+			"integrity": "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==",
1112+			"license": "MIT"
1113+		},
1114+		"node_modules/@standard-schema/spec": {
1115+			"version": "1.1.0",
1116+			"resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz",
1117+			"integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==",
1118+			"dev": true,
1119+			"license": "MIT"
1120+		},
1121+		"node_modules/@sveltejs/acorn-typescript": {
1122+			"version": "1.0.9",
1123+			"resolved": "https://registry.npmjs.org/@sveltejs/acorn-typescript/-/acorn-typescript-1.0.9.tgz",
1124+			"integrity": "sha512-lVJX6qEgs/4DOcRTpo56tmKzVPtoWAaVbL4hfO7t7NVwl9AAXzQR6cihesW1BmNMPl+bK6dreu2sOKBP2Q9CIA==",
1125+			"dev": true,
1126+			"license": "MIT",
1127+			"peerDependencies": {
1128+				"acorn": "^8.9.0"
1129+			}
1130+		},
1131+		"node_modules/@sveltejs/adapter-node": {
1132+			"version": "5.5.4",
1133+			"resolved": "https://registry.npmjs.org/@sveltejs/adapter-node/-/adapter-node-5.5.4.tgz",
1134+			"integrity": "sha512-45X92CXW+2J8ZUzPv3eLlKWEzINKiiGeFWTjyER4ZN4sGgNoaoeSkCY/QYNxHpPXy71QPsctwccBo9jJs0ySPQ==",
1135+			"dev": true,
1136+			"license": "MIT",
1137+			"dependencies": {
1138+				"@rollup/plugin-commonjs": "^29.0.0",
1139+				"@rollup/plugin-json": "^6.1.0",
1140+				"@rollup/plugin-node-resolve": "^16.0.0",
1141+				"rollup": "^4.59.0"
1142+			},
1143+			"peerDependencies": {
1144+				"@sveltejs/kit": "^2.4.0"
1145+			}
1146+		},
1147+		"node_modules/@sveltejs/kit": {
1148+			"version": "2.55.0",
1149+			"resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.55.0.tgz",
1150+			"integrity": "sha512-MdFRjevVxmAknf2NbaUkDF16jSIzXMWd4Nfah0Qp8TtQVoSp3bV4jKt8mX7z7qTUTWvgSaxtR0EG5WJf53gcuA==",
1151+			"dev": true,
1152+			"license": "MIT",
1153+			"dependencies": {
1154+				"@standard-schema/spec": "^1.0.0",
1155+				"@sveltejs/acorn-typescript": "^1.0.5",
1156+				"@types/cookie": "^0.6.0",
1157+				"acorn": "^8.14.1",
1158+				"cookie": "^0.6.0",
1159+				"devalue": "^5.6.4",
1160+				"esm-env": "^1.2.2",
1161+				"kleur": "^4.1.5",
1162+				"magic-string": "^0.30.5",
1163+				"mrmime": "^2.0.0",
1164+				"set-cookie-parser": "^3.0.0",
1165+				"sirv": "^3.0.0"
1166+			},
1167+			"bin": {
1168+				"svelte-kit": "svelte-kit.js"
1169+			},
1170+			"engines": {
1171+				"node": ">=18.13"
1172+			},
1173+			"peerDependencies": {
1174+				"@opentelemetry/api": "^1.0.0",
1175+				"@sveltejs/vite-plugin-svelte": "^3.0.0 || ^4.0.0-next.1 || ^5.0.0 || ^6.0.0-next.0 || ^7.0.0",
1176+				"svelte": "^4.0.0 || ^5.0.0-next.0",
1177+				"typescript": "^5.3.3",
1178+				"vite": "^5.0.3 || ^6.0.0 || ^7.0.0-beta.0 || ^8.0.0"
1179+			},
1180+			"peerDependenciesMeta": {
1181+				"@opentelemetry/api": {
1182+					"optional": true
1183+				},
1184+				"typescript": {
1185+					"optional": true
1186+				}
1187+			}
1188+		},
1189+		"node_modules/@sveltejs/vite-plugin-svelte": {
1190+			"version": "6.2.4",
1191+			"resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-6.2.4.tgz",
1192+			"integrity": "sha512-ou/d51QSdTyN26D7h6dSpusAKaZkAiGM55/AKYi+9AGZw7q85hElbjK3kEyzXHhLSnRISHOYzVge6x0jRZ7DXA==",
1193+			"dev": true,
1194+			"license": "MIT",
1195+			"dependencies": {
1196+				"@sveltejs/vite-plugin-svelte-inspector": "^5.0.0",
1197+				"deepmerge": "^4.3.1",
1198+				"magic-string": "^0.30.21",
1199+				"obug": "^2.1.0",
1200+				"vitefu": "^1.1.1"
1201+			},
1202+			"engines": {
1203+				"node": "^20.19 || ^22.12 || >=24"
1204+			},
1205+			"peerDependencies": {
1206+				"svelte": "^5.0.0",
1207+				"vite": "^6.3.0 || ^7.0.0"
1208+			}
1209+		},
1210+		"node_modules/@sveltejs/vite-plugin-svelte-inspector": {
1211+			"version": "5.0.2",
1212+			"resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte-inspector/-/vite-plugin-svelte-inspector-5.0.2.tgz",
1213+			"integrity": "sha512-TZzRTcEtZffICSAoZGkPSl6Etsj2torOVrx6Uw0KpXxrec9Gg6jFWQ60Q3+LmNGfZSxHRCZL7vXVZIWmuV50Ig==",
1214+			"dev": true,
1215+			"license": "MIT",
1216+			"dependencies": {
1217+				"obug": "^2.1.0"
1218+			},
1219+			"engines": {
1220+				"node": "^20.19 || ^22.12 || >=24"
1221+			},
1222+			"peerDependencies": {
1223+				"@sveltejs/vite-plugin-svelte": "^6.0.0-next.0",
1224+				"svelte": "^5.0.0",
1225+				"vite": "^6.3.0 || ^7.0.0"
1226+			}
1227+		},
1228+		"node_modules/@tailwindcss/node": {
1229+			"version": "4.2.2",
1230+			"resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.2.2.tgz",
1231+			"integrity": "sha512-pXS+wJ2gZpVXqFaUEjojq7jzMpTGf8rU6ipJz5ovJV6PUGmlJ+jvIwGrzdHdQ80Sg+wmQxUFuoW1UAAwHNEdFA==",
1232+			"dev": true,
1233+			"license": "MIT",
1234+			"dependencies": {
1235+				"@jridgewell/remapping": "^2.3.5",
1236+				"enhanced-resolve": "^5.19.0",
1237+				"jiti": "^2.6.1",
1238+				"lightningcss": "1.32.0",
1239+				"magic-string": "^0.30.21",
1240+				"source-map-js": "^1.2.1",
1241+				"tailwindcss": "4.2.2"
1242+			}
1243+		},
1244+		"node_modules/@tailwindcss/oxide": {
1245+			"version": "4.2.2",
1246+			"resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.2.2.tgz",
1247+			"integrity": "sha512-qEUA07+E5kehxYp9BVMpq9E8vnJuBHfJEC0vPC5e7iL/hw7HR61aDKoVoKzrG+QKp56vhNZe4qwkRmMC0zDLvg==",
1248+			"dev": true,
1249+			"license": "MIT",
1250+			"engines": {
1251+				"node": ">= 20"
1252+			},
1253+			"optionalDependencies": {
1254+				"@tailwindcss/oxide-android-arm64": "4.2.2",
1255+				"@tailwindcss/oxide-darwin-arm64": "4.2.2",
1256+				"@tailwindcss/oxide-darwin-x64": "4.2.2",
1257+				"@tailwindcss/oxide-freebsd-x64": "4.2.2",
1258+				"@tailwindcss/oxide-linux-arm-gnueabihf": "4.2.2",
1259+				"@tailwindcss/oxide-linux-arm64-gnu": "4.2.2",
1260+				"@tailwindcss/oxide-linux-arm64-musl": "4.2.2",
1261+				"@tailwindcss/oxide-linux-x64-gnu": "4.2.2",
1262+				"@tailwindcss/oxide-linux-x64-musl": "4.2.2",
1263+				"@tailwindcss/oxide-wasm32-wasi": "4.2.2",
1264+				"@tailwindcss/oxide-win32-arm64-msvc": "4.2.2",
1265+				"@tailwindcss/oxide-win32-x64-msvc": "4.2.2"
1266+			}
1267+		},
1268+		"node_modules/@tailwindcss/oxide-android-arm64": {
1269+			"version": "4.2.2",
1270+			"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.2.2.tgz",
1271+			"integrity": "sha512-dXGR1n+P3B6748jZO/SvHZq7qBOqqzQ+yFrXpoOWWALWndF9MoSKAT3Q0fYgAzYzGhxNYOoysRvYlpixRBBoDg==",
1272+			"cpu": [
1273+				"arm64"
1274+			],
1275+			"dev": true,
1276+			"license": "MIT",
1277+			"optional": true,
1278+			"os": [
1279+				"android"
1280+			],
1281+			"engines": {
1282+				"node": ">= 20"
1283+			}
1284+		},
1285+		"node_modules/@tailwindcss/oxide-darwin-arm64": {
1286+			"version": "4.2.2",
1287+			"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.2.2.tgz",
1288+			"integrity": "sha512-iq9Qjr6knfMpZHj55/37ouZeykwbDqF21gPFtfnhCCKGDcPI/21FKC9XdMO/XyBM7qKORx6UIhGgg6jLl7BZlg==",
1289+			"cpu": [
1290+				"arm64"
1291+			],
1292+			"dev": true,
1293+			"license": "MIT",
1294+			"optional": true,
1295+			"os": [
1296+				"darwin"
1297+			],
1298+			"engines": {
1299+				"node": ">= 20"
1300+			}
1301+		},
1302+		"node_modules/@tailwindcss/oxide-darwin-x64": {
1303+			"version": "4.2.2",
1304+			"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.2.2.tgz",
1305+			"integrity": "sha512-BlR+2c3nzc8f2G639LpL89YY4bdcIdUmiOOkv2GQv4/4M0vJlpXEa0JXNHhCHU7VWOKWT/CjqHdTP8aUuDJkuw==",
1306+			"cpu": [
1307+				"x64"
1308+			],
1309+			"dev": true,
1310+			"license": "MIT",
1311+			"optional": true,
1312+			"os": [
1313+				"darwin"
1314+			],
1315+			"engines": {
1316+				"node": ">= 20"
1317+			}
1318+		},
1319+		"node_modules/@tailwindcss/oxide-freebsd-x64": {
1320+			"version": "4.2.2",
1321+			"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.2.2.tgz",
1322+			"integrity": "sha512-YUqUgrGMSu2CDO82hzlQ5qSb5xmx3RUrke/QgnoEx7KvmRJHQuZHZmZTLSuuHwFf0DJPybFMXMYf+WJdxHy/nQ==",
1323+			"cpu": [
1324+				"x64"
1325+			],
1326+			"dev": true,
1327+			"license": "MIT",
1328+			"optional": true,
1329+			"os": [
1330+				"freebsd"
1331+			],
1332+			"engines": {
1333+				"node": ">= 20"
1334+			}
1335+		},
1336+		"node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": {
1337+			"version": "4.2.2",
1338+			"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.2.2.tgz",
1339+			"integrity": "sha512-FPdhvsW6g06T9BWT0qTwiVZYE2WIFo2dY5aCSpjG/S/u1tby+wXoslXS0kl3/KXnULlLr1E3NPRRw0g7t2kgaQ==",
1340+			"cpu": [
1341+				"arm"
1342+			],
1343+			"dev": true,
1344+			"license": "MIT",
1345+			"optional": true,
1346+			"os": [
1347+				"linux"
1348+			],
1349+			"engines": {
1350+				"node": ">= 20"
1351+			}
1352+		},
1353+		"node_modules/@tailwindcss/oxide-linux-arm64-gnu": {
1354+			"version": "4.2.2",
1355+			"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.2.2.tgz",
1356+			"integrity": "sha512-4og1V+ftEPXGttOO7eCmW7VICmzzJWgMx+QXAJRAhjrSjumCwWqMfkDrNu1LXEQzNAwz28NCUpucgQPrR4S2yw==",
1357+			"cpu": [
1358+				"arm64"
1359+			],
1360+			"dev": true,
1361+			"license": "MIT",
1362+			"optional": true,
1363+			"os": [
1364+				"linux"
1365+			],
1366+			"engines": {
1367+				"node": ">= 20"
1368+			}
1369+		},
1370+		"node_modules/@tailwindcss/oxide-linux-arm64-musl": {
1371+			"version": "4.2.2",
1372+			"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.2.2.tgz",
1373+			"integrity": "sha512-oCfG/mS+/+XRlwNjnsNLVwnMWYH7tn/kYPsNPh+JSOMlnt93mYNCKHYzylRhI51X+TbR+ufNhhKKzm6QkqX8ag==",
1374+			"cpu": [
1375+				"arm64"
1376+			],
1377+			"dev": true,
1378+			"license": "MIT",
1379+			"optional": true,
1380+			"os": [
1381+				"linux"
1382+			],
1383+			"engines": {
1384+				"node": ">= 20"
1385+			}
1386+		},
1387+		"node_modules/@tailwindcss/oxide-linux-x64-gnu": {
1388+			"version": "4.2.2",
1389+			"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.2.2.tgz",
1390+			"integrity": "sha512-rTAGAkDgqbXHNp/xW0iugLVmX62wOp2PoE39BTCGKjv3Iocf6AFbRP/wZT/kuCxC9QBh9Pu8XPkv/zCZB2mcMg==",
1391+			"cpu": [
1392+				"x64"
1393+			],
1394+			"dev": true,
1395+			"license": "MIT",
1396+			"optional": true,
1397+			"os": [
1398+				"linux"
1399+			],
1400+			"engines": {
1401+				"node": ">= 20"
1402+			}
1403+		},
1404+		"node_modules/@tailwindcss/oxide-linux-x64-musl": {
1405+			"version": "4.2.2",
1406+			"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.2.2.tgz",
1407+			"integrity": "sha512-XW3t3qwbIwiSyRCggeO2zxe3KWaEbM0/kW9e8+0XpBgyKU4ATYzcVSMKteZJ1iukJ3HgHBjbg9P5YPRCVUxlnQ==",
1408+			"cpu": [
1409+				"x64"
1410+			],
1411+			"dev": true,
1412+			"license": "MIT",
1413+			"optional": true,
1414+			"os": [
1415+				"linux"
1416+			],
1417+			"engines": {
1418+				"node": ">= 20"
1419+			}
1420+		},
1421+		"node_modules/@tailwindcss/oxide-wasm32-wasi": {
1422+			"version": "4.2.2",
1423+			"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.2.2.tgz",
1424+			"integrity": "sha512-eKSztKsmEsn1O5lJ4ZAfyn41NfG7vzCg496YiGtMDV86jz1q/irhms5O0VrY6ZwTUkFy/EKG3RfWgxSI3VbZ8Q==",
1425+			"bundleDependencies": [
1426+				"@napi-rs/wasm-runtime",
1427+				"@emnapi/core",
1428+				"@emnapi/runtime",
1429+				"@tybys/wasm-util",
1430+				"@emnapi/wasi-threads",
1431+				"tslib"
1432+			],
1433+			"cpu": [
1434+				"wasm32"
1435+			],
1436+			"dev": true,
1437+			"license": "MIT",
1438+			"optional": true,
1439+			"dependencies": {
1440+				"@emnapi/core": "^1.8.1",
1441+				"@emnapi/runtime": "^1.8.1",
1442+				"@emnapi/wasi-threads": "^1.1.0",
1443+				"@napi-rs/wasm-runtime": "^1.1.1",
1444+				"@tybys/wasm-util": "^0.10.1",
1445+				"tslib": "^2.8.1"
1446+			},
1447+			"engines": {
1448+				"node": ">=14.0.0"
1449+			}
1450+		},
1451+		"node_modules/@tailwindcss/oxide-win32-arm64-msvc": {
1452+			"version": "4.2.2",
1453+			"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.2.2.tgz",
1454+			"integrity": "sha512-qPmaQM4iKu5mxpsrWZMOZRgZv1tOZpUm+zdhhQP0VhJfyGGO3aUKdbh3gDZc/dPLQwW4eSqWGrrcWNBZWUWaXQ==",
1455+			"cpu": [
1456+				"arm64"
1457+			],
1458+			"dev": true,
1459+			"license": "MIT",
1460+			"optional": true,
1461+			"os": [
1462+				"win32"
1463+			],
1464+			"engines": {
1465+				"node": ">= 20"
1466+			}
1467+		},
1468+		"node_modules/@tailwindcss/oxide-win32-x64-msvc": {
1469+			"version": "4.2.2",
1470+			"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.2.2.tgz",
1471+			"integrity": "sha512-1T/37VvI7WyH66b+vqHj/cLwnCxt7Qt3WFu5Q8hk65aOvlwAhs7rAp1VkulBJw/N4tMirXjVnylTR72uI0HGcA==",
1472+			"cpu": [
1473+				"x64"
1474+			],
1475+			"dev": true,
1476+			"license": "MIT",
1477+			"optional": true,
1478+			"os": [
1479+				"win32"
1480+			],
1481+			"engines": {
1482+				"node": ">= 20"
1483+			}
1484+		},
1485+		"node_modules/@tailwindcss/vite": {
1486+			"version": "4.2.2",
1487+			"resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.2.2.tgz",
1488+			"integrity": "sha512-mEiF5HO1QqCLXoNEfXVA1Tzo+cYsrqV7w9Juj2wdUFyW07JRenqMG225MvPwr3ZD9N1bFQj46X7r33iHxLUW0w==",
1489+			"dev": true,
1490+			"license": "MIT",
1491+			"dependencies": {
1492+				"@tailwindcss/node": "4.2.2",
1493+				"@tailwindcss/oxide": "4.2.2",
1494+				"tailwindcss": "4.2.2"
1495+			},
1496+			"peerDependencies": {
1497+				"vite": "^5.2.0 || ^6 || ^7 || ^8"
1498+			}
1499+		},
1500+		"node_modules/@types/cookie": {
1501+			"version": "0.6.0",
1502+			"resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz",
1503+			"integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==",
1504+			"dev": true,
1505+			"license": "MIT"
1506+		},
1507+		"node_modules/@types/estree": {
1508+			"version": "1.0.8",
1509+			"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
1510+			"integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
1511+			"dev": true,
1512+			"license": "MIT"
1513+		},
1514+		"node_modules/@types/hast": {
1515+			"version": "3.0.4",
1516+			"resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz",
1517+			"integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==",
1518+			"license": "MIT",
1519+			"dependencies": {
1520+				"@types/unist": "*"
1521+			}
1522+		},
1523+		"node_modules/@types/mdast": {
1524+			"version": "4.0.4",
1525+			"resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz",
1526+			"integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==",
1527+			"license": "MIT",
1528+			"dependencies": {
1529+				"@types/unist": "*"
1530+			}
1531+		},
1532+		"node_modules/@types/node": {
1533+			"version": "25.5.0",
1534+			"resolved": "https://registry.npmjs.org/@types/node/-/node-25.5.0.tgz",
1535+			"integrity": "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw==",
1536+			"dev": true,
1537+			"license": "MIT",
1538+			"dependencies": {
1539+				"undici-types": "~7.18.0"
1540+			}
1541+		},
1542+		"node_modules/@types/resolve": {
1543+			"version": "1.20.2",
1544+			"resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz",
1545+			"integrity": "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==",
1546+			"dev": true,
1547+			"license": "MIT"
1548+		},
1549+		"node_modules/@types/trusted-types": {
1550+			"version": "2.0.7",
1551+			"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
1552+			"integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
1553+			"dev": true,
1554+			"license": "MIT"
1555+		},
1556+		"node_modules/@types/unist": {
1557+			"version": "3.0.3",
1558+			"resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz",
1559+			"integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==",
1560+			"license": "MIT"
1561+		},
1562+		"node_modules/@types/ws": {
1563+			"version": "8.18.1",
1564+			"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz",
1565+			"integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==",
1566+			"dev": true,
1567+			"license": "MIT",
1568+			"dependencies": {
1569+				"@types/node": "*"
1570+			}
1571+		},
1572+		"node_modules/@typescript-eslint/types": {
1573+			"version": "8.57.2",
1574+			"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.57.2.tgz",
1575+			"integrity": "sha512-/iZM6FnM4tnx9csuTxspMW4BOSegshwX5oBDznJ7S4WggL7Vczz5d2W11ecc4vRrQMQHXRSxzrCsyG5EsPPTbA==",
1576+			"dev": true,
1577+			"license": "MIT",
1578+			"engines": {
1579+				"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
1580+			},
1581+			"funding": {
1582+				"type": "opencollective",
1583+				"url": "https://opencollective.com/typescript-eslint"
1584+			}
1585+		},
1586+		"node_modules/@ungap/structured-clone": {
1587+			"version": "1.3.0",
1588+			"resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz",
1589+			"integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==",
1590+			"license": "ISC"
1591+		},
1592+		"node_modules/@xterm/addon-fit": {
1593+			"version": "0.11.0",
1594+			"resolved": "https://registry.npmjs.org/@xterm/addon-fit/-/addon-fit-0.11.0.tgz",
1595+			"integrity": "sha512-jYcgT6xtVYhnhgxh3QgYDnnNMYTcf8ElbxxFzX0IZo+vabQqSPAjC3c1wJrKB5E19VwQei89QCiZZP86DCPF7g==",
1596+			"license": "MIT"
1597+		},
1598+		"node_modules/@xterm/xterm": {
1599+			"version": "6.0.0",
1600+			"resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-6.0.0.tgz",
1601+			"integrity": "sha512-TQwDdQGtwwDt+2cgKDLn0IRaSxYu1tSUjgKarSDkUM0ZNiSRXFpjxEsvc/Zgc5kq5omJ+V0a8/kIM2WD3sMOYg==",
1602+			"license": "MIT",
1603+			"workspaces": [
1604+				"addons/*"
1605+			]
1606+		},
1607+		"node_modules/abbrev": {
1608+			"version": "1.1.1",
1609+			"resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz",
1610+			"integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==",
1611+			"license": "ISC"
1612+		},
1613+		"node_modules/acorn": {
1614+			"version": "8.16.0",
1615+			"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz",
1616+			"integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==",
1617+			"dev": true,
1618+			"license": "MIT",
1619+			"bin": {
1620+				"acorn": "bin/acorn"
1621+			},
1622+			"engines": {
1623+				"node": ">=0.4.0"
1624+			}
1625+		},
1626+		"node_modules/aria-query": {
1627+			"version": "5.3.1",
1628+			"resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.1.tgz",
1629+			"integrity": "sha512-Z/ZeOgVl7bcSYZ/u/rh0fOpvEpq//LZmdbkXyc7syVzjPAhfOa9ebsdTSjEBDU4vs5nC98Kfduj1uFo0qyET3g==",
1630+			"dev": true,
1631+			"license": "Apache-2.0",
1632+			"engines": {
1633+				"node": ">= 0.4"
1634+			}
1635+		},
1636+		"node_modules/axobject-query": {
1637+			"version": "4.1.0",
1638+			"resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz",
1639+			"integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==",
1640+			"dev": true,
1641+			"license": "Apache-2.0",
1642+			"engines": {
1643+				"node": ">= 0.4"
1644+			}
1645+		},
1646+		"node_modules/bundle-name": {
1647+			"version": "4.1.0",
1648+			"resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz",
1649+			"integrity": "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==",
1650+			"license": "MIT",
1651+			"dependencies": {
1652+				"run-applescript": "^7.0.0"
1653+			},
1654+			"engines": {
1655+				"node": ">=18"
1656+			},
1657+			"funding": {
1658+				"url": "https://github.com/sponsors/sindresorhus"
1659+			}
1660+		},
1661+		"node_modules/ccount": {
1662+			"version": "2.0.1",
1663+			"resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz",
1664+			"integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==",
1665+			"license": "MIT",
1666+			"funding": {
1667+				"type": "github",
1668+				"url": "https://github.com/sponsors/wooorm"
1669+			}
1670+		},
1671+		"node_modules/character-entities-html4": {
1672+			"version": "2.1.0",
1673+			"resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz",
1674+			"integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==",
1675+			"license": "MIT",
1676+			"funding": {
1677+				"type": "github",
1678+				"url": "https://github.com/sponsors/wooorm"
1679+			}
1680+		},
1681+		"node_modules/character-entities-legacy": {
1682+			"version": "3.0.0",
1683+			"resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz",
1684+			"integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==",
1685+			"license": "MIT",
1686+			"funding": {
1687+				"type": "github",
1688+				"url": "https://github.com/sponsors/wooorm"
1689+			}
1690+		},
1691+		"node_modules/chokidar": {
1692+			"version": "4.0.3",
1693+			"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz",
1694+			"integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==",
1695+			"dev": true,
1696+			"license": "MIT",
1697+			"dependencies": {
1698+				"readdirp": "^4.0.1"
1699+			},
1700+			"engines": {
1701+				"node": ">= 14.16.0"
1702+			},
1703+			"funding": {
1704+				"url": "https://paulmillr.com/funding/"
1705+			}
1706+		},
1707+		"node_modules/clsx": {
1708+			"version": "2.1.1",
1709+			"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
1710+			"integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
1711+			"dev": true,
1712+			"license": "MIT",
1713+			"engines": {
1714+				"node": ">=6"
1715+			}
1716+		},
1717+		"node_modules/comma-separated-tokens": {
1718+			"version": "2.0.3",
1719+			"resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz",
1720+			"integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==",
1721+			"license": "MIT",
1722+			"funding": {
1723+				"type": "github",
1724+				"url": "https://github.com/sponsors/wooorm"
1725+			}
1726+		},
1727+		"node_modules/commondir": {
1728+			"version": "1.0.1",
1729+			"resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz",
1730+			"integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==",
1731+			"dev": true,
1732+			"license": "MIT"
1733+		},
1734+		"node_modules/cookie": {
1735+			"version": "0.6.0",
1736+			"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz",
1737+			"integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==",
1738+			"dev": true,
1739+			"license": "MIT",
1740+			"engines": {
1741+				"node": ">= 0.6"
1742+			}
1743+		},
1744+		"node_modules/debug": {
1745+			"version": "4.4.3",
1746+			"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
1747+			"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
1748+			"license": "MIT",
1749+			"dependencies": {
1750+				"ms": "^2.1.3"
1751+			},
1752+			"engines": {
1753+				"node": ">=6.0"
1754+			},
1755+			"peerDependenciesMeta": {
1756+				"supports-color": {
1757+					"optional": true
1758+				}
1759+			}
1760+		},
1761+		"node_modules/deepmerge": {
1762+			"version": "4.3.1",
1763+			"resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz",
1764+			"integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==",
1765+			"dev": true,
1766+			"license": "MIT",
1767+			"engines": {
1768+				"node": ">=0.10.0"
1769+			}
1770+		},
1771+		"node_modules/default-browser": {
1772+			"version": "5.5.0",
1773+			"resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.5.0.tgz",
1774+			"integrity": "sha512-H9LMLr5zwIbSxrmvikGuI/5KGhZ8E2zH3stkMgM5LpOWDutGM2JZaj460Udnf1a+946zc7YBgrqEWwbk7zHvGw==",
1775+			"license": "MIT",
1776+			"dependencies": {
1777+				"bundle-name": "^4.1.0",
1778+				"default-browser-id": "^5.0.0"
1779+			},
1780+			"engines": {
1781+				"node": ">=18"
1782+			},
1783+			"funding": {
1784+				"url": "https://github.com/sponsors/sindresorhus"
1785+			}
1786+		},
1787+		"node_modules/default-browser-id": {
1788+			"version": "5.0.1",
1789+			"resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.1.tgz",
1790+			"integrity": "sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==",
1791+			"license": "MIT",
1792+			"engines": {
1793+				"node": ">=18"
1794+			},
1795+			"funding": {
1796+				"url": "https://github.com/sponsors/sindresorhus"
1797+			}
1798+		},
1799+		"node_modules/define-lazy-prop": {
1800+			"version": "3.0.0",
1801+			"resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz",
1802+			"integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==",
1803+			"license": "MIT",
1804+			"engines": {
1805+				"node": ">=12"
1806+			},
1807+			"funding": {
1808+				"url": "https://github.com/sponsors/sindresorhus"
1809+			}
1810+		},
1811+		"node_modules/dequal": {
1812+			"version": "2.0.3",
1813+			"resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
1814+			"integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==",
1815+			"license": "MIT",
1816+			"engines": {
1817+				"node": ">=6"
1818+			}
1819+		},
1820+		"node_modules/detect-libc": {
1821+			"version": "2.1.2",
1822+			"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
1823+			"integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
1824+			"dev": true,
1825+			"license": "Apache-2.0",
1826+			"engines": {
1827+				"node": ">=8"
1828+			}
1829+		},
1830+		"node_modules/devalue": {
1831+			"version": "5.6.4",
1832+			"resolved": "https://registry.npmjs.org/devalue/-/devalue-5.6.4.tgz",
1833+			"integrity": "sha512-Gp6rDldRsFh/7XuouDbxMH3Mx8GMCcgzIb1pDTvNyn8pZGQ22u+Wa+lGV9dQCltFQ7uVw0MhRyb8XDskNFOReA==",
1834+			"dev": true,
1835+			"license": "MIT"
1836+		},
1837+		"node_modules/devlop": {
1838+			"version": "1.1.0",
1839+			"resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz",
1840+			"integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==",
1841+			"license": "MIT",
1842+			"dependencies": {
1843+				"dequal": "^2.0.0"
1844+			},
1845+			"funding": {
1846+				"type": "github",
1847+				"url": "https://github.com/sponsors/wooorm"
1848+			}
1849+		},
1850+		"node_modules/diff": {
1851+			"version": "8.0.4",
1852+			"resolved": "https://registry.npmjs.org/diff/-/diff-8.0.4.tgz",
1853+			"integrity": "sha512-DPi0FmjiSU5EvQV0++GFDOJ9ASQUVFh5kD+OzOnYdi7n3Wpm9hWWGfB/O2blfHcMVTL5WkQXSnRiK9makhrcnw==",
1854+			"license": "BSD-3-Clause",
1855+			"engines": {
1856+				"node": ">=0.3.1"
1857+			}
1858+		},
1859+		"node_modules/diff2html": {
1860+			"version": "3.4.56",
1861+			"resolved": "https://registry.npmjs.org/diff2html/-/diff2html-3.4.56.tgz",
1862+			"integrity": "sha512-u9gfn+BlbHcyO7vItCIC4z49LJDUt31tODzOfAuJ5R1E7IdlRL6KjugcB9zOpejD+XiR+dDZbsnHSQ3g6A/u8A==",
1863+			"license": "MIT",
1864+			"dependencies": {
1865+				"@profoundlogic/hogan": "^3.0.4",
1866+				"diff": "^8.0.3"
1867+			},
1868+			"engines": {
1869+				"node": ">=12"
1870+			},
1871+			"optionalDependencies": {
1872+				"highlight.js": "11.11.1"
1873+			}
1874+		},
1875+		"node_modules/enhanced-resolve": {
1876+			"version": "5.20.1",
1877+			"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.20.1.tgz",
1878+			"integrity": "sha512-Qohcme7V1inbAfvjItgw0EaxVX5q2rdVEZHRBrEQdRZTssLDGsL8Lwrznl8oQ/6kuTJONLaDcGjkNP247XEhcA==",
1879+			"dev": true,
1880+			"license": "MIT",
1881+			"dependencies": {
1882+				"graceful-fs": "^4.2.4",
1883+				"tapable": "^2.3.0"
1884+			},
1885+			"engines": {
1886+				"node": ">=10.13.0"
1887+			}
1888+		},
1889+		"node_modules/esbuild": {
1890+			"version": "0.27.4",
1891+			"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.4.tgz",
1892+			"integrity": "sha512-Rq4vbHnYkK5fws5NF7MYTU68FPRE1ajX7heQ/8QXXWqNgqqJ/GkmmyxIzUnf2Sr/bakf8l54716CcMGHYhMrrQ==",
1893+			"dev": true,
1894+			"hasInstallScript": true,
1895+			"license": "MIT",
1896+			"bin": {
1897+				"esbuild": "bin/esbuild"
1898+			},
1899+			"engines": {
1900+				"node": ">=18"
1901+			},
1902+			"optionalDependencies": {
1903+				"@esbuild/aix-ppc64": "0.27.4",
1904+				"@esbuild/android-arm": "0.27.4",
1905+				"@esbuild/android-arm64": "0.27.4",
1906+				"@esbuild/android-x64": "0.27.4",
1907+				"@esbuild/darwin-arm64": "0.27.4",
1908+				"@esbuild/darwin-x64": "0.27.4",
1909+				"@esbuild/freebsd-arm64": "0.27.4",
1910+				"@esbuild/freebsd-x64": "0.27.4",
1911+				"@esbuild/linux-arm": "0.27.4",
1912+				"@esbuild/linux-arm64": "0.27.4",
1913+				"@esbuild/linux-ia32": "0.27.4",
1914+				"@esbuild/linux-loong64": "0.27.4",
1915+				"@esbuild/linux-mips64el": "0.27.4",
1916+				"@esbuild/linux-ppc64": "0.27.4",
1917+				"@esbuild/linux-riscv64": "0.27.4",
1918+				"@esbuild/linux-s390x": "0.27.4",
1919+				"@esbuild/linux-x64": "0.27.4",
1920+				"@esbuild/netbsd-arm64": "0.27.4",
1921+				"@esbuild/netbsd-x64": "0.27.4",
1922+				"@esbuild/openbsd-arm64": "0.27.4",
1923+				"@esbuild/openbsd-x64": "0.27.4",
1924+				"@esbuild/openharmony-arm64": "0.27.4",
1925+				"@esbuild/sunos-x64": "0.27.4",
1926+				"@esbuild/win32-arm64": "0.27.4",
1927+				"@esbuild/win32-ia32": "0.27.4",
1928+				"@esbuild/win32-x64": "0.27.4"
1929+			}
1930+		},
1931+		"node_modules/esm-env": {
1932+			"version": "1.2.2",
1933+			"resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.2.2.tgz",
1934+			"integrity": "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==",
1935+			"dev": true,
1936+			"license": "MIT"
1937+		},
1938+		"node_modules/esrap": {
1939+			"version": "2.2.4",
1940+			"resolved": "https://registry.npmjs.org/esrap/-/esrap-2.2.4.tgz",
1941+			"integrity": "sha512-suICpxAmZ9A8bzJjEl/+rLJiDKC0X4gYWUxT6URAWBLvlXmtbZd5ySMu/N2ZGEtMCAmflUDPSehrP9BQcsGcSg==",
1942+			"dev": true,
1943+			"license": "MIT",
1944+			"dependencies": {
1945+				"@jridgewell/sourcemap-codec": "^1.4.15",
1946+				"@typescript-eslint/types": "^8.2.0"
1947+			}
1948+		},
1949+		"node_modules/estree-walker": {
1950+			"version": "2.0.2",
1951+			"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
1952+			"integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
1953+			"dev": true,
1954+			"license": "MIT"
1955+		},
1956+		"node_modules/fdir": {
1957+			"version": "6.5.0",
1958+			"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
1959+			"integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
1960+			"dev": true,
1961+			"license": "MIT",
1962+			"engines": {
1963+				"node": ">=12.0.0"
1964+			},
1965+			"peerDependencies": {
1966+				"picomatch": "^3 || ^4"
1967+			},
1968+			"peerDependenciesMeta": {
1969+				"picomatch": {
1970+					"optional": true
1971+				}
1972+			}
1973+		},
1974+		"node_modules/fsevents": {
1975+			"version": "2.3.3",
1976+			"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
1977+			"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
1978+			"dev": true,
1979+			"hasInstallScript": true,
1980+			"license": "MIT",
1981+			"optional": true,
1982+			"os": [
1983+				"darwin"
1984+			],
1985+			"engines": {
1986+				"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
1987+			}
1988+		},
1989+		"node_modules/function-bind": {
1990+			"version": "1.1.2",
1991+			"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
1992+			"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
1993+			"dev": true,
1994+			"license": "MIT",
1995+			"funding": {
1996+				"url": "https://github.com/sponsors/ljharb"
1997+			}
1998+		},
1999+		"node_modules/graceful-fs": {
2000+			"version": "4.2.11",
2001+			"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
2002+			"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
2003+			"dev": true,
2004+			"license": "ISC"
2005+		},
2006+		"node_modules/hasown": {
2007+			"version": "2.0.2",
2008+			"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
2009+			"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
2010+			"dev": true,
2011+			"license": "MIT",
2012+			"dependencies": {
2013+				"function-bind": "^1.1.2"
2014+			},
2015+			"engines": {
2016+				"node": ">= 0.4"
2017+			}
2018+		},
2019+		"node_modules/hast-util-to-html": {
2020+			"version": "9.0.5",
2021+			"resolved": "https://registry.npmjs.org/hast-util-to-html/-/hast-util-to-html-9.0.5.tgz",
2022+			"integrity": "sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw==",
2023+			"license": "MIT",
2024+			"dependencies": {
2025+				"@types/hast": "^3.0.0",
2026+				"@types/unist": "^3.0.0",
2027+				"ccount": "^2.0.0",
2028+				"comma-separated-tokens": "^2.0.0",
2029+				"hast-util-whitespace": "^3.0.0",
2030+				"html-void-elements": "^3.0.0",
2031+				"mdast-util-to-hast": "^13.0.0",
2032+				"property-information": "^7.0.0",
2033+				"space-separated-tokens": "^2.0.0",
2034+				"stringify-entities": "^4.0.0",
2035+				"zwitch": "^2.0.4"
2036+			},
2037+			"funding": {
2038+				"type": "opencollective",
2039+				"url": "https://opencollective.com/unified"
2040+			}
2041+		},
2042+		"node_modules/hast-util-whitespace": {
2043+			"version": "3.0.0",
2044+			"resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz",
2045+			"integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==",
2046+			"license": "MIT",
2047+			"dependencies": {
2048+				"@types/hast": "^3.0.0"
2049+			},
2050+			"funding": {
2051+				"type": "opencollective",
2052+				"url": "https://opencollective.com/unified"
2053+			}
2054+		},
2055+		"node_modules/highlight.js": {
2056+			"version": "11.11.1",
2057+			"resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.11.1.tgz",
2058+			"integrity": "sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==",
2059+			"license": "BSD-3-Clause",
2060+			"optional": true,
2061+			"engines": {
2062+				"node": ">=12.0.0"
2063+			}
2064+		},
2065+		"node_modules/html-void-elements": {
2066+			"version": "3.0.0",
2067+			"resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-3.0.0.tgz",
2068+			"integrity": "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==",
2069+			"license": "MIT",
2070+			"funding": {
2071+				"type": "github",
2072+				"url": "https://github.com/sponsors/wooorm"
2073+			}
2074+		},
2075+		"node_modules/is-core-module": {
2076+			"version": "2.16.1",
2077+			"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz",
2078+			"integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==",
2079+			"dev": true,
2080+			"license": "MIT",
2081+			"dependencies": {
2082+				"hasown": "^2.0.2"
2083+			},
2084+			"engines": {
2085+				"node": ">= 0.4"
2086+			},
2087+			"funding": {
2088+				"url": "https://github.com/sponsors/ljharb"
2089+			}
2090+		},
2091+		"node_modules/is-docker": {
2092+			"version": "3.0.0",
2093+			"resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz",
2094+			"integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==",
2095+			"license": "MIT",
2096+			"bin": {
2097+				"is-docker": "cli.js"
2098+			},
2099+			"engines": {
2100+				"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
2101+			},
2102+			"funding": {
2103+				"url": "https://github.com/sponsors/sindresorhus"
2104+			}
2105+		},
2106+		"node_modules/is-in-ssh": {
2107+			"version": "1.0.0",
2108+			"resolved": "https://registry.npmjs.org/is-in-ssh/-/is-in-ssh-1.0.0.tgz",
2109+			"integrity": "sha512-jYa6Q9rH90kR1vKB6NM7qqd1mge3Fx4Dhw5TVlK1MUBqhEOuCagrEHMevNuCcbECmXZ0ThXkRm+Ymr51HwEPAw==",
2110+			"license": "MIT",
2111+			"engines": {
2112+				"node": ">=20"
2113+			},
2114+			"funding": {
2115+				"url": "https://github.com/sponsors/sindresorhus"
2116+			}
2117+		},
2118+		"node_modules/is-inside-container": {
2119+			"version": "1.0.0",
2120+			"resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz",
2121+			"integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==",
2122+			"license": "MIT",
2123+			"dependencies": {
2124+				"is-docker": "^3.0.0"
2125+			},
2126+			"bin": {
2127+				"is-inside-container": "cli.js"
2128+			},
2129+			"engines": {
2130+				"node": ">=14.16"
2131+			},
2132+			"funding": {
2133+				"url": "https://github.com/sponsors/sindresorhus"
2134+			}
2135+		},
2136+		"node_modules/is-module": {
2137+			"version": "1.0.0",
2138+			"resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz",
2139+			"integrity": "sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==",
2140+			"dev": true,
2141+			"license": "MIT"
2142+		},
2143+		"node_modules/is-reference": {
2144+			"version": "1.2.1",
2145+			"resolved": "https://registry.npmjs.org/is-reference/-/is-reference-1.2.1.tgz",
2146+			"integrity": "sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==",
2147+			"dev": true,
2148+			"license": "MIT",
2149+			"dependencies": {
2150+				"@types/estree": "*"
2151+			}
2152+		},
2153+		"node_modules/is-wsl": {
2154+			"version": "3.1.1",
2155+			"resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.1.tgz",
2156+			"integrity": "sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==",
2157+			"license": "MIT",
2158+			"dependencies": {
2159+				"is-inside-container": "^1.0.0"
2160+			},
2161+			"engines": {
2162+				"node": ">=16"
2163+			},
2164+			"funding": {
2165+				"url": "https://github.com/sponsors/sindresorhus"
2166+			}
2167+		},
2168+		"node_modules/jiti": {
2169+			"version": "2.6.1",
2170+			"resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz",
2171+			"integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==",
2172+			"dev": true,
2173+			"license": "MIT",
2174+			"bin": {
2175+				"jiti": "lib/jiti-cli.mjs"
2176+			}
2177+		},
2178+		"node_modules/kleur": {
2179+			"version": "4.1.5",
2180+			"resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz",
2181+			"integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==",
2182+			"dev": true,
2183+			"license": "MIT",
2184+			"engines": {
2185+				"node": ">=6"
2186+			}
2187+		},
2188+		"node_modules/lightningcss": {
2189+			"version": "1.32.0",
2190+			"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz",
2191+			"integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==",
2192+			"dev": true,
2193+			"license": "MPL-2.0",
2194+			"dependencies": {
2195+				"detect-libc": "^2.0.3"
2196+			},
2197+			"engines": {
2198+				"node": ">= 12.0.0"
2199+			},
2200+			"funding": {
2201+				"type": "opencollective",
2202+				"url": "https://opencollective.com/parcel"
2203+			},
2204+			"optionalDependencies": {
2205+				"lightningcss-android-arm64": "1.32.0",
2206+				"lightningcss-darwin-arm64": "1.32.0",
2207+				"lightningcss-darwin-x64": "1.32.0",
2208+				"lightningcss-freebsd-x64": "1.32.0",
2209+				"lightningcss-linux-arm-gnueabihf": "1.32.0",
2210+				"lightningcss-linux-arm64-gnu": "1.32.0",
2211+				"lightningcss-linux-arm64-musl": "1.32.0",
2212+				"lightningcss-linux-x64-gnu": "1.32.0",
2213+				"lightningcss-linux-x64-musl": "1.32.0",
2214+				"lightningcss-win32-arm64-msvc": "1.32.0",
2215+				"lightningcss-win32-x64-msvc": "1.32.0"
2216+			}
2217+		},
2218+		"node_modules/lightningcss-android-arm64": {
2219+			"version": "1.32.0",
2220+			"resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz",
2221+			"integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==",
2222+			"cpu": [
2223+				"arm64"
2224+			],
2225+			"dev": true,
2226+			"license": "MPL-2.0",
2227+			"optional": true,
2228+			"os": [
2229+				"android"
2230+			],
2231+			"engines": {
2232+				"node": ">= 12.0.0"
2233+			},
2234+			"funding": {
2235+				"type": "opencollective",
2236+				"url": "https://opencollective.com/parcel"
2237+			}
2238+		},
2239+		"node_modules/lightningcss-darwin-arm64": {
2240+			"version": "1.32.0",
2241+			"resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz",
2242+			"integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==",
2243+			"cpu": [
2244+				"arm64"
2245+			],
2246+			"dev": true,
2247+			"license": "MPL-2.0",
2248+			"optional": true,
2249+			"os": [
2250+				"darwin"
2251+			],
2252+			"engines": {
2253+				"node": ">= 12.0.0"
2254+			},
2255+			"funding": {
2256+				"type": "opencollective",
2257+				"url": "https://opencollective.com/parcel"
2258+			}
2259+		},
2260+		"node_modules/lightningcss-darwin-x64": {
2261+			"version": "1.32.0",
2262+			"resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz",
2263+			"integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==",
2264+			"cpu": [
2265+				"x64"
2266+			],
2267+			"dev": true,
2268+			"license": "MPL-2.0",
2269+			"optional": true,
2270+			"os": [
2271+				"darwin"
2272+			],
2273+			"engines": {
2274+				"node": ">= 12.0.0"
2275+			},
2276+			"funding": {
2277+				"type": "opencollective",
2278+				"url": "https://opencollective.com/parcel"
2279+			}
2280+		},
2281+		"node_modules/lightningcss-freebsd-x64": {
2282+			"version": "1.32.0",
2283+			"resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz",
2284+			"integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==",
2285+			"cpu": [
2286+				"x64"
2287+			],
2288+			"dev": true,
2289+			"license": "MPL-2.0",
2290+			"optional": true,
2291+			"os": [
2292+				"freebsd"
2293+			],
2294+			"engines": {
2295+				"node": ">= 12.0.0"
2296+			},
2297+			"funding": {
2298+				"type": "opencollective",
2299+				"url": "https://opencollective.com/parcel"
2300+			}
2301+		},
2302+		"node_modules/lightningcss-linux-arm-gnueabihf": {
2303+			"version": "1.32.0",
2304+			"resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz",
2305+			"integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==",
2306+			"cpu": [
2307+				"arm"
2308+			],
2309+			"dev": true,
2310+			"license": "MPL-2.0",
2311+			"optional": true,
2312+			"os": [
2313+				"linux"
2314+			],
2315+			"engines": {
2316+				"node": ">= 12.0.0"
2317+			},
2318+			"funding": {
2319+				"type": "opencollective",
2320+				"url": "https://opencollective.com/parcel"
2321+			}
2322+		},
2323+		"node_modules/lightningcss-linux-arm64-gnu": {
2324+			"version": "1.32.0",
2325+			"resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz",
2326+			"integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==",
2327+			"cpu": [
2328+				"arm64"
2329+			],
2330+			"dev": true,
2331+			"license": "MPL-2.0",
2332+			"optional": true,
2333+			"os": [
2334+				"linux"
2335+			],
2336+			"engines": {
2337+				"node": ">= 12.0.0"
2338+			},
2339+			"funding": {
2340+				"type": "opencollective",
2341+				"url": "https://opencollective.com/parcel"
2342+			}
2343+		},
2344+		"node_modules/lightningcss-linux-arm64-musl": {
2345+			"version": "1.32.0",
2346+			"resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz",
2347+			"integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==",
2348+			"cpu": [
2349+				"arm64"
2350+			],
2351+			"dev": true,
2352+			"license": "MPL-2.0",
2353+			"optional": true,
2354+			"os": [
2355+				"linux"
2356+			],
2357+			"engines": {
2358+				"node": ">= 12.0.0"
2359+			},
2360+			"funding": {
2361+				"type": "opencollective",
2362+				"url": "https://opencollective.com/parcel"
2363+			}
2364+		},
2365+		"node_modules/lightningcss-linux-x64-gnu": {
2366+			"version": "1.32.0",
2367+			"resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz",
2368+			"integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==",
2369+			"cpu": [
2370+				"x64"
2371+			],
2372+			"dev": true,
2373+			"license": "MPL-2.0",
2374+			"optional": true,
2375+			"os": [
2376+				"linux"
2377+			],
2378+			"engines": {
2379+				"node": ">= 12.0.0"
2380+			},
2381+			"funding": {
2382+				"type": "opencollective",
2383+				"url": "https://opencollective.com/parcel"
2384+			}
2385+		},
2386+		"node_modules/lightningcss-linux-x64-musl": {
2387+			"version": "1.32.0",
2388+			"resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz",
2389+			"integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==",
2390+			"cpu": [
2391+				"x64"
2392+			],
2393+			"dev": true,
2394+			"license": "MPL-2.0",
2395+			"optional": true,
2396+			"os": [
2397+				"linux"
2398+			],
2399+			"engines": {
2400+				"node": ">= 12.0.0"
2401+			},
2402+			"funding": {
2403+				"type": "opencollective",
2404+				"url": "https://opencollective.com/parcel"
2405+			}
2406+		},
2407+		"node_modules/lightningcss-win32-arm64-msvc": {
2408+			"version": "1.32.0",
2409+			"resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz",
2410+			"integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==",
2411+			"cpu": [
2412+				"arm64"
2413+			],
2414+			"dev": true,
2415+			"license": "MPL-2.0",
2416+			"optional": true,
2417+			"os": [
2418+				"win32"
2419+			],
2420+			"engines": {
2421+				"node": ">= 12.0.0"
2422+			},
2423+			"funding": {
2424+				"type": "opencollective",
2425+				"url": "https://opencollective.com/parcel"
2426+			}
2427+		},
2428+		"node_modules/lightningcss-win32-x64-msvc": {
2429+			"version": "1.32.0",
2430+			"resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz",
2431+			"integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==",
2432+			"cpu": [
2433+				"x64"
2434+			],
2435+			"dev": true,
2436+			"license": "MPL-2.0",
2437+			"optional": true,
2438+			"os": [
2439+				"win32"
2440+			],
2441+			"engines": {
2442+				"node": ">= 12.0.0"
2443+			},
2444+			"funding": {
2445+				"type": "opencollective",
2446+				"url": "https://opencollective.com/parcel"
2447+			}
2448+		},
2449+		"node_modules/locate-character": {
2450+			"version": "3.0.0",
2451+			"resolved": "https://registry.npmjs.org/locate-character/-/locate-character-3.0.0.tgz",
2452+			"integrity": "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==",
2453+			"dev": true,
2454+			"license": "MIT"
2455+		},
2456+		"node_modules/magic-string": {
2457+			"version": "0.30.21",
2458+			"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
2459+			"integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",
2460+			"dev": true,
2461+			"license": "MIT",
2462+			"dependencies": {
2463+				"@jridgewell/sourcemap-codec": "^1.5.5"
2464+			}
2465+		},
2466+		"node_modules/mdast-util-to-hast": {
2467+			"version": "13.2.1",
2468+			"resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.1.tgz",
2469+			"integrity": "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==",
2470+			"license": "MIT",
2471+			"dependencies": {
2472+				"@types/hast": "^3.0.0",
2473+				"@types/mdast": "^4.0.0",
2474+				"@ungap/structured-clone": "^1.0.0",
2475+				"devlop": "^1.0.0",
2476+				"micromark-util-sanitize-uri": "^2.0.0",
2477+				"trim-lines": "^3.0.0",
2478+				"unist-util-position": "^5.0.0",
2479+				"unist-util-visit": "^5.0.0",
2480+				"vfile": "^6.0.0"
2481+			},
2482+			"funding": {
2483+				"type": "opencollective",
2484+				"url": "https://opencollective.com/unified"
2485+			}
2486+		},
2487+		"node_modules/micromark-util-character": {
2488+			"version": "2.1.1",
2489+			"resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz",
2490+			"integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==",
2491+			"funding": [
2492+				{
2493+					"type": "GitHub Sponsors",
2494+					"url": "https://github.com/sponsors/unifiedjs"
2495+				},
2496+				{
2497+					"type": "OpenCollective",
2498+					"url": "https://opencollective.com/unified"
2499+				}
2500+			],
2501+			"license": "MIT",
2502+			"dependencies": {
2503+				"micromark-util-symbol": "^2.0.0",
2504+				"micromark-util-types": "^2.0.0"
2505+			}
2506+		},
2507+		"node_modules/micromark-util-encode": {
2508+			"version": "2.0.1",
2509+			"resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz",
2510+			"integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==",
2511+			"funding": [
2512+				{
2513+					"type": "GitHub Sponsors",
2514+					"url": "https://github.com/sponsors/unifiedjs"
2515+				},
2516+				{
2517+					"type": "OpenCollective",
2518+					"url": "https://opencollective.com/unified"
2519+				}
2520+			],
2521+			"license": "MIT"
2522+		},
2523+		"node_modules/micromark-util-sanitize-uri": {
2524+			"version": "2.0.1",
2525+			"resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz",
2526+			"integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==",
2527+			"funding": [
2528+				{
2529+					"type": "GitHub Sponsors",
2530+					"url": "https://github.com/sponsors/unifiedjs"
2531+				},
2532+				{
2533+					"type": "OpenCollective",
2534+					"url": "https://opencollective.com/unified"
2535+				}
2536+			],
2537+			"license": "MIT",
2538+			"dependencies": {
2539+				"micromark-util-character": "^2.0.0",
2540+				"micromark-util-encode": "^2.0.0",
2541+				"micromark-util-symbol": "^2.0.0"
2542+			}
2543+		},
2544+		"node_modules/micromark-util-symbol": {
2545+			"version": "2.0.1",
2546+			"resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz",
2547+			"integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==",
2548+			"funding": [
2549+				{
2550+					"type": "GitHub Sponsors",
2551+					"url": "https://github.com/sponsors/unifiedjs"
2552+				},
2553+				{
2554+					"type": "OpenCollective",
2555+					"url": "https://opencollective.com/unified"
2556+				}
2557+			],
2558+			"license": "MIT"
2559+		},
2560+		"node_modules/micromark-util-types": {
2561+			"version": "2.0.2",
2562+			"resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.2.tgz",
2563+			"integrity": "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==",
2564+			"funding": [
2565+				{
2566+					"type": "GitHub Sponsors",
2567+					"url": "https://github.com/sponsors/unifiedjs"
2568+				},
2569+				{
2570+					"type": "OpenCollective",
2571+					"url": "https://opencollective.com/unified"
2572+				}
2573+			],
2574+			"license": "MIT"
2575+		},
2576+		"node_modules/mri": {
2577+			"version": "1.2.0",
2578+			"resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz",
2579+			"integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==",
2580+			"dev": true,
2581+			"license": "MIT",
2582+			"engines": {
2583+				"node": ">=4"
2584+			}
2585+		},
2586+		"node_modules/mrmime": {
2587+			"version": "2.0.1",
2588+			"resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz",
2589+			"integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==",
2590+			"dev": true,
2591+			"license": "MIT",
2592+			"engines": {
2593+				"node": ">=10"
2594+			}
2595+		},
2596+		"node_modules/ms": {
2597+			"version": "2.1.3",
2598+			"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
2599+			"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
2600+			"license": "MIT"
2601+		},
2602+		"node_modules/nanoid": {
2603+			"version": "3.3.11",
2604+			"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
2605+			"integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
2606+			"dev": true,
2607+			"funding": [
2608+				{
2609+					"type": "github",
2610+					"url": "https://github.com/sponsors/ai"
2611+				}
2612+			],
2613+			"license": "MIT",
2614+			"bin": {
2615+				"nanoid": "bin/nanoid.cjs"
2616+			},
2617+			"engines": {
2618+				"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
2619+			}
2620+		},
2621+		"node_modules/node-addon-api": {
2622+			"version": "7.1.1",
2623+			"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz",
2624+			"integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==",
2625+			"license": "MIT"
2626+		},
2627+		"node_modules/node-pty": {
2628+			"version": "1.2.0-beta.12",
2629+			"resolved": "https://registry.npmjs.org/node-pty/-/node-pty-1.2.0-beta.12.tgz",
2630+			"integrity": "sha512-uExTCG/4VmSJa4+TjxFwPXv8BfacmfFEBL6JpxCMDghcwqzvD0yTcGmZ1fKOK6HY33tp0CelLblqTECJizc+Yw==",
2631+			"hasInstallScript": true,
2632+			"license": "MIT",
2633+			"dependencies": {
2634+				"node-addon-api": "^7.1.0"
2635+			}
2636+		},
2637+		"node_modules/nopt": {
2638+			"version": "1.0.10",
2639+			"resolved": "https://registry.npmjs.org/nopt/-/nopt-1.0.10.tgz",
2640+			"integrity": "sha512-NWmpvLSqUrgrAC9HCuxEvb+PSloHpqVu+FqcO4eeF2h5qYRhA7ev6KvelyQAKtegUbC6RypJnlEOhd8vloNKYg==",
2641+			"license": "MIT",
2642+			"dependencies": {
2643+				"abbrev": "1"
2644+			},
2645+			"bin": {
2646+				"nopt": "bin/nopt.js"
2647+			},
2648+			"engines": {
2649+				"node": "*"
2650+			}
2651+		},
2652+		"node_modules/obug": {
2653+			"version": "2.1.1",
2654+			"resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz",
2655+			"integrity": "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==",
2656+			"dev": true,
2657+			"funding": [
2658+				"https://github.com/sponsors/sxzz",
2659+				"https://opencollective.com/debug"
2660+			],
2661+			"license": "MIT"
2662+		},
2663+		"node_modules/oniguruma-parser": {
2664+			"version": "0.12.1",
2665+			"resolved": "https://registry.npmjs.org/oniguruma-parser/-/oniguruma-parser-0.12.1.tgz",
2666+			"integrity": "sha512-8Unqkvk1RYc6yq2WBYRj4hdnsAxVze8i7iPfQr8e4uSP3tRv0rpZcbGUDvxfQQcdwHt/e9PrMvGCsa8OqG9X3w==",
2667+			"license": "MIT"
2668+		},
2669+		"node_modules/oniguruma-to-es": {
2670+			"version": "4.3.5",
2671+			"resolved": "https://registry.npmjs.org/oniguruma-to-es/-/oniguruma-to-es-4.3.5.tgz",
2672+			"integrity": "sha512-Zjygswjpsewa0NLTsiizVuMQZbp0MDyM6lIt66OxsF21npUDlzpHi1Mgb/qhQdkb+dWFTzJmFbEWdvZgRho8eQ==",
2673+			"license": "MIT",
2674+			"dependencies": {
2675+				"oniguruma-parser": "^0.12.1",
2676+				"regex": "^6.1.0",
2677+				"regex-recursion": "^6.0.2"
2678+			}
2679+		},
2680+		"node_modules/open": {
2681+			"version": "11.0.0",
2682+			"resolved": "https://registry.npmjs.org/open/-/open-11.0.0.tgz",
2683+			"integrity": "sha512-smsWv2LzFjP03xmvFoJ331ss6h+jixfA4UUV/Bsiyuu4YJPfN+FIQGOIiv4w9/+MoHkfkJ22UIaQWRVFRfH6Vw==",
2684+			"license": "MIT",
2685+			"dependencies": {
2686+				"default-browser": "^5.4.0",
2687+				"define-lazy-prop": "^3.0.0",
2688+				"is-in-ssh": "^1.0.0",
2689+				"is-inside-container": "^1.0.0",
2690+				"powershell-utils": "^0.1.0",
2691+				"wsl-utils": "^0.3.0"
2692+			},
2693+			"engines": {
2694+				"node": ">=20"
2695+			},
2696+			"funding": {
2697+				"url": "https://github.com/sponsors/sindresorhus"
2698+			}
2699+		},
2700+		"node_modules/path-parse": {
2701+			"version": "1.0.7",
2702+			"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
2703+			"integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
2704+			"dev": true,
2705+			"license": "MIT"
2706+		},
2707+		"node_modules/picocolors": {
2708+			"version": "1.1.1",
2709+			"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
2710+			"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
2711+			"dev": true,
2712+			"license": "ISC"
2713+		},
2714+		"node_modules/picomatch": {
2715+			"version": "4.0.4",
2716+			"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
2717+			"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
2718+			"dev": true,
2719+			"license": "MIT",
2720+			"engines": {
2721+				"node": ">=12"
2722+			},
2723+			"funding": {
2724+				"url": "https://github.com/sponsors/jonschlinkert"
2725+			}
2726+		},
2727+		"node_modules/postcss": {
2728+			"version": "8.5.8",
2729+			"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz",
2730+			"integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==",
2731+			"dev": true,
2732+			"funding": [
2733+				{
2734+					"type": "opencollective",
2735+					"url": "https://opencollective.com/postcss/"
2736+				},
2737+				{
2738+					"type": "tidelift",
2739+					"url": "https://tidelift.com/funding/github/npm/postcss"
2740+				},
2741+				{
2742+					"type": "github",
2743+					"url": "https://github.com/sponsors/ai"
2744+				}
2745+			],
2746+			"license": "MIT",
2747+			"dependencies": {
2748+				"nanoid": "^3.3.11",
2749+				"picocolors": "^1.1.1",
2750+				"source-map-js": "^1.2.1"
2751+			},
2752+			"engines": {
2753+				"node": "^10 || ^12 || >=14"
2754+			}
2755+		},
2756+		"node_modules/powershell-utils": {
2757+			"version": "0.1.0",
2758+			"resolved": "https://registry.npmjs.org/powershell-utils/-/powershell-utils-0.1.0.tgz",
2759+			"integrity": "sha512-dM0jVuXJPsDN6DvRpea484tCUaMiXWjuCn++HGTqUWzGDjv5tZkEZldAJ/UMlqRYGFrD/etByo4/xOuC/snX2A==",
2760+			"license": "MIT",
2761+			"engines": {
2762+				"node": ">=20"
2763+			},
2764+			"funding": {
2765+				"url": "https://github.com/sponsors/sindresorhus"
2766+			}
2767+		},
2768+		"node_modules/property-information": {
2769+			"version": "7.1.0",
2770+			"resolved": "https://registry.npmjs.org/property-information/-/property-information-7.1.0.tgz",
2771+			"integrity": "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==",
2772+			"license": "MIT",
2773+			"funding": {
2774+				"type": "github",
2775+				"url": "https://github.com/sponsors/wooorm"
2776+			}
2777+		},
2778+		"node_modules/readdirp": {
2779+			"version": "4.1.2",
2780+			"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz",
2781+			"integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==",
2782+			"dev": true,
2783+			"license": "MIT",
2784+			"engines": {
2785+				"node": ">= 14.18.0"
2786+			},
2787+			"funding": {
2788+				"type": "individual",
2789+				"url": "https://paulmillr.com/funding/"
2790+			}
2791+		},
2792+		"node_modules/regex": {
2793+			"version": "6.1.0",
2794+			"resolved": "https://registry.npmjs.org/regex/-/regex-6.1.0.tgz",
2795+			"integrity": "sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg==",
2796+			"license": "MIT",
2797+			"dependencies": {
2798+				"regex-utilities": "^2.3.0"
2799+			}
2800+		},
2801+		"node_modules/regex-recursion": {
2802+			"version": "6.0.2",
2803+			"resolved": "https://registry.npmjs.org/regex-recursion/-/regex-recursion-6.0.2.tgz",
2804+			"integrity": "sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg==",
2805+			"license": "MIT",
2806+			"dependencies": {
2807+				"regex-utilities": "^2.3.0"
2808+			}
2809+		},
2810+		"node_modules/regex-utilities": {
2811+			"version": "2.3.0",
2812+			"resolved": "https://registry.npmjs.org/regex-utilities/-/regex-utilities-2.3.0.tgz",
2813+			"integrity": "sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng==",
2814+			"license": "MIT"
2815+		},
2816+		"node_modules/resolve": {
2817+			"version": "1.22.11",
2818+			"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz",
2819+			"integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==",
2820+			"dev": true,
2821+			"license": "MIT",
2822+			"dependencies": {
2823+				"is-core-module": "^2.16.1",
2824+				"path-parse": "^1.0.7",
2825+				"supports-preserve-symlinks-flag": "^1.0.0"
2826+			},
2827+			"bin": {
2828+				"resolve": "bin/resolve"
2829+			},
2830+			"engines": {
2831+				"node": ">= 0.4"
2832+			},
2833+			"funding": {
2834+				"url": "https://github.com/sponsors/ljharb"
2835+			}
2836+		},
2837+		"node_modules/rollup": {
2838+			"version": "4.60.0",
2839+			"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.0.tgz",
2840+			"integrity": "sha512-yqjxruMGBQJ2gG4HtjZtAfXArHomazDHoFwFFmZZl0r7Pdo7qCIXKqKHZc8yeoMgzJJ+pO6pEEHa+V7uzWlrAQ==",
2841+			"dev": true,
2842+			"license": "MIT",
2843+			"dependencies": {
2844+				"@types/estree": "1.0.8"
2845+			},
2846+			"bin": {
2847+				"rollup": "dist/bin/rollup"
2848+			},
2849+			"engines": {
2850+				"node": ">=18.0.0",
2851+				"npm": ">=8.0.0"
2852+			},
2853+			"optionalDependencies": {
2854+				"@rollup/rollup-android-arm-eabi": "4.60.0",
2855+				"@rollup/rollup-android-arm64": "4.60.0",
2856+				"@rollup/rollup-darwin-arm64": "4.60.0",
2857+				"@rollup/rollup-darwin-x64": "4.60.0",
2858+				"@rollup/rollup-freebsd-arm64": "4.60.0",
2859+				"@rollup/rollup-freebsd-x64": "4.60.0",
2860+				"@rollup/rollup-linux-arm-gnueabihf": "4.60.0",
2861+				"@rollup/rollup-linux-arm-musleabihf": "4.60.0",
2862+				"@rollup/rollup-linux-arm64-gnu": "4.60.0",
2863+				"@rollup/rollup-linux-arm64-musl": "4.60.0",
2864+				"@rollup/rollup-linux-loong64-gnu": "4.60.0",
2865+				"@rollup/rollup-linux-loong64-musl": "4.60.0",
2866+				"@rollup/rollup-linux-ppc64-gnu": "4.60.0",
2867+				"@rollup/rollup-linux-ppc64-musl": "4.60.0",
2868+				"@rollup/rollup-linux-riscv64-gnu": "4.60.0",
2869+				"@rollup/rollup-linux-riscv64-musl": "4.60.0",
2870+				"@rollup/rollup-linux-s390x-gnu": "4.60.0",
2871+				"@rollup/rollup-linux-x64-gnu": "4.60.0",
2872+				"@rollup/rollup-linux-x64-musl": "4.60.0",
2873+				"@rollup/rollup-openbsd-x64": "4.60.0",
2874+				"@rollup/rollup-openharmony-arm64": "4.60.0",
2875+				"@rollup/rollup-win32-arm64-msvc": "4.60.0",
2876+				"@rollup/rollup-win32-ia32-msvc": "4.60.0",
2877+				"@rollup/rollup-win32-x64-gnu": "4.60.0",
2878+				"@rollup/rollup-win32-x64-msvc": "4.60.0",
2879+				"fsevents": "~2.3.2"
2880+			}
2881+		},
2882+		"node_modules/run-applescript": {
2883+			"version": "7.1.0",
2884+			"resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.1.0.tgz",
2885+			"integrity": "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==",
2886+			"license": "MIT",
2887+			"engines": {
2888+				"node": ">=18"
2889+			},
2890+			"funding": {
2891+				"url": "https://github.com/sponsors/sindresorhus"
2892+			}
2893+		},
2894+		"node_modules/sade": {
2895+			"version": "1.8.1",
2896+			"resolved": "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz",
2897+			"integrity": "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==",
2898+			"dev": true,
2899+			"license": "MIT",
2900+			"dependencies": {
2901+				"mri": "^1.1.0"
2902+			},
2903+			"engines": {
2904+				"node": ">=6"
2905+			}
2906+		},
2907+		"node_modules/set-cookie-parser": {
2908+			"version": "3.1.0",
2909+			"resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-3.1.0.tgz",
2910+			"integrity": "sha512-kjnC1DXBHcxaOaOXBHBeRtltsDG2nUiUni+jP92M9gYdW12rsmx92UsfpH7o5tDRs7I1ZZPSQJQGv3UaRfCiuw==",
2911+			"dev": true,
2912+			"license": "MIT"
2913+		},
2914+		"node_modules/shiki": {
2915+			"version": "4.0.2",
2916+			"resolved": "https://registry.npmjs.org/shiki/-/shiki-4.0.2.tgz",
2917+			"integrity": "sha512-eAVKTMedR5ckPo4xne/PjYQYrU3qx78gtJZ+sHlXEg5IHhhoQhMfZVzetTYuaJS0L2Ef3AcCRzCHV8T0WI6nIQ==",
2918+			"license": "MIT",
2919+			"dependencies": {
2920+				"@shikijs/core": "4.0.2",
2921+				"@shikijs/engine-javascript": "4.0.2",
2922+				"@shikijs/engine-oniguruma": "4.0.2",
2923+				"@shikijs/langs": "4.0.2",
2924+				"@shikijs/themes": "4.0.2",
2925+				"@shikijs/types": "4.0.2",
2926+				"@shikijs/vscode-textmate": "^10.0.2",
2927+				"@types/hast": "^3.0.4"
2928+			},
2929+			"engines": {
2930+				"node": ">=20"
2931+			}
2932+		},
2933+		"node_modules/simple-git": {
2934+			"version": "3.33.0",
2935+			"resolved": "https://registry.npmjs.org/simple-git/-/simple-git-3.33.0.tgz",
2936+			"integrity": "sha512-D4V/tGC2sjsoNhoMybKyGoE+v8A60hRawKQ1iFRA1zwuDgGZCBJ4ByOzZ5J8joBbi4Oam0qiPH+GhzmSBwbJng==",
2937+			"license": "MIT",
2938+			"dependencies": {
2939+				"@kwsites/file-exists": "^1.1.1",
2940+				"@kwsites/promise-deferred": "^1.1.1",
2941+				"debug": "^4.4.0"
2942+			},
2943+			"funding": {
2944+				"type": "github",
2945+				"url": "https://github.com/steveukx/git-js?sponsor=1"
2946+			}
2947+		},
2948+		"node_modules/sirv": {
2949+			"version": "3.0.2",
2950+			"resolved": "https://registry.npmjs.org/sirv/-/sirv-3.0.2.tgz",
2951+			"integrity": "sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==",
2952+			"dev": true,
2953+			"license": "MIT",
2954+			"dependencies": {
2955+				"@polka/url": "^1.0.0-next.24",
2956+				"mrmime": "^2.0.0",
2957+				"totalist": "^3.0.0"
2958+			},
2959+			"engines": {
2960+				"node": ">=18"
2961+			}
2962+		},
2963+		"node_modules/source-map-js": {
2964+			"version": "1.2.1",
2965+			"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
2966+			"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
2967+			"dev": true,
2968+			"license": "BSD-3-Clause",
2969+			"engines": {
2970+				"node": ">=0.10.0"
2971+			}
2972+		},
2973+		"node_modules/space-separated-tokens": {
2974+			"version": "2.0.2",
2975+			"resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz",
2976+			"integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==",
2977+			"license": "MIT",
2978+			"funding": {
2979+				"type": "github",
2980+				"url": "https://github.com/sponsors/wooorm"
2981+			}
2982+		},
2983+		"node_modules/stringify-entities": {
2984+			"version": "4.0.4",
2985+			"resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz",
2986+			"integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==",
2987+			"license": "MIT",
2988+			"dependencies": {
2989+				"character-entities-html4": "^2.0.0",
2990+				"character-entities-legacy": "^3.0.0"
2991+			},
2992+			"funding": {
2993+				"type": "github",
2994+				"url": "https://github.com/sponsors/wooorm"
2995+			}
2996+		},
2997+		"node_modules/supports-preserve-symlinks-flag": {
2998+			"version": "1.0.0",
2999+			"resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
3000+			"integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
3001+			"dev": true,
3002+			"license": "MIT",
3003+			"engines": {
3004+				"node": ">= 0.4"
3005+			},
3006+			"funding": {
3007+				"url": "https://github.com/sponsors/ljharb"
3008+			}
3009+		},
3010+		"node_modules/svelte": {
3011+			"version": "5.55.0",
3012+			"resolved": "https://registry.npmjs.org/svelte/-/svelte-5.55.0.tgz",
3013+			"integrity": "sha512-SThllKq6TRMBwPtat7ASnm/9CDXnIhBR0NPGw0ujn2DVYx9rVwsPZxDaDQcYGdUz/3BYVsCzdq7pZarRQoGvtw==",
3014+			"dev": true,
3015+			"license": "MIT",
3016+			"dependencies": {
3017+				"@jridgewell/remapping": "^2.3.4",
3018+				"@jridgewell/sourcemap-codec": "^1.5.0",
3019+				"@sveltejs/acorn-typescript": "^1.0.5",
3020+				"@types/estree": "^1.0.5",
3021+				"@types/trusted-types": "^2.0.7",
3022+				"acorn": "^8.12.1",
3023+				"aria-query": "5.3.1",
3024+				"axobject-query": "^4.1.0",
3025+				"clsx": "^2.1.1",
3026+				"devalue": "^5.6.4",
3027+				"esm-env": "^1.2.1",
3028+				"esrap": "^2.2.2",
3029+				"is-reference": "^3.0.3",
3030+				"locate-character": "^3.0.0",
3031+				"magic-string": "^0.30.11",
3032+				"zimmerframe": "^1.1.2"
3033+			},
3034+			"engines": {
3035+				"node": ">=18"
3036+			}
3037+		},
3038+		"node_modules/svelte-check": {
3039+			"version": "4.4.5",
3040+			"resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-4.4.5.tgz",
3041+			"integrity": "sha512-1bSwIRCvvmSHrlK52fOlZmVtUZgil43jNL/2H18pRpa+eQjzGt6e3zayxhp1S7GajPFKNM/2PMCG+DZFHlG9fw==",
3042+			"dev": true,
3043+			"license": "MIT",
3044+			"dependencies": {
3045+				"@jridgewell/trace-mapping": "^0.3.25",
3046+				"chokidar": "^4.0.1",
3047+				"fdir": "^6.2.0",
3048+				"picocolors": "^1.0.0",
3049+				"sade": "^1.7.4"
3050+			},
3051+			"bin": {
3052+				"svelte-check": "bin/svelte-check"
3053+			},
3054+			"engines": {
3055+				"node": ">= 18.0.0"
3056+			},
3057+			"peerDependencies": {
3058+				"svelte": "^4.0.0 || ^5.0.0-next.0",
3059+				"typescript": ">=5.0.0"
3060+			}
3061+		},
3062+		"node_modules/svelte/node_modules/is-reference": {
3063+			"version": "3.0.3",
3064+			"resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.3.tgz",
3065+			"integrity": "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==",
3066+			"dev": true,
3067+			"license": "MIT",
3068+			"dependencies": {
3069+				"@types/estree": "^1.0.6"
3070+			}
3071+		},
3072+		"node_modules/tailwindcss": {
3073+			"version": "4.2.2",
3074+			"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.2.2.tgz",
3075+			"integrity": "sha512-KWBIxs1Xb6NoLdMVqhbhgwZf2PGBpPEiwOqgI4pFIYbNTfBXiKYyWoTsXgBQ9WFg/OlhnvHaY+AEpW7wSmFo2Q==",
3076+			"dev": true,
3077+			"license": "MIT"
3078+		},
3079+		"node_modules/tapable": {
3080+			"version": "2.3.2",
3081+			"resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.2.tgz",
3082+			"integrity": "sha512-1MOpMXuhGzGL5TTCZFItxCc0AARf1EZFQkGqMm7ERKj8+Hgr5oLvJOVFcC+lRmR8hCe2S3jC4T5D7Vg/d7/fhA==",
3083+			"dev": true,
3084+			"license": "MIT",
3085+			"engines": {
3086+				"node": ">=6"
3087+			},
3088+			"funding": {
3089+				"type": "opencollective",
3090+				"url": "https://opencollective.com/webpack"
3091+			}
3092+		},
3093+		"node_modules/tinyglobby": {
3094+			"version": "0.2.15",
3095+			"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
3096+			"integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==",
3097+			"dev": true,
3098+			"license": "MIT",
3099+			"dependencies": {
3100+				"fdir": "^6.5.0",
3101+				"picomatch": "^4.0.3"
3102+			},
3103+			"engines": {
3104+				"node": ">=12.0.0"
3105+			},
3106+			"funding": {
3107+				"url": "https://github.com/sponsors/SuperchupuDev"
3108+			}
3109+		},
3110+		"node_modules/totalist": {
3111+			"version": "3.0.1",
3112+			"resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz",
3113+			"integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==",
3114+			"dev": true,
3115+			"license": "MIT",
3116+			"engines": {
3117+				"node": ">=6"
3118+			}
3119+		},
3120+		"node_modules/trim-lines": {
3121+			"version": "3.0.1",
3122+			"resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz",
3123+			"integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==",
3124+			"license": "MIT",
3125+			"funding": {
3126+				"type": "github",
3127+				"url": "https://github.com/sponsors/wooorm"
3128+			}
3129+		},
3130+		"node_modules/typescript": {
3131+			"version": "5.9.3",
3132+			"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
3133+			"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
3134+			"dev": true,
3135+			"license": "Apache-2.0",
3136+			"bin": {
3137+				"tsc": "bin/tsc",
3138+				"tsserver": "bin/tsserver"
3139+			},
3140+			"engines": {
3141+				"node": ">=14.17"
3142+			}
3143+		},
3144+		"node_modules/undici-types": {
3145+			"version": "7.18.2",
3146+			"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz",
3147+			"integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==",
3148+			"dev": true,
3149+			"license": "MIT"
3150+		},
3151+		"node_modules/unist-util-is": {
3152+			"version": "6.0.1",
3153+			"resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.1.tgz",
3154+			"integrity": "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==",
3155+			"license": "MIT",
3156+			"dependencies": {
3157+				"@types/unist": "^3.0.0"
3158+			},
3159+			"funding": {
3160+				"type": "opencollective",
3161+				"url": "https://opencollective.com/unified"
3162+			}
3163+		},
3164+		"node_modules/unist-util-position": {
3165+			"version": "5.0.0",
3166+			"resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz",
3167+			"integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==",
3168+			"license": "MIT",
3169+			"dependencies": {
3170+				"@types/unist": "^3.0.0"
3171+			},
3172+			"funding": {
3173+				"type": "opencollective",
3174+				"url": "https://opencollective.com/unified"
3175+			}
3176+		},
3177+		"node_modules/unist-util-stringify-position": {
3178+			"version": "4.0.0",
3179+			"resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz",
3180+			"integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==",
3181+			"license": "MIT",
3182+			"dependencies": {
3183+				"@types/unist": "^3.0.0"
3184+			},
3185+			"funding": {
3186+				"type": "opencollective",
3187+				"url": "https://opencollective.com/unified"
3188+			}
3189+		},
3190+		"node_modules/unist-util-visit": {
3191+			"version": "5.1.0",
3192+			"resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.1.0.tgz",
3193+			"integrity": "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==",
3194+			"license": "MIT",
3195+			"dependencies": {
3196+				"@types/unist": "^3.0.0",
3197+				"unist-util-is": "^6.0.0",
3198+				"unist-util-visit-parents": "^6.0.0"
3199+			},
3200+			"funding": {
3201+				"type": "opencollective",
3202+				"url": "https://opencollective.com/unified"
3203+			}
3204+		},
3205+		"node_modules/unist-util-visit-parents": {
3206+			"version": "6.0.2",
3207+			"resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.2.tgz",
3208+			"integrity": "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==",
3209+			"license": "MIT",
3210+			"dependencies": {
3211+				"@types/unist": "^3.0.0",
3212+				"unist-util-is": "^6.0.0"
3213+			},
3214+			"funding": {
3215+				"type": "opencollective",
3216+				"url": "https://opencollective.com/unified"
3217+			}
3218+		},
3219+		"node_modules/vfile": {
3220+			"version": "6.0.3",
3221+			"resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz",
3222+			"integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==",
3223+			"license": "MIT",
3224+			"dependencies": {
3225+				"@types/unist": "^3.0.0",
3226+				"vfile-message": "^4.0.0"
3227+			},
3228+			"funding": {
3229+				"type": "opencollective",
3230+				"url": "https://opencollective.com/unified"
3231+			}
3232+		},
3233+		"node_modules/vfile-message": {
3234+			"version": "4.0.3",
3235+			"resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz",
3236+			"integrity": "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==",
3237+			"license": "MIT",
3238+			"dependencies": {
3239+				"@types/unist": "^3.0.0",
3240+				"unist-util-stringify-position": "^4.0.0"
3241+			},
3242+			"funding": {
3243+				"type": "opencollective",
3244+				"url": "https://opencollective.com/unified"
3245+			}
3246+		},
3247+		"node_modules/vite": {
3248+			"version": "7.3.1",
3249+			"resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz",
3250+			"integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==",
3251+			"dev": true,
3252+			"license": "MIT",
3253+			"dependencies": {
3254+				"esbuild": "^0.27.0",
3255+				"fdir": "^6.5.0",
3256+				"picomatch": "^4.0.3",
3257+				"postcss": "^8.5.6",
3258+				"rollup": "^4.43.0",
3259+				"tinyglobby": "^0.2.15"
3260+			},
3261+			"bin": {
3262+				"vite": "bin/vite.js"
3263+			},
3264+			"engines": {
3265+				"node": "^20.19.0 || >=22.12.0"
3266+			},
3267+			"funding": {
3268+				"url": "https://github.com/vitejs/vite?sponsor=1"
3269+			},
3270+			"optionalDependencies": {
3271+				"fsevents": "~2.3.3"
3272+			},
3273+			"peerDependencies": {
3274+				"@types/node": "^20.19.0 || >=22.12.0",
3275+				"jiti": ">=1.21.0",
3276+				"less": "^4.0.0",
3277+				"lightningcss": "^1.21.0",
3278+				"sass": "^1.70.0",
3279+				"sass-embedded": "^1.70.0",
3280+				"stylus": ">=0.54.8",
3281+				"sugarss": "^5.0.0",
3282+				"terser": "^5.16.0",
3283+				"tsx": "^4.8.1",
3284+				"yaml": "^2.4.2"
3285+			},
3286+			"peerDependenciesMeta": {
3287+				"@types/node": {
3288+					"optional": true
3289+				},
3290+				"jiti": {
3291+					"optional": true
3292+				},
3293+				"less": {
3294+					"optional": true
3295+				},
3296+				"lightningcss": {
3297+					"optional": true
3298+				},
3299+				"sass": {
3300+					"optional": true
3301+				},
3302+				"sass-embedded": {
3303+					"optional": true
3304+				},
3305+				"stylus": {
3306+					"optional": true
3307+				},
3308+				"sugarss": {
3309+					"optional": true
3310+				},
3311+				"terser": {
3312+					"optional": true
3313+				},
3314+				"tsx": {
3315+					"optional": true
3316+				},
3317+				"yaml": {
3318+					"optional": true
3319+				}
3320+			}
3321+		},
3322+		"node_modules/vitefu": {
3323+			"version": "1.1.2",
3324+			"resolved": "https://registry.npmjs.org/vitefu/-/vitefu-1.1.2.tgz",
3325+			"integrity": "sha512-zpKATdUbzbsycPFBN71nS2uzBUQiVnFoOrr2rvqv34S1lcAgMKKkjWleLGeiJlZ8lwCXvtWaRn7R3ZC16SYRuw==",
3326+			"dev": true,
3327+			"license": "MIT",
3328+			"workspaces": [
3329+				"tests/deps/*",
3330+				"tests/projects/*",
3331+				"tests/projects/workspace/packages/*"
3332+			],
3333+			"peerDependencies": {
3334+				"vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-beta.0"
3335+			},
3336+			"peerDependenciesMeta": {
3337+				"vite": {
3338+					"optional": true
3339+				}
3340+			}
3341+		},
3342+		"node_modules/ws": {
3343+			"version": "8.20.0",
3344+			"resolved": "https://registry.npmjs.org/ws/-/ws-8.20.0.tgz",
3345+			"integrity": "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==",
3346+			"license": "MIT",
3347+			"engines": {
3348+				"node": ">=10.0.0"
3349+			},
3350+			"peerDependencies": {
3351+				"bufferutil": "^4.0.1",
3352+				"utf-8-validate": ">=5.0.2"
3353+			},
3354+			"peerDependenciesMeta": {
3355+				"bufferutil": {
3356+					"optional": true
3357+				},
3358+				"utf-8-validate": {
3359+					"optional": true
3360+				}
3361+			}
3362+		},
3363+		"node_modules/wsl-utils": {
3364+			"version": "0.3.1",
3365+			"resolved": "https://registry.npmjs.org/wsl-utils/-/wsl-utils-0.3.1.tgz",
3366+			"integrity": "sha512-g/eziiSUNBSsdDJtCLB8bdYEUMj4jR7AGeUo96p/3dTafgjHhpF4RiCFPiRILwjQoDXx5MqkBr4fwWtR3Ky4Wg==",
3367+			"license": "MIT",
3368+			"dependencies": {
3369+				"is-wsl": "^3.1.0",
3370+				"powershell-utils": "^0.1.0"
3371+			},
3372+			"engines": {
3373+				"node": ">=20"
3374+			},
3375+			"funding": {
3376+				"url": "https://github.com/sponsors/sindresorhus"
3377+			}
3378+		},
3379+		"node_modules/zimmerframe": {
3380+			"version": "1.1.4",
3381+			"resolved": "https://registry.npmjs.org/zimmerframe/-/zimmerframe-1.1.4.tgz",
3382+			"integrity": "sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ==",
3383+			"dev": true,
3384+			"license": "MIT"
3385+		},
3386+		"node_modules/zwitch": {
3387+			"version": "2.0.4",
3388+			"resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz",
3389+			"integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==",
3390+			"license": "MIT",
3391+			"funding": {
3392+				"type": "github",
3393+				"url": "https://github.com/sponsors/wooorm"
3394+			}
3395+		}
3396+	}
3397+}
A package.json
+49, -0
 1@@ -0,0 +1,49 @@
 2+{
 3+	"name": "local-code-review",
 4+	"version": "0.1.0",
 5+	"description": "Local code review tool — GitHub PR-like diff view with inline comments for AI-assisted development",
 6+	"type": "module",
 7+	"bin": {
 8+		"local-code-review": "./bin/lcr.js",
 9+		"lcr": "./bin/lcr.js"
10+	},
11+	"files": [
12+		"bin/",
13+		"build/"
14+	],
15+	"license": "MIT",
16+	"engines": {
17+		"node": ">=18"
18+	},
19+	"scripts": {
20+		"prepublishOnly": "npm run build",
21+		"dev": "vite dev",
22+		"build": "vite build",
23+		"preview": "vite preview",
24+		"prepare": "svelte-kit sync || echo ''",
25+		"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
26+		"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch"
27+	},
28+	"devDependencies": {
29+		"@sveltejs/adapter-node": "^5.5.2",
30+		"@sveltejs/kit": "^2.50.2",
31+		"@sveltejs/vite-plugin-svelte": "^6.2.4",
32+		"@tailwindcss/vite": "^4.1.18",
33+		"@types/ws": "^8.18.1",
34+		"svelte": "^5.54.0",
35+		"svelte-check": "^4.4.2",
36+		"tailwindcss": "^4.1.18",
37+		"typescript": "^5.9.3",
38+		"vite": "^7.3.1"
39+	},
40+	"dependencies": {
41+		"@xterm/addon-fit": "^0.11.0",
42+		"@xterm/xterm": "^6.0.0",
43+		"diff2html": "^3.4.56",
44+		"node-pty": "^1.2.0-beta.12",
45+		"open": "^11.0.0",
46+		"shiki": "^4.0.2",
47+		"simple-git": "^3.33.0",
48+		"ws": "^8.20.0"
49+	}
50+}
A src/app.d.ts
+13, -0
 1@@ -0,0 +1,13 @@
 2+// See https://svelte.dev/docs/kit/types#app.d.ts
 3+// for information about these interfaces
 4+declare global {
 5+	namespace App {
 6+		// interface Error {}
 7+		// interface Locals {}
 8+		// interface PageData {}
 9+		// interface PageState {}
10+		// interface Platform {}
11+	}
12+}
13+
14+export {};
A src/app.html
+11, -0
 1@@ -0,0 +1,11 @@
 2+<!doctype html>
 3+<html lang="en">
 4+	<head>
 5+		<meta charset="utf-8" />
 6+		<meta name="viewport" content="width=device-width, initial-scale=1" />
 7+		%sveltekit.head%
 8+	</head>
 9+	<body data-sveltekit-preload-data="hover">
10+		<div style="display: contents">%sveltekit.body%</div>
11+	</body>
12+</html>
A src/lib/assets/favicon.svg
+1, -0
1@@ -0,0 +1 @@
2+<svg xmlns="http://www.w3.org/2000/svg" width="107" height="128" viewBox="0 0 107 128"><title>svelte-logo</title><path d="M94.157 22.819c-10.4-14.885-30.94-19.297-45.792-9.835L22.282 29.608A29.92 29.92 0 0 0 8.764 49.65a31.5 31.5 0 0 0 3.108 20.231 30 30 0 0 0-4.477 11.183 31.9 31.9 0 0 0 5.448 24.116c10.402 14.887 30.942 19.297 45.791 9.835l26.083-16.624A29.92 29.92 0 0 0 98.235 78.35a31.53 31.53 0 0 0-3.105-20.232 30 30 0 0 0 4.474-11.182 31.88 31.88 0 0 0-5.447-24.116" style="fill:#ff3e00"/><path d="M45.817 106.582a20.72 20.72 0 0 1-22.237-8.243 19.17 19.17 0 0 1-3.277-14.503 18 18 0 0 1 .624-2.435l.49-1.498 1.337.981a33.6 33.6 0 0 0 10.203 5.098l.97.294-.09.968a5.85 5.85 0 0 0 1.052 3.878 6.24 6.24 0 0 0 6.695 2.485 5.8 5.8 0 0 0 1.603-.704L69.27 76.28a5.43 5.43 0 0 0 2.45-3.631 5.8 5.8 0 0 0-.987-4.371 6.24 6.24 0 0 0-6.698-2.487 5.7 5.7 0 0 0-1.6.704l-9.953 6.345a19 19 0 0 1-5.296 2.326 20.72 20.72 0 0 1-22.237-8.243 19.17 19.17 0 0 1-3.277-14.502 17.99 17.99 0 0 1 8.13-12.052l26.081-16.623a19 19 0 0 1 5.3-2.329 20.72 20.72 0 0 1 22.237 8.243 19.17 19.17 0 0 1 3.277 14.503 18 18 0 0 1-.624 2.435l-.49 1.498-1.337-.98a33.6 33.6 0 0 0-10.203-5.1l-.97-.294.09-.968a5.86 5.86 0 0 0-1.052-3.878 6.24 6.24 0 0 0-6.696-2.485 5.8 5.8 0 0 0-1.602.704L37.73 51.72a5.42 5.42 0 0 0-2.449 3.63 5.79 5.79 0 0 0 .986 4.372 6.24 6.24 0 0 0 6.698 2.486 5.8 5.8 0 0 0 1.602-.704l9.952-6.342a19 19 0 0 1 5.295-2.328 20.72 20.72 0 0 1 22.237 8.242 19.17 19.17 0 0 1 3.277 14.503 18 18 0 0 1-8.13 12.053l-26.081 16.622a19 19 0 0 1-5.3 2.328" style="fill:#fff"/></svg>
A src/lib/components/CommentEditor.svelte
+95, -0
 1@@ -0,0 +1,95 @@
 2+<script lang="ts">
 3+	import { commentingLine } from '$lib/stores/ui.ts';
 4+	import { addComment } from '$lib/stores/review.ts';
 5+
 6+	interface Props {
 7+		filePath: string;
 8+		lineNumber: number;
 9+		side: 'old' | 'new';
10+		threadId?: string;
11+		originalLineContent?: string;
12+		onclose?: () => void;
13+	}
14+
15+	let { filePath, lineNumber, side, threadId, originalLineContent, onclose }: Props = $props();
16+
17+	let text = $state('');
18+	let commentType = $state<'comment' | 'suggestion' | 'question'>('comment');
19+	let submitting = $state(false);
20+	let textarea: HTMLTextAreaElement | undefined = $state();
21+
22+	$effect(() => {
23+		textarea?.focus();
24+	});
25+
26+	async function submit() {
27+		if (!text.trim()) return;
28+		submitting = true;
29+		try {
30+			await addComment(filePath, lineNumber, side, text.trim(), commentType, threadId, originalLineContent);
31+			text = '';
32+			$commentingLine = null;
33+			onclose?.();
34+		} finally {
35+			submitting = false;
36+		}
37+	}
38+
39+	function cancel() {
40+		$commentingLine = null;
41+		onclose?.();
42+	}
43+
44+	function handleKeydown(e: KeyboardEvent) {
45+		if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) {
46+			e.preventDefault();
47+			submit();
48+		}
49+		if (e.key === 'Escape') {
50+			cancel();
51+		}
52+	}
53+</script>
54+
55+<div class="bg-zinc-800 border border-zinc-600 rounded-lg p-3 my-1 mx-2">
56+	<div class="flex gap-1 mb-2">
57+		{#each ['comment', 'suggestion', 'question'] as type}
58+			{@const active = commentType === type}
59+			<button
60+				class="text-xs px-2 py-1 rounded transition-colors {active ? 'bg-zinc-600 text-white' : 'text-zinc-400 hover:text-zinc-300 hover:bg-zinc-700'}"
61+				onclick={() => (commentType = type as typeof commentType)}
62+			>
63+				{type === 'comment' ? '💬' : type === 'suggestion' ? '💡' : '❓'}
64+				{type}
65+			</button>
66+		{/each}
67+	</div>
68+	<textarea
69+		bind:this={textarea}
70+		bind:value={text}
71+		onkeydown={handleKeydown}
72+		placeholder="Leave a {commentType}..."
73+		class="w-full bg-zinc-900 text-zinc-200 border border-zinc-600 rounded px-3 py-2 text-sm resize-y min-h-[80px] focus:outline-none focus:border-blue-500 placeholder:text-zinc-500"
74+		rows="3"
75+	></textarea>
76+	<div class="flex items-center justify-between mt-2">
77+		<span class="text-xs text-zinc-500">
78+			{navigator?.platform?.includes('Mac') ? '⌘' : 'Ctrl'}+Enter to submit
79+		</span>
80+		<div class="flex gap-2">
81+			<button
82+				class="text-xs px-3 py-1.5 rounded text-zinc-400 hover:text-zinc-200 hover:bg-zinc-700 transition-colors"
83+				onclick={cancel}
84+			>
85+				Cancel
86+			</button>
87+			<button
88+				class="text-xs px-3 py-1.5 rounded bg-blue-600 text-white hover:bg-blue-500 transition-colors disabled:opacity-50"
89+				disabled={!text.trim() || submitting}
90+				onclick={submit}
91+			>
92+				{submitting ? 'Adding...' : 'Add ' + commentType}
93+			</button>
94+		</div>
95+	</div>
96+</div>
A src/lib/components/CommentThread.svelte
+76, -0
 1@@ -0,0 +1,76 @@
 2+<script lang="ts">
 3+	import type { ReviewThread } from '$lib/types/index.ts';
 4+	import { resolveThread, deleteThread } from '$lib/stores/review.ts';
 5+	import CommentEditor from './CommentEditor.svelte';
 6+
 7+	interface Props {
 8+		thread: ReviewThread;
 9+	}
10+
11+	let { thread }: Props = $props();
12+	let replying = $state(false);
13+
14+	const typeIcons: Record<string, string> = {
15+		comment: '💬',
16+		suggestion: '💡',
17+		question: '❓'
18+	};
19+
20+	function formatTime(iso: string): string {
21+		const d = new Date(iso);
22+		return d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
23+	}
24+</script>
25+
26+<div class="border-l-2 {thread.outdated ? 'border-yellow-500/60 opacity-75' : thread.resolved ? 'border-zinc-600 opacity-60' : 'border-blue-500'} bg-zinc-800/50 rounded-r-lg my-1 mx-2">
27+	{#if thread.outdated}
28+		<div class="px-3 py-1.5 bg-yellow-900/20 border-b border-zinc-700 flex items-center gap-2">
29+			<span class="text-xs font-medium text-yellow-400">Outdated</span>
30+			<span class="text-xs text-yellow-500/70">The code this comment was on has changed</span>
31+		</div>
32+	{/if}
33+	{#each thread.comments as comment, i}
34+		<div class="px-3 py-2 {i > 0 ? 'border-t border-zinc-700' : ''}">
35+			<div class="flex items-center gap-2 mb-1">
36+				<span class="text-sm">{typeIcons[comment.type]}</span>
37+				<span class="text-xs font-medium text-zinc-300 capitalize">{comment.type}</span>
38+				<span class="text-xs text-zinc-500">{formatTime(comment.createdAt)}</span>
39+				<span class="text-xs text-zinc-600">Line {comment.lineNumber}</span>
40+			</div>
41+			<p class="text-sm text-zinc-200 whitespace-pre-wrap">{comment.body}</p>
42+		</div>
43+	{/each}
44+
45+	<div class="px-3 py-2 border-t border-zinc-700 flex gap-2">
46+		<button
47+			class="text-xs text-zinc-400 hover:text-blue-400 transition-colors"
48+			onclick={() => (replying = !replying)}
49+		>
50+			Reply
51+		</button>
52+		<button
53+			class="text-xs text-zinc-400 hover:text-green-400 transition-colors"
54+			onclick={() => resolveThread(thread.id)}
55+		>
56+			{thread.resolved ? 'Unresolve' : 'Resolve'}
57+		</button>
58+		<button
59+			class="text-xs text-zinc-400 hover:text-red-400 transition-colors"
60+			onclick={() => deleteThread(thread.id)}
61+		>
62+			Delete
63+		</button>
64+	</div>
65+
66+	{#if replying}
67+		<div class="px-2 pb-2">
68+			<CommentEditor
69+				filePath={thread.filePath}
70+				lineNumber={thread.lineNumber}
71+				side={thread.side}
72+				threadId={thread.id}
73+				onclose={() => (replying = false)}
74+			/>
75+		</div>
76+	{/if}
77+</div>
A src/lib/components/CommitDialog.svelte
+141, -0
  1@@ -0,0 +1,141 @@
  2+<script lang="ts">
  3+	import { showCommitDialog } from '$lib/stores/ui.ts';
  4+	import { approvedCount } from '$lib/stores/files.ts';
  5+
  6+	let message = $state('');
  7+	let commitMode = $state<'approved' | 'all'>('approved');
  8+	let committing = $state(false);
  9+	let result = $state<{ success: boolean; hash?: string; error?: string } | null>(null);
 10+
 11+	async function handleCommit() {
 12+		if (!message.trim()) return;
 13+		committing = true;
 14+		result = null;
 15+
 16+		try {
 17+			const res = await fetch('/api/commit', {
 18+				method: 'POST',
 19+				headers: { 'Content-Type': 'application/json' },
 20+				body: JSON.stringify({ message: message.trim(), mode: commitMode })
 21+			});
 22+			result = await res.json();
 23+		} catch (err) {
 24+			result = { success: false, error: String(err) };
 25+		} finally {
 26+			committing = false;
 27+		}
 28+	}
 29+
 30+	function close() {
 31+		$showCommitDialog = false;
 32+		message = '';
 33+		result = null;
 34+	}
 35+
 36+	function handleKeydown(e: KeyboardEvent) {
 37+		if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) {
 38+			e.preventDefault();
 39+			handleCommit();
 40+		}
 41+		if (e.key === 'Escape') close();
 42+	}
 43+</script>
 44+
 45+{#if $showCommitDialog}
 46+	<!-- svelte-ignore a11y_click_events_have_key_events -->
 47+	<!-- svelte-ignore a11y_no_static_element_interactions -->
 48+	<div class="fixed inset-0 bg-black/50 z-50 flex items-center justify-center" onclick={close}>
 49+		<!-- svelte-ignore a11y_click_events_have_key_events -->
 50+		<!-- svelte-ignore a11y_no_static_element_interactions -->
 51+		<div class="bg-zinc-800 border border-zinc-600 rounded-xl shadow-2xl w-full max-w-lg mx-4" onclick={(e) => e.stopPropagation()}>
 52+			<div class="px-5 py-4 border-b border-zinc-700">
 53+				<h2 class="text-lg font-semibold text-zinc-200">Commit Changes</h2>
 54+			</div>
 55+
 56+			{#if result?.success}
 57+				<div class="p-5">
 58+					<div class="text-center">
 59+						<div class="text-green-400 text-4xl mb-3">✓</div>
 60+						<p class="text-zinc-200 font-medium">Committed successfully</p>
 61+						<p class="text-zinc-400 text-sm mt-1">Hash: {result.hash}</p>
 62+					</div>
 63+				</div>
 64+				<div class="px-5 py-3 border-t border-zinc-700 flex justify-end">
 65+					<button
 66+						class="text-sm px-4 py-2 rounded bg-zinc-600 text-white hover:bg-zinc-500 transition-colors"
 67+						onclick={close}
 68+					>
 69+						Done
 70+					</button>
 71+				</div>
 72+			{:else}
 73+				<div class="p-5 space-y-4">
 74+					<!-- Commit scope -->
 75+					<div>
 76+						<label class="block text-sm text-zinc-400 mb-2">What to commit</label>
 77+						<div class="flex gap-2">
 78+							<button
 79+								class="flex-1 text-left px-3 py-2 rounded-lg border text-sm transition-colors {commitMode === 'approved' ? 'border-green-600 bg-green-600/10 text-green-400' : 'border-zinc-600 text-zinc-400 hover:border-zinc-500'}"
 80+								onclick={() => (commitMode = 'approved')}
 81+							>
 82+								<div class="font-medium">Approved files only</div>
 83+								<div class="text-xs mt-0.5 opacity-70">{$approvedCount.approved} of {$approvedCount.total} files</div>
 84+							</button>
 85+							<button
 86+								class="flex-1 text-left px-3 py-2 rounded-lg border text-sm transition-colors {commitMode === 'all' ? 'border-blue-600 bg-blue-600/10 text-blue-400' : 'border-zinc-600 text-zinc-400 hover:border-zinc-500'}"
 87+								onclick={() => (commitMode = 'all')}
 88+							>
 89+								<div class="font-medium">All changes</div>
 90+								<div class="text-xs mt-0.5 opacity-70">{$approvedCount.total} files</div>
 91+							</button>
 92+						</div>
 93+					</div>
 94+
 95+					{#if commitMode === 'approved' && $approvedCount.approved === 0}
 96+						<div class="text-sm rounded-lg px-3 py-2 bg-yellow-500/10 text-yellow-400">
 97+							No files approved yet. Approve files in the sidebar first, or commit all changes.
 98+						</div>
 99+					{/if}
100+
101+					<!-- Commit message -->
102+					<div>
103+						<label class="block text-sm text-zinc-400 mb-2" for="commit-msg">Commit message</label>
104+						<textarea
105+							id="commit-msg"
106+							bind:value={message}
107+							onkeydown={handleKeydown}
108+							class="w-full bg-zinc-900 text-zinc-200 border border-zinc-600 rounded-lg px-3 py-2 text-sm resize-y min-h-[100px] focus:outline-none focus:border-blue-500 placeholder:text-zinc-500"
109+							placeholder="Describe your changes..."
110+							rows="4"
111+						></textarea>
112+					</div>
113+
114+					{#if result?.error}
115+						<p class="text-red-400 text-sm">{result.error}</p>
116+					{/if}
117+				</div>
118+
119+				<div class="px-5 py-3 border-t border-zinc-700 flex items-center justify-between">
120+					<span class="text-xs text-zinc-500">
121+						{navigator?.platform?.includes('Mac') ? '⌘' : 'Ctrl'}+Enter to commit
122+					</span>
123+					<div class="flex gap-2">
124+						<button
125+							class="text-sm px-4 py-2 rounded text-zinc-400 hover:text-zinc-200 hover:bg-zinc-700 transition-colors"
126+							onclick={close}
127+						>
128+							Cancel
129+						</button>
130+						<button
131+							class="text-sm px-4 py-2 rounded bg-green-600 text-white hover:bg-green-500 transition-colors disabled:opacity-50"
132+							disabled={!message.trim() || committing || (commitMode === 'approved' && $approvedCount.approved === 0)}
133+							onclick={handleCommit}
134+						>
135+							{committing ? 'Committing...' : commitMode === 'approved' ? `Commit ${$approvedCount.approved} files` : 'Commit all'}
136+						</button>
137+					</div>
138+				</div>
139+			{/if}
140+		</div>
141+	</div>
142+{/if}
A src/lib/components/DiffView.svelte
+370, -0
  1@@ -0,0 +1,370 @@
  2+<script lang="ts">
  3+	import type { DiffFile, DiffLine, DiffHunk } from '$lib/types/index.ts';
  4+	import type { ReviewThread } from '$lib/types/index.ts';
  5+	import { viewMode, commentingLine } from '$lib/stores/ui.ts';
  6+	import { threadsByFile } from '$lib/stores/review.ts';
  7+	import CommentEditor from './CommentEditor.svelte';
  8+	import CommentThread from './CommentThread.svelte';
  9+
 10+	interface Props {
 11+		file: DiffFile;
 12+	}
 13+
 14+	let { file }: Props = $props();
 15+
 16+	// Track expanded context lines per hunk boundary
 17+	let expandedLines = $state<Map<string, DiffLine[]>>(new Map());
 18+
 19+	let fileThreads = $derived($threadsByFile.get(file.path) || []);
 20+
 21+	function getThreadsForLine(lineNumber: number, side: 'old' | 'new'): ReviewThread[] {
 22+		return fileThreads.filter((t) => t.lineNumber === lineNumber && t.side === side);
 23+	}
 24+
 25+	function startComment(lineNumber: number, side: 'old' | 'new', originalLineContent?: string) {
 26+		$commentingLine = { filePath: file.path, lineNumber, side, originalLineContent };
 27+	}
 28+
 29+	function isCommenting(lineNumber: number, side: 'old' | 'new'): boolean {
 30+		const c = $commentingLine;
 31+		return c !== null && c.filePath === file.path && c.lineNumber === lineNumber && c.side === side;
 32+	}
 33+
 34+	function getLineKey(line: DiffLine): number {
 35+		if (line.type === 'del') return line.oldNumber!;
 36+		return line.newNumber!;
 37+	}
 38+
 39+	function getLineSide(line: DiffLine): 'old' | 'new' {
 40+		return line.type === 'del' ? 'old' : 'new';
 41+	}
 42+
 43+	function lineBg(type: string): string {
 44+		if (type === 'add') return 'bg-green-950/40';
 45+		if (type === 'del') return 'bg-red-950/40';
 46+		return '';
 47+	}
 48+
 49+	const EXPAND_COUNT = 20;
 50+
 51+	// Get the effective end line of a hunk (including its after-expansion)
 52+	function getHunkEndLine(hunkIndex: number): number {
 53+		const hunk = file.hunks[hunkIndex];
 54+		const afterLines = expandedLines.get(`after-${hunkIndex}`) || [];
 55+		if (afterLines.length > 0) {
 56+			return afterLines[afterLines.length - 1].newNumber || afterLines[afterLines.length - 1].oldNumber || 0;
 57+		}
 58+		return hunk.newStart + hunk.newLines - 1;
 59+	}
 60+
 61+	// Get the effective start line of a hunk (including its before-expansion)
 62+	function getHunkStartLine(hunkIndex: number): number {
 63+		const hunk = file.hunks[hunkIndex];
 64+		const beforeLines = expandedLines.get(`before-${hunkIndex}`) || [];
 65+		if (beforeLines.length > 0) {
 66+			return beforeLines[0].newNumber || beforeLines[0].oldNumber || 0;
 67+		}
 68+		return hunk.newStart;
 69+	}
 70+
 71+	async function expandBefore(hunkIndex: number) {
 72+		const key = `before-${hunkIndex}`;
 73+		const existing = expandedLines.get(key) || [];
 74+		const currentFirstLine = getHunkStartLine(hunkIndex);
 75+
 76+		// Don't expand past the previous hunk's end
 77+		let minLine = 1;
 78+		if (hunkIndex > 0) {
 79+			minLine = getHunkEndLine(hunkIndex - 1) + 1;
 80+		}
 81+
 82+		const endLine = currentFirstLine - 1;
 83+		const startLine = Math.max(minLine, endLine - EXPAND_COUNT + 1);
 84+		if (startLine > endLine) return;
 85+
 86+		const res = await fetch(`/api/context?file=${encodeURIComponent(file.path)}&start=${startLine}&end=${endLine}`);
 87+		const data = await res.json();
 88+		if (data.lines) {
 89+			const newMap = new Map(expandedLines);
 90+			newMap.set(key, [...data.lines, ...existing]);
 91+			expandedLines = newMap;
 92+		}
 93+	}
 94+
 95+	async function expandAfter(hunkIndex: number) {
 96+		const key = `after-${hunkIndex}`;
 97+		const existing = expandedLines.get(key) || [];
 98+		const currentLastLine = getHunkEndLine(hunkIndex);
 99+
100+		// Don't expand past the next hunk's start
101+		let maxLine = Infinity;
102+		if (hunkIndex < file.hunks.length - 1) {
103+			maxLine = getHunkStartLine(hunkIndex + 1) - 1;
104+		}
105+
106+		const startLine = currentLastLine + 1;
107+		const endLine = Math.min(startLine + EXPAND_COUNT - 1, maxLine);
108+		if (startLine > endLine) return;
109+
110+		const res = await fetch(`/api/context?file=${encodeURIComponent(file.path)}&start=${startLine}&end=${endLine}`);
111+		const data = await res.json();
112+		if (data.lines && data.lines.length > 0) {
113+			const newMap = new Map(expandedLines);
114+			newMap.set(key, [...existing, ...data.lines]);
115+			expandedLines = newMap;
116+		}
117+	}
118+
119+	function getExpandedBefore(hunkIndex: number): DiffLine[] {
120+		return expandedLines.get(`before-${hunkIndex}`) || [];
121+	}
122+
123+	function getExpandedAfter(hunkIndex: number): DiffLine[] {
124+		return expandedLines.get(`after-${hunkIndex}`) || [];
125+	}
126+
127+	function canExpandBefore(hunkIndex: number): boolean {
128+		const currentStart = getHunkStartLine(hunkIndex);
129+		let minLine = 1;
130+		if (hunkIndex > 0) {
131+			minLine = getHunkEndLine(hunkIndex - 1) + 1;
132+		}
133+		return currentStart > minLine;
134+	}
135+
136+	function canExpandAfter(hunkIndex: number): boolean {
137+		const currentEnd = getHunkEndLine(hunkIndex);
138+		if (hunkIndex < file.hunks.length - 1) {
139+			const nextStart = getHunkStartLine(hunkIndex + 1);
140+			return currentEnd < nextStart - 1;
141+		}
142+		return true; // last hunk — can always try
143+	}
144+
145+	// Hide the hunk header when the previous hunk's expanded area
146+	// reaches right up to this hunk (no gap left)
147+	function shouldShowHeader(hunkIndex: number): boolean {
148+		if (hunkIndex === 0) return true; // always show first header
149+		return canExpandBefore(hunkIndex);
150+	}
151+</script>
152+
153+<div class="diff-view font-mono text-sm">
154+	{#each file.hunks as hunk, hunkIndex}
155+		{@const beforeLines = getExpandedBefore(hunkIndex)}
156+		{@const afterLines = getExpandedAfter(hunkIndex)}
157+
158+		<!-- Hunk header — hidden when merged with previous hunk -->
159+		{#if shouldShowHeader(hunkIndex)}
160+			<div class="bg-zinc-800 text-zinc-500 text-xs border-y border-zinc-700 select-none flex items-center">
161+				{#if canExpandBefore(hunkIndex)}
162+					<button
163+						class="text-blue-400 hover:text-blue-300 hover:bg-zinc-700 transition-colors px-3 py-1"
164+						onclick={() => expandBefore(hunkIndex)}
165+						title="Show {EXPAND_COUNT} more lines above"
166+					>
167+						<svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
168+							<path stroke-linecap="round" stroke-linejoin="round" d="M5 15l7-7 7 7" />
169+						</svg>
170+					</button>
171+				{/if}
172+				<span class="flex-1 px-2 py-1">{hunk.header}</span>
173+			</div>
174+		{/if}
175+
176+		{#if $viewMode === 'unified'}
177+			<!-- Expanded lines before hunk -->
178+			{#each beforeLines as line}
179+				{@const lineNum = line.newNumber || line.oldNumber || 0}
180+				<div class="diff-line group flex hover:bg-zinc-700/20 relative">
181+					<button class="absolute left-0 top-0 w-5 h-full flex items-center justify-center opacity-0 group-hover:opacity-100 text-blue-400 hover:text-blue-300 z-10 transition-opacity" onclick={() => startComment(lineNum, 'new', line.content)} title="Add comment">+</button>
182+					<span class="w-12 shrink-0 text-right pr-2 text-zinc-600 select-none text-xs leading-6 pl-5">{line.oldNumber ?? ''}</span>
183+					<span class="w-12 shrink-0 text-right pr-2 text-zinc-600 select-none text-xs leading-6 border-r border-zinc-700">{line.newNumber ?? ''}</span>
184+					<span class="w-6 shrink-0 text-center select-none leading-6"> </span>
185+					<span class="flex-1 whitespace-pre-wrap break-all leading-6 pr-4">{#if line.html}{@html line.html}{:else}{line.content || ' '}{/if}</span>
186+				</div>
187+			{/each}
188+
189+			<!-- Actual hunk lines -->
190+			{#each hunk.lines as line}
191+				{@const lineNum = getLineKey(line)}
192+				{@const side = getLineSide(line)}
193+				{@const lineThreads = getThreadsForLine(lineNum, side)}
194+				<div class="diff-line group flex hover:bg-zinc-700/20 relative {lineBg(line.type)}">
195+					<button class="absolute left-0 top-0 w-5 h-full flex items-center justify-center opacity-0 group-hover:opacity-100 text-blue-400 hover:text-blue-300 z-10 transition-opacity" onclick={() => startComment(lineNum, side, line.content)} title="Add comment">+</button>
196+					<span class="w-12 shrink-0 text-right pr-2 text-zinc-600 select-none text-xs leading-6 pl-5">{line.oldNumber ?? ''}</span>
197+					<span class="w-12 shrink-0 text-right pr-2 text-zinc-600 select-none text-xs leading-6 border-r border-zinc-700">{line.newNumber ?? ''}</span>
198+					<span class="w-6 shrink-0 text-center select-none leading-6 {line.type === 'add' ? 'text-green-400' : line.type === 'del' ? 'text-red-400' : ''}">{line.type === 'add' ? '+' : line.type === 'del' ? '-' : ' '}</span>
199+					<span class="flex-1 whitespace-pre-wrap break-all leading-6 pr-4">{#if line.html}{@html line.html}{:else}{line.content || ' '}{/if}</span>
200+				</div>
201+				{#each lineThreads as thread}<CommentThread {thread} />{/each}
202+				{#if isCommenting(lineNum, side)}<CommentEditor filePath={file.path} lineNumber={lineNum} {side} originalLineContent={$commentingLine?.originalLineContent} />{/if}
203+			{/each}
204+
205+			<!-- Expanded lines after hunk -->
206+			{#each afterLines as line}
207+				{@const lineNum = line.newNumber || line.oldNumber || 0}
208+				<div class="diff-line group flex hover:bg-zinc-700/20 relative">
209+					<button class="absolute left-0 top-0 w-5 h-full flex items-center justify-center opacity-0 group-hover:opacity-100 text-blue-400 hover:text-blue-300 z-10 transition-opacity" onclick={() => startComment(lineNum, 'new', line.content)} title="Add comment">+</button>
210+					<span class="w-12 shrink-0 text-right pr-2 text-zinc-600 select-none text-xs leading-6 pl-5">{line.oldNumber ?? ''}</span>
211+					<span class="w-12 shrink-0 text-right pr-2 text-zinc-600 select-none text-xs leading-6 border-r border-zinc-700">{line.newNumber ?? ''}</span>
212+					<span class="w-6 shrink-0 text-center select-none leading-6"> </span>
213+					<span class="flex-1 whitespace-pre-wrap break-all leading-6 pr-4">{#if line.html}{@html line.html}{:else}{line.content || ' '}{/if}</span>
214+				</div>
215+			{/each}
216+
217+			<!-- Expand down button below hunk content -->
218+			{#if canExpandAfter(hunkIndex)}
219+				<div class="flex items-center border-b border-zinc-700/50 bg-zinc-800/30 hover:bg-zinc-800/60 transition-colors">
220+					<button
221+						class="text-blue-400 hover:text-blue-300 transition-colors px-3 py-0.5 flex items-center gap-2 text-xs"
222+						onclick={() => expandAfter(hunkIndex)}
223+						title="Show {EXPAND_COUNT} more lines below"
224+					>
225+						<svg class="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
226+							<path stroke-linecap="round" stroke-linejoin="round" d="M19 9l-7 7-7-7" />
227+						</svg>
228+						Expand
229+					</button>
230+				</div>
231+			{/if}
232+		{:else}
233+			<!-- SPLIT VIEW -->
234+			<!-- Expanded before -->
235+			{#each beforeLines as line}
236+				<div class="flex">
237+					<div class="w-1/2 border-r border-zinc-700">
238+						<div class="diff-line flex leading-6">
239+							<span class="w-12 shrink-0 text-right pr-2 text-zinc-600 select-none text-xs leading-6 pl-5">{line.oldNumber ?? ''}</span>
240+							<span class="w-6 shrink-0 text-center select-none leading-6"> </span>
241+							<span class="flex-1 whitespace-pre-wrap break-all leading-6 pr-2">{#if line.html}{@html line.html}{:else}{line.content || ' '}{/if}</span>
242+						</div>
243+					</div>
244+					<div class="w-1/2">
245+						<div class="diff-line flex leading-6">
246+							<span class="w-12 shrink-0 text-right pr-2 text-zinc-600 select-none text-xs leading-6 pl-5">{line.newNumber ?? ''}</span>
247+							<span class="w-6 shrink-0 text-center select-none leading-6"> </span>
248+							<span class="flex-1 whitespace-pre-wrap break-all leading-6 pr-2">{#if line.html}{@html line.html}{:else}{line.content || ' '}{/if}</span>
249+						</div>
250+					</div>
251+				</div>
252+			{/each}
253+
254+			<!-- Split hunk lines -->
255+			{@const pairs = buildSplitPairs(hunk.lines)}
256+			{#each pairs as pair}
257+				{@const oldThreads = pair.old ? getThreadsForLine(pair.old.oldNumber!, 'old') : []}
258+				{@const newThreads = pair.new ? getThreadsForLine(pair.new.newNumber!, 'new') : []}
259+				<div class="flex">
260+					<div class="w-1/2 border-r border-zinc-700">
261+						{#if pair.old}
262+							<div class="diff-line group flex hover:bg-zinc-700/20 relative {pair.old.type === 'del' ? 'bg-red-950/40' : ''}">
263+								<button class="absolute left-0 top-0 w-5 h-full flex items-center justify-center opacity-0 group-hover:opacity-100 text-blue-400 hover:text-blue-300 z-10 transition-opacity" onclick={() => startComment(pair.old!.oldNumber!, 'old', pair.old!.content)} title="Add comment">+</button>
264+								<span class="w-12 shrink-0 text-right pr-2 text-zinc-600 select-none text-xs leading-6 pl-5">{pair.old.oldNumber ?? ''}</span>
265+								<span class="w-6 shrink-0 text-center select-none leading-6 {pair.old.type === 'del' ? 'text-red-400' : ''}">{pair.old.type === 'del' ? '-' : ' '}</span>
266+								<span class="flex-1 whitespace-pre-wrap break-all leading-6 pr-2">{#if pair.old.html}{@html pair.old.html}{:else}{pair.old.content || ' '}{/if}</span>
267+							</div>
268+						{:else}<div class="diff-line flex bg-zinc-800/20 leading-6">&nbsp;</div>{/if}
269+					</div>
270+					<div class="w-1/2">
271+						{#if pair.new}
272+							<div class="diff-line group flex hover:bg-zinc-700/20 relative {pair.new.type === 'add' ? 'bg-green-950/40' : ''}">
273+								<button class="absolute left-0 top-0 w-5 h-full flex items-center justify-center opacity-0 group-hover:opacity-100 text-blue-400 hover:text-blue-300 z-10 transition-opacity" onclick={() => startComment(pair.new!.newNumber!, 'new', pair.new!.content)} title="Add comment">+</button>
274+								<span class="w-12 shrink-0 text-right pr-2 text-zinc-600 select-none text-xs leading-6 pl-5">{pair.new.newNumber ?? ''}</span>
275+								<span class="w-6 shrink-0 text-center select-none leading-6 {pair.new.type === 'add' ? 'text-green-400' : ''}">{pair.new.type === 'add' ? '+' : ' '}</span>
276+								<span class="flex-1 whitespace-pre-wrap break-all leading-6 pr-2">{#if pair.new.html}{@html pair.new.html}{:else}{pair.new.content || ' '}{/if}</span>
277+							</div>
278+						{:else}<div class="diff-line flex bg-zinc-800/20 leading-6">&nbsp;</div>{/if}
279+					</div>
280+				</div>
281+				{#if oldThreads.length > 0 || newThreads.length > 0}
282+					<div class="flex">
283+						<div class="w-1/2 border-r border-zinc-700">
284+							{#each oldThreads as thread}<CommentThread {thread} />{/each}
285+							{#if pair.old && isCommenting(pair.old.oldNumber!, 'old')}<CommentEditor filePath={file.path} lineNumber={pair.old.oldNumber!} side="old" originalLineContent={$commentingLine?.originalLineContent} />{/if}
286+						</div>
287+						<div class="w-1/2">
288+							{#each newThreads as thread}<CommentThread {thread} />{/each}
289+							{#if pair.new && isCommenting(pair.new.newNumber!, 'new')}<CommentEditor filePath={file.path} lineNumber={pair.new.newNumber!} side="new" originalLineContent={$commentingLine?.originalLineContent} />{/if}
290+						</div>
291+					</div>
292+				{/if}
293+			{/each}
294+
295+			<!-- Expanded after -->
296+			{#each afterLines as line}
297+				<div class="flex">
298+					<div class="w-1/2 border-r border-zinc-700">
299+						<div class="diff-line flex leading-6">
300+							<span class="w-12 shrink-0 text-right pr-2 text-zinc-600 select-none text-xs leading-6 pl-5">{line.oldNumber ?? ''}</span>
301+							<span class="w-6 shrink-0 text-center select-none leading-6"> </span>
302+							<span class="flex-1 whitespace-pre-wrap break-all leading-6 pr-2">{#if line.html}{@html line.html}{:else}{line.content || ' '}{/if}</span>
303+						</div>
304+					</div>
305+					<div class="w-1/2">
306+						<div class="diff-line flex leading-6">
307+							<span class="w-12 shrink-0 text-right pr-2 text-zinc-600 select-none text-xs leading-6 pl-5">{line.newNumber ?? ''}</span>
308+							<span class="w-6 shrink-0 text-center select-none leading-6"> </span>
309+							<span class="flex-1 whitespace-pre-wrap break-all leading-6 pr-2">{#if line.html}{@html line.html}{:else}{line.content || ' '}{/if}</span>
310+						</div>
311+					</div>
312+				</div>
313+			{/each}
314+
315+			{#if canExpandAfter(hunkIndex)}
316+				<div class="flex items-center border-b border-zinc-700/50 bg-zinc-800/30 hover:bg-zinc-800/60 transition-colors">
317+					<button
318+						class="text-blue-400 hover:text-blue-300 transition-colors px-3 py-0.5 flex items-center gap-2 text-xs"
319+						onclick={() => expandAfter(hunkIndex)}
320+						title="Show {EXPAND_COUNT} more lines below"
321+					>
322+						<svg class="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
323+							<path stroke-linecap="round" stroke-linejoin="round" d="M19 9l-7 7-7-7" />
324+						</svg>
325+						Expand
326+					</button>
327+				</div>
328+			{/if}
329+		{/if}
330+	{/each}
331+</div>
332+
333+<script lang="ts" module>
334+	import type { DiffLine as DL } from '$lib/types/index.ts';
335+
336+	interface SplitPair {
337+		old: DL | null;
338+		new: DL | null;
339+	}
340+
341+	function buildSplitPairs(lines: DL[]): SplitPair[] {
342+		const pairs: SplitPair[] = [];
343+		const dels: DL[] = [];
344+		const adds: DL[] = [];
345+
346+		function flushPending() {
347+			const max = Math.max(dels.length, adds.length);
348+			for (let i = 0; i < max; i++) {
349+				pairs.push({
350+					old: dels[i] || null,
351+					new: adds[i] || null
352+				});
353+			}
354+			dels.length = 0;
355+			adds.length = 0;
356+		}
357+
358+		for (const line of lines) {
359+			if (line.type === 'context') {
360+				flushPending();
361+				pairs.push({ old: line, new: line });
362+			} else if (line.type === 'del') {
363+				dels.push(line);
364+			} else if (line.type === 'add') {
365+				adds.push(line);
366+			}
367+		}
368+		flushPending();
369+		return pairs;
370+	}
371+</script>
A src/lib/components/ExportDialog.svelte
+115, -0
  1@@ -0,0 +1,115 @@
  2+<script lang="ts">
  3+	import { showExportDialog } from '$lib/stores/ui.ts';
  4+	import { sendReviewToClaude } from '$lib/stores/terminal.ts';
  5+
  6+	let exporting = $state(false);
  7+	let result = $state<{ success: boolean; path?: string; message?: string; text?: string } | null>(null);
  8+
  9+	async function exportAs(format: string) {
 10+		exporting = true;
 11+		result = null;
 12+		try {
 13+			if (format === 'claude') {
 14+				// Use terminal panel with streaming
 15+				const res = await sendReviewToClaude();
 16+				if (res.success) {
 17+					$showExportDialog = false;
 18+					return;
 19+				}
 20+				result = res;
 21+				return;
 22+			}
 23+
 24+			const res = await fetch('/api/export', {
 25+				method: 'POST',
 26+				headers: { 'Content-Type': 'application/json' },
 27+				body: JSON.stringify({ format })
 28+			});
 29+			result = await res.json();
 30+
 31+			if (format === 'clipboard' && result?.text) {
 32+				await navigator.clipboard.writeText(result.text);
 33+				result = { success: true, message: 'Copied to clipboard!' };
 34+			}
 35+		} catch (err) {
 36+			result = { success: false, message: String(err) };
 37+		} finally {
 38+			exporting = false;
 39+		}
 40+	}
 41+
 42+	function close() {
 43+		$showExportDialog = false;
 44+		result = null;
 45+	}
 46+</script>
 47+
 48+{#if $showExportDialog}
 49+	<!-- svelte-ignore a11y_click_events_have_key_events -->
 50+	<!-- svelte-ignore a11y_no_static_element_interactions -->
 51+	<div class="fixed inset-0 bg-black/50 z-50 flex items-center justify-center" onclick={close}>
 52+		<!-- svelte-ignore a11y_click_events_have_key_events -->
 53+		<!-- svelte-ignore a11y_no_static_element_interactions -->
 54+		<div class="bg-zinc-800 border border-zinc-600 rounded-xl shadow-2xl w-full max-w-md mx-4" onclick={(e) => e.stopPropagation()}>
 55+			<div class="px-5 py-4 border-b border-zinc-700">
 56+				<h2 class="text-lg font-semibold text-zinc-200">Export Review</h2>
 57+				<p class="text-sm text-zinc-400 mt-1">Send your review comments to AI or save for reference</p>
 58+			</div>
 59+
 60+			<div class="p-5 space-y-2">
 61+				<button
 62+					class="w-full text-left px-4 py-3 rounded-lg border border-zinc-600 hover:bg-zinc-700/50 transition-colors group"
 63+					onclick={() => exportAs('claude')}
 64+					disabled={exporting}
 65+				>
 66+					<div class="text-sm font-medium text-zinc-200 group-hover:text-white">Send to Claude Code</div>
 67+					<div class="text-xs text-zinc-400 mt-0.5">Stream review to Claude with live output in terminal</div>
 68+				</button>
 69+
 70+				<button
 71+					class="w-full text-left px-4 py-3 rounded-lg border border-zinc-600 hover:bg-zinc-700/50 transition-colors group"
 72+					onclick={() => exportAs('clipboard')}
 73+					disabled={exporting}
 74+				>
 75+					<div class="text-sm font-medium text-zinc-200 group-hover:text-white">Copy to Clipboard</div>
 76+					<div class="text-xs text-zinc-400 mt-0.5">Copy structured review text for any AI tool</div>
 77+				</button>
 78+
 79+				<button
 80+					class="w-full text-left px-4 py-3 rounded-lg border border-zinc-600 hover:bg-zinc-700/50 transition-colors group"
 81+					onclick={() => exportAs('json')}
 82+					disabled={exporting}
 83+				>
 84+					<div class="text-sm font-medium text-zinc-200 group-hover:text-white">Export as JSON</div>
 85+					<div class="text-xs text-zinc-400 mt-0.5">Save .lcr-review.json in project root</div>
 86+				</button>
 87+
 88+				<button
 89+					class="w-full text-left px-4 py-3 rounded-lg border border-zinc-600 hover:bg-zinc-700/50 transition-colors group"
 90+					onclick={() => exportAs('markdown')}
 91+					disabled={exporting}
 92+				>
 93+					<div class="text-sm font-medium text-zinc-200 group-hover:text-white">Export as Markdown</div>
 94+					<div class="text-xs text-zinc-400 mt-0.5">Save .lcr-review.md in project root</div>
 95+				</button>
 96+			</div>
 97+
 98+			{#if result}
 99+				<div class="px-5 pb-3">
100+					<div class="text-sm rounded-lg px-3 py-2 {result.success ? 'bg-green-500/10 text-green-400' : 'bg-red-500/10 text-red-400'}">
101+						{result.message || result.path || 'Done!'}
102+					</div>
103+				</div>
104+			{/if}
105+
106+			<div class="px-5 py-3 border-t border-zinc-700 flex justify-end">
107+				<button
108+					class="text-sm px-4 py-2 rounded text-zinc-400 hover:text-zinc-200 hover:bg-zinc-700 transition-colors"
109+					onclick={close}
110+				>
111+					Close
112+				</button>
113+			</div>
114+		</div>
115+	</div>
116+{/if}
A src/lib/components/FileTree.svelte
+88, -0
 1@@ -0,0 +1,88 @@
 2+<script lang="ts">
 3+	import { files, selectedFile, fileCommentCounts, toggleApproval } from '$lib/stores/files.ts';
 4+
 5+	const statusColors: Record<string, string> = {
 6+		added: 'text-green-400',
 7+		modified: 'text-yellow-400',
 8+		deleted: 'text-red-400',
 9+		renamed: 'text-blue-400'
10+	};
11+
12+	const statusLabels: Record<string, string> = {
13+		added: 'A',
14+		modified: 'M',
15+		deleted: 'D',
16+		renamed: 'R'
17+	};
18+
19+	function selectFile(path: string) {
20+		$selectedFile = path;
21+	}
22+
23+	function getFilename(path: string): string {
24+		return path.split('/').pop() || path;
25+	}
26+
27+	function getDirectory(path: string): string {
28+		const parts = path.split('/');
29+		if (parts.length <= 1) return '';
30+		return parts.slice(0, -1).join('/');
31+	}
32+
33+	function handleApprove(e: Event, path: string) {
34+		e.stopPropagation();
35+		toggleApproval(path);
36+	}
37+</script>
38+
39+<div class="h-full overflow-y-auto">
40+	<div class="px-3 py-2 text-xs font-semibold text-zinc-400 uppercase tracking-wider">
41+		Changed Files ({$files.length})
42+	</div>
43+	<div class="space-y-px">
44+		{#each $files as file}
45+			{@const isSelected = $selectedFile === file.path}
46+			{@const commentCount = $fileCommentCounts[file.path] || 0}
47+			<!-- svelte-ignore a11y_click_events_have_key_events -->
48+			<!-- svelte-ignore a11y_no_static_element_interactions -->
49+			<div
50+				class="w-full text-left px-3 py-1.5 flex items-center gap-2 text-sm hover:bg-zinc-700/50 transition-colors cursor-pointer {isSelected ? 'bg-zinc-700/80 text-white' : 'text-zinc-300'} {file.approved ? 'opacity-60' : ''}"
51+				onclick={() => selectFile(file.path)}
52+			>
53+				<!-- Approve checkbox -->
54+				<button
55+					class="shrink-0 w-4 h-4 rounded border flex items-center justify-center transition-colors {file.approved ? 'bg-green-600 border-green-600 text-white' : 'border-zinc-500 hover:border-green-400 text-transparent hover:text-green-400'}"
56+					onclick={(e) => handleApprove(e, file.path)}
57+					title={file.approved ? 'Unapprove file' : 'Approve file'}
58+				>
59+					<svg class="w-3 h-3" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="3">
60+						<path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7" />
61+					</svg>
62+				</button>
63+
64+				<span class="font-mono text-xs font-bold w-4 text-center {statusColors[file.status]}">
65+					{statusLabels[file.status]}
66+				</span>
67+				<span class="truncate flex-1 {file.approved ? 'line-through decoration-zinc-500' : ''}">
68+					{#if getDirectory(file.path)}
69+						<span class="text-zinc-500">{getDirectory(file.path)}/</span>
70+					{/if}
71+					<span class="font-medium">{getFilename(file.path)}</span>
72+				</span>
73+				<span class="text-xs text-zinc-500 tabular-nums shrink-0">
74+					{#if file.additions > 0}
75+						<span class="text-green-400">+{file.additions}</span>
76+					{/if}
77+					{#if file.deletions > 0}
78+						<span class="text-red-400">-{file.deletions}</span>
79+					{/if}
80+				</span>
81+				{#if commentCount > 0}
82+					<span class="bg-blue-500/20 text-blue-400 text-xs px-1.5 py-0.5 rounded-full font-medium">
83+						{commentCount}
84+					</span>
85+				{/if}
86+			</div>
87+		{/each}
88+	</div>
89+</div>
A src/lib/components/TerminalPanel.svelte
+273, -0
  1@@ -0,0 +1,273 @@
  2+<script lang="ts">
  3+	import {
  4+		terminalOpen,
  5+		terminalHeight,
  6+		sendReviewToClaude,
  7+		stopClaudeSession,
  8+		restartSession,
  9+		hasPendingReview
 10+	} from '$lib/stores/terminal.ts';
 11+	import { onDestroy } from 'svelte';
 12+
 13+	let isResizing = $state(false);
 14+	let minimized = $state(false);
 15+	let connected = $state(false);
 16+	let initError = $state<string | null>(null);
 17+
 18+	let term: any = null;
 19+	let fitAddon: any = null;
 20+	let ws: WebSocket | null = null;
 21+	let resizeObserver: ResizeObserver | null = null;
 22+
 23+	onDestroy(() => {
 24+		resizeObserver?.disconnect();
 25+		ws?.close();
 26+		ws = null;
 27+		term?.dispose();
 28+		term = null;
 29+	});
 30+
 31+	// Svelte action — runs when the DOM element is actually mounted
 32+	function terminalAction(node: HTMLDivElement) {
 33+		initTerminal(node);
 34+		return {
 35+			destroy() {
 36+				resizeObserver?.disconnect();
 37+				resizeObserver = null;
 38+			}
 39+		};
 40+	}
 41+
 42+	async function initTerminal(node: HTMLDivElement) {
 43+		try {
 44+			const [{ Terminal }, { FitAddon }] = await Promise.all([
 45+				import('@xterm/xterm'),
 46+				import('@xterm/addon-fit')
 47+			]);
 48+			await import('@xterm/xterm/css/xterm.css');
 49+
 50+			term = new Terminal({
 51+				cursorBlink: true,
 52+				fontSize: 13,
 53+				fontFamily: 'Menlo, Monaco, "Courier New", monospace',
 54+				theme: {
 55+					background: '#1e1e1e',
 56+					foreground: '#d4d4d4',
 57+					cursor: '#d4d4d4',
 58+					selectionBackground: '#264f78',
 59+					black: '#1e1e1e',
 60+					red: '#f44747',
 61+					green: '#6a9955',
 62+					yellow: '#d7ba7d',
 63+					blue: '#569cd6',
 64+					magenta: '#c586c0',
 65+					cyan: '#4ec9b0',
 66+					white: '#d4d4d4',
 67+					brightBlack: '#808080',
 68+					brightRed: '#f44747',
 69+					brightGreen: '#6a9955',
 70+					brightYellow: '#d7ba7d',
 71+					brightBlue: '#569cd6',
 72+					brightMagenta: '#c586c0',
 73+					brightCyan: '#4ec9b0',
 74+					brightWhite: '#ffffff'
 75+				},
 76+				allowProposedApi: true
 77+			});
 78+
 79+			fitAddon = new FitAddon();
 80+			term.loadAddon(fitAddon);
 81+			term.open(node);
 82+
 83+			requestAnimationFrame(() => fitAddon?.fit());
 84+
 85+			connectWs();
 86+
 87+			term.onData((data: string) => {
 88+				if (ws?.readyState === WebSocket.OPEN) {
 89+					ws.send(JSON.stringify({ type: 'input', data }));
 90+				}
 91+			});
 92+
 93+			resizeObserver = new ResizeObserver(() => {
 94+				if (fitAddon && !minimized) {
 95+					fitAddon.fit();
 96+					if (ws?.readyState === WebSocket.OPEN && term) {
 97+						ws.send(JSON.stringify({
 98+							type: 'resize',
 99+							cols: term.cols,
100+							rows: term.rows
101+						}));
102+					}
103+				}
104+			});
105+			resizeObserver.observe(node);
106+		} catch (err) {
107+			initError = String(err);
108+			console.error('Terminal init failed:', err);
109+		}
110+	}
111+
112+	function getWsUrl(): string {
113+		const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
114+		if (import.meta.env.DEV) {
115+			// If VITE_LCR_WS_PORT is set, use it. Otherwise derive from page port + 1000.
116+			const wsPort = import.meta.env.VITE_LCR_WS_PORT || String(parseInt(window.location.port, 10) + 1000);
117+			return `ws://localhost:${wsPort}/ws/terminal`;
118+		}
119+		return `${protocol}//${window.location.host}/ws/terminal`;
120+	}
121+
122+	function connectWs() {
123+		ws = new WebSocket(getWsUrl());
124+
125+		ws.onopen = () => {
126+			connected = true;
127+			// Send resize info
128+			if (term) {
129+				ws!.send(JSON.stringify({ type: 'resize', cols: term.cols, rows: term.rows }));
130+			}
131+			// Check for pending review from ExportDialog
132+			if (hasPendingReview()) {
133+				ws!.send(JSON.stringify({ type: 'review' }));
134+			}
135+		};
136+
137+		ws.onmessage = (event) => {
138+			try {
139+				const msg = JSON.parse(event.data);
140+				if (msg.type === 'output' && term) {
141+					term.write(msg.data);
142+				} else if (msg.type === 'exit') {
143+					term?.write('\r\n\x1b[33mSession ended.\x1b[0m\r\n');
144+					connected = false;
145+				} else if (msg.type === 'request-resize' && term) {
146+					// Server needs dimensions to start a new session
147+					ws!.send(JSON.stringify({ type: 'resize', cols: term.cols, rows: term.rows }));
148+				}
149+			} catch {
150+				term?.write(event.data);
151+			}
152+		};
153+
154+		ws.onclose = () => {
155+			connected = false;
156+			setTimeout(() => {
157+				if ($terminalOpen && (!ws || ws.readyState === WebSocket.CLOSED)) {
158+					connectWs();
159+				}
160+			}, 2000);
161+		};
162+
163+		ws.onerror = () => {
164+			connected = false;
165+		};
166+
167+		(globalThis as any).__lcr_ws = ws;
168+	}
169+
170+	function startResize(e: MouseEvent) {
171+		e.preventDefault();
172+		isResizing = true;
173+		const startY = e.clientY;
174+		const startHeight = $terminalHeight;
175+
176+		function onMove(e: MouseEvent) {
177+			const delta = startY - e.clientY;
178+			const newHeight = Math.max(100, Math.min(window.innerHeight * 0.8, startHeight + delta));
179+			$terminalHeight = newHeight;
180+		}
181+
182+		function onUp() {
183+			isResizing = false;
184+			window.removeEventListener('mousemove', onMove);
185+			window.removeEventListener('mouseup', onUp);
186+			requestAnimationFrame(() => fitAddon?.fit());
187+		}
188+
189+		window.addEventListener('mousemove', onMove);
190+		window.addEventListener('mouseup', onUp);
191+	}
192+</script>
193+
194+{#if $terminalOpen}
195+	<div
196+		class="flex flex-col border-t border-zinc-700 bg-[#1e1e1e]"
197+		style="height: {minimized ? 'auto' : `${$terminalHeight}px`}"
198+	>
199+		{#if !minimized}
200+			<div
201+				class="h-1 cursor-row-resize hover:bg-blue-500/50 transition-colors {isResizing ? 'bg-blue-500/50' : ''}"
202+				onmousedown={startResize}
203+				role="separator"
204+			></div>
205+		{/if}
206+
207+		<div class="flex items-center gap-2 px-3 py-1.5 bg-zinc-900 border-b border-zinc-700/50 select-none shrink-0">
208+			<div class="flex items-center gap-2">
209+				<span class="text-xs font-medium text-zinc-300">Claude Terminal</span>
210+				<span class="flex items-center gap-1">
211+					<span class="w-2 h-2 rounded-full {connected ? 'bg-green-400' : 'bg-zinc-500'}"></span>
212+					<span class="text-[10px] text-zinc-500">{connected ? 'Connected' : 'Disconnected'}</span>
213+				</span>
214+			</div>
215+
216+			<div class="flex-1"></div>
217+
218+			<div class="flex items-center gap-1">
219+				<button
220+					onclick={sendReviewToClaude}
221+					class="text-[10px] px-2 py-0.5 rounded text-blue-400 hover:bg-blue-400/10 transition-colors"
222+					title="Send current review comments to Claude"
223+				>
224+					Send Review
225+				</button>
226+
227+				<button
228+					onclick={stopClaudeSession}
229+					class="text-[10px] px-2 py-0.5 rounded text-red-400 hover:bg-red-400/10 transition-colors"
230+				>
231+					Stop
232+				</button>
233+
234+				<button
235+					onclick={restartSession}
236+					class="text-[10px] px-2 py-0.5 rounded text-zinc-400 hover:bg-zinc-700 transition-colors"
237+				>
238+					Restart
239+				</button>
240+
241+				<button
242+					onclick={() => term?.clear()}
243+					class="text-[10px] px-2 py-0.5 rounded text-zinc-400 hover:bg-zinc-700 transition-colors"
244+				>
245+					Clear
246+				</button>
247+
248+				<button
249+					onclick={() => minimized = !minimized}
250+					class="text-[10px] px-2 py-0.5 rounded text-zinc-400 hover:bg-zinc-700 transition-colors"
251+				>
252+					{minimized ? 'Expand' : 'Minimize'}
253+				</button>
254+
255+				<button
256+					onclick={() => $terminalOpen = false}
257+					class="text-[10px] px-2 py-0.5 rounded text-zinc-400 hover:bg-zinc-700 transition-colors"
258+				>
259+					Close
260+				</button>
261+			</div>
262+		</div>
263+
264+		{#if !minimized}
265+			<div
266+				use:terminalAction
267+				class="flex-1 overflow-hidden"
268+			></div>
269+			{#if initError}
270+				<div class="p-3 text-red-400 text-xs">{initError}</div>
271+			{/if}
272+		{/if}
273+	</div>
274+{/if}
A src/lib/components/Toolbar.svelte
+136, -0
  1@@ -0,0 +1,136 @@
  2+<script lang="ts">
  3+	import { viewMode, showCommitDialog, showExportDialog } from '$lib/stores/ui.ts';
  4+	import { stats, reviewStatus, submitReview } from '$lib/stores/review.ts';
  5+	import { diffMode, approvedCount, diffStats, loadAllDiffs } from '$lib/stores/files.ts';
  6+
  7+	let showReviewMenu = $state(false);
  8+
  9+	async function handleApprove() {
 10+		await submitReview('approved');
 11+		$showCommitDialog = true;
 12+	}
 13+
 14+	async function handleRequestChanges() {
 15+		await submitReview('changes_requested');
 16+		$showExportDialog = true;
 17+	}
 18+
 19+	async function setDiffMode(mode: 'full' | 'unstaged') {
 20+		$diffMode = mode;
 21+		await loadAllDiffs(mode);
 22+	}
 23+</script>
 24+
 25+<header class="bg-zinc-800 border-b border-zinc-700 px-4 py-2 flex items-center gap-3">
 26+	<h1 class="text-sm font-semibold text-zinc-200 mr-4">Local Code Review</h1>
 27+
 28+	<!-- View mode toggle -->
 29+	<div class="flex bg-zinc-900 rounded overflow-hidden border border-zinc-700">
 30+		<button
 31+			class="text-xs px-3 py-1 transition-colors {$viewMode === 'unified' ? 'bg-zinc-600 text-white' : 'text-zinc-400 hover:text-zinc-200'}"
 32+			onclick={() => ($viewMode = 'unified')}
 33+		>
 34+			Unified
 35+		</button>
 36+		<button
 37+			class="text-xs px-3 py-1 transition-colors {$viewMode === 'split' ? 'bg-zinc-600 text-white' : 'text-zinc-400 hover:text-zinc-200'}"
 38+			onclick={() => ($viewMode = 'split')}
 39+		>
 40+			Split
 41+		</button>
 42+	</div>
 43+
 44+	<!-- Diff mode toggle -->
 45+	<div class="flex bg-zinc-900 rounded overflow-hidden border border-zinc-700">
 46+		<button
 47+			class="text-xs px-3 py-1 transition-colors {$diffMode === 'full' ? 'bg-zinc-600 text-white' : 'text-zinc-400 hover:text-zinc-200'}"
 48+			onclick={() => setDiffMode('full')}
 49+		>
 50+			Full Diff
 51+		</button>
 52+		<button
 53+			class="text-xs px-3 py-1 transition-colors {$diffMode === 'unstaged' ? 'bg-zinc-600 text-white' : 'text-zinc-400 hover:text-zinc-200'}"
 54+			onclick={() => setDiffMode('unstaged')}
 55+		>
 56+			Needs Review
 57+		</button>
 58+	</div>
 59+
 60+	<div class="flex-1"></div>
 61+
 62+	<!-- Diff stats -->
 63+	{#if $approvedCount.total > 0}
 64+		<span class="text-xs text-zinc-400 flex items-center gap-2">
 65+			<span><span class="text-green-400">{$approvedCount.approved}</span>/{$approvedCount.total} approved</span>
 66+			<span class="text-zinc-600">|</span>
 67+			<span class="text-green-400">+{$diffStats.additions}</span>
 68+			<span class="text-red-400">-{$diffStats.deletions}</span>
 69+			{#if $diffStats.unapprovedAdditions > 0 || $diffStats.unapprovedDeletions > 0}
 70+				<span class="text-zinc-600">|</span>
 71+				<span class="text-yellow-400">
 72+					{$diffStats.unapprovedAdditions + $diffStats.unapprovedDeletions} lines to review
 73+				</span>
 74+			{/if}
 75+		</span>
 76+	{/if}
 77+
 78+	<!-- Stats -->
 79+	{#if $stats.totalComments > 0}
 80+		<span class="text-xs text-zinc-400">
 81+			{$stats.totalComments} comment{$stats.totalComments !== 1 ? 's' : ''}
 82+			{#if $stats.unresolvedThreads > 0}
 83+				<span class="text-yellow-400">({$stats.unresolvedThreads} unresolved)</span>
 84+			{/if}
 85+		</span>
 86+	{/if}
 87+
 88+	<!-- Status badge -->
 89+	{#if $reviewStatus !== 'pending'}
 90+		<span
 91+			class="text-xs px-2 py-0.5 rounded-full {$reviewStatus === 'approved' ? 'bg-green-500/20 text-green-400' : 'bg-yellow-500/20 text-yellow-400'}"
 92+		>
 93+			{$reviewStatus === 'approved' ? 'Approved' : 'Changes Requested'}
 94+		</span>
 95+	{/if}
 96+
 97+	<!-- Export button -->
 98+	<button
 99+		class="text-xs px-3 py-1.5 rounded border border-zinc-600 text-zinc-300 hover:bg-zinc-700 transition-colors"
100+		onclick={() => ($showExportDialog = true)}
101+	>
102+		Export
103+	</button>
104+
105+	<!-- Review actions -->
106+	<div class="relative">
107+		<button
108+			class="text-xs px-3 py-1.5 rounded bg-green-600 text-white hover:bg-green-500 transition-colors"
109+			onclick={() => (showReviewMenu = !showReviewMenu)}
110+		>
111+			Review ▾
112+		</button>
113+
114+		{#if showReviewMenu}
115+			<!-- svelte-ignore a11y_click_events_have_key_events -->
116+			<!-- svelte-ignore a11y_no_static_element_interactions -->
117+			<div
118+				class="fixed inset-0 z-10"
119+				onclick={() => (showReviewMenu = false)}
120+			></div>
121+			<div class="absolute right-0 top-full mt-1 bg-zinc-800 border border-zinc-600 rounded-lg shadow-xl z-20 py-1 min-w-[220px]">
122+				<button
123+					class="w-full text-left px-3 py-2 text-sm text-zinc-300 hover:bg-zinc-700 transition-colors"
124+					onclick={() => { handleRequestChanges(); showReviewMenu = false; }}
125+				>
126+					<span class="text-yellow-400 mr-2">↻</span> Request Changes (send to AI)
127+				</button>
128+				<button
129+					class="w-full text-left px-3 py-2 text-sm text-zinc-300 hover:bg-zinc-700 transition-colors"
130+					onclick={() => { handleApprove(); showReviewMenu = false; }}
131+				>
132+					<span class="text-green-400 mr-2">✓</span> Approve & Commit
133+				</button>
134+			</div>
135+		{/if}
136+	</div>
137+</header>
A src/lib/index.ts
+1, -0
1@@ -0,0 +1 @@
2+// place files you want to import through the `$lib` alias in this folder.
A src/lib/server/export.ts
+227, -0
  1@@ -0,0 +1,227 @@
  2+import { writeFile } from 'fs/promises';
  3+import { join } from 'path';
  4+import { spawn } from 'child_process';
  5+import type { ReviewExport, ReviewExportFile, ReviewExportThread } from '$lib/types/index.ts';
  6+import { getRepoDir, getRepoInfo, getStatus, getFileContent } from './git.ts';
  7+import { getReview, getThreads } from './review-store.ts';
  8+
  9+export async function buildExport(): Promise<ReviewExport> {
 10+	const { branch, repoName } = await getRepoInfo();
 11+	const review = getReview();
 12+	const changedFiles = await getStatus();
 13+
 14+	const files: ReviewExportFile[] = [];
 15+
 16+	for (const file of changedFiles) {
 17+		const fileThreads = getThreads(file.path);
 18+		if (fileThreads.length === 0) continue;
 19+
 20+		let content = '';
 21+		try {
 22+			content = await getFileContent(file.path);
 23+		} catch {
 24+			// file might be new
 25+		}
 26+		const contentLines = content.split('\n');
 27+
 28+		const threads: ReviewExportThread[] = fileThreads.map((t) => {
 29+			const start = Math.max(0, t.lineNumber - 3);
 30+			const end = Math.min(contentLines.length, t.lineNumber + 2);
 31+			const codeContext = contentLines.slice(start, end).join('\n');
 32+
 33+			return {
 34+				lineNumber: t.lineNumber,
 35+				side: t.side,
 36+				codeContext,
 37+				comments: t.comments.map((c) => ({ body: c.body, type: c.type }))
 38+			};
 39+		});
 40+
 41+		files.push({
 42+			path: file.path,
 43+			status: file.status,
 44+			threads
 45+		});
 46+	}
 47+
 48+	return {
 49+		version: '1.0',
 50+		repository: repoName,
 51+		branch,
 52+		exportedAt: new Date().toISOString(),
 53+		review: {
 54+			status: review.status,
 55+			summary: review.summary,
 56+			files
 57+		}
 58+	};
 59+}
 60+
 61+export async function exportJSON(): Promise<string> {
 62+	const data = await buildExport();
 63+	const filePath = join(getRepoDir(), '.lcr-review.json');
 64+	await writeFile(filePath, JSON.stringify(data, null, 2));
 65+	return filePath;
 66+}
 67+
 68+export async function exportMarkdown(): Promise<string> {
 69+	const data = await buildExport();
 70+	const lines: string[] = [
 71+		`# Code Review: ${data.repository}`,
 72+		``,
 73+		`**Branch:** ${data.branch}`,
 74+		`**Status:** ${data.review.status}`,
 75+		`**Date:** ${data.exportedAt}`,
 76+		``
 77+	];
 78+
 79+	if (data.review.summary) {
 80+		lines.push(`## Summary`, ``, data.review.summary, ``);
 81+	}
 82+
 83+	for (const file of data.review.files) {
 84+		lines.push(`## ${file.path} (${file.status})`, ``);
 85+		for (const thread of file.threads) {
 86+			lines.push(`### Line ${thread.lineNumber} (${thread.side})`, ``);
 87+			if (thread.codeContext) {
 88+				lines.push('```', thread.codeContext, '```', ``);
 89+			}
 90+			for (const comment of thread.comments) {
 91+				const prefix = comment.type === 'suggestion' ? '💡' : comment.type === 'question' ? '❓' : '💬';
 92+				lines.push(`${prefix} **${comment.type}:** ${comment.body}`, ``);
 93+			}
 94+		}
 95+	}
 96+
 97+	const filePath = join(getRepoDir(), '.lcr-review.md');
 98+	await writeFile(filePath, lines.join('\n'));
 99+	return filePath;
100+}
101+
102+import { statSync } from 'fs';
103+import { execSync } from 'child_process';
104+
105+export function findClaude(): string {
106+	const paths = [
107+		join(process.env.HOME || '', '.local', 'bin', 'claude'),
108+		'/usr/local/bin/claude',
109+		'/opt/homebrew/bin/claude'
110+	];
111+	for (const p of paths) {
112+		try {
113+			statSync(p);
114+			return p;
115+		} catch {}
116+	}
117+	try {
118+		return execSync('which claude', { encoding: 'utf-8' }).trim();
119+	} catch {}
120+	return 'claude';
121+}
122+
123+export async function exportToClaude(): Promise<{ success: boolean; message: string }> {
124+	const data = await buildExport();
125+
126+	if (data.review.files.length === 0) {
127+		return { success: false, message: 'No review comments to send. Add comments first.' };
128+	}
129+
130+	const prompt = buildClaudePrompt(data);
131+	const claudePath = findClaude();
132+
133+	return new Promise((resolve) => {
134+		const child = spawn(claudePath, ['-p'], {
135+			cwd: getRepoDir(),
136+			stdio: ['pipe', 'pipe', 'pipe'],
137+			env: {
138+				...process.env,
139+				PATH: `${join(process.env.HOME || '', '.local', 'bin')}:/usr/local/bin:/opt/homebrew/bin:${process.env.PATH || ''}`
140+			}
141+		});
142+
143+		child.stdin.write(prompt);
144+		child.stdin.end();
145+
146+		let stdout = '';
147+		let stderr = '';
148+
149+		child.stdout.on('data', (chunk) => { stdout += chunk.toString(); });
150+		child.stderr.on('data', (chunk) => { stderr += chunk.toString(); });
151+
152+		child.on('close', (code) => {
153+			if (code === 0) {
154+				resolve({ success: true, message: stdout || 'Review sent to Claude Code successfully.' });
155+			} else {
156+				resolve({
157+					success: false,
158+					message: stderr || stdout || `Claude Code exited with code ${code}`
159+				});
160+			}
161+		});
162+
163+		child.on('error', (err) => {
164+			resolve({
165+				success: false,
166+				message: `Failed to launch Claude Code: ${err.message}. Make sure 'claude' CLI is installed.`
167+			});
168+		});
169+	});
170+}
171+
172+export function buildClaudePrompt(data: ReviewExport): string {
173+	const lines: string[] = [
174+		'I have completed a code review of the current changes. Please address each review comment below by making the necessary changes to the codebase.',
175+		''
176+	];
177+
178+	if (data.review.summary) {
179+		lines.push(`Overall review summary: ${data.review.summary}`, '');
180+	}
181+
182+	for (const file of data.review.files) {
183+		lines.push(`File: ${file.path} (${file.status})`);
184+		for (const thread of file.threads) {
185+			lines.push(`  Line ${thread.lineNumber}:`);
186+			if (thread.codeContext) {
187+				lines.push(`  Context:`, `  \`\`\``, `  ${thread.codeContext}`, `  \`\`\``);
188+			}
189+			for (const comment of thread.comments) {
190+				lines.push(`  [${comment.type.toUpperCase()}]: ${comment.body}`);
191+			}
192+			lines.push('');
193+		}
194+	}
195+
196+	return lines.join('\n');
197+}
198+
199+export function getExportPrompt(): string {
200+	// For clipboard copy — same as Claude prompt but returned as string
201+	const review = getReview();
202+	if (review.threads.length === 0) return 'No review comments.';
203+
204+	const lines: string[] = [
205+		'Code review comments:',
206+		''
207+	];
208+
209+	const fileGroups = new Map<string, typeof review.threads>();
210+	for (const thread of review.threads) {
211+		const existing = fileGroups.get(thread.filePath) || [];
212+		existing.push(thread);
213+		fileGroups.set(thread.filePath, existing);
214+	}
215+
216+	for (const [path, threads] of fileGroups) {
217+		lines.push(`File: ${path}`);
218+		for (const thread of threads) {
219+			lines.push(`  Line ${thread.lineNumber} (${thread.side}):`);
220+			for (const comment of thread.comments) {
221+				lines.push(`    [${comment.type}]: ${comment.body}`);
222+			}
223+		}
224+		lines.push('');
225+	}
226+
227+	return lines.join('\n');
228+}
A src/lib/server/git.ts
+414, -0
  1@@ -0,0 +1,414 @@
  2+import simpleGit, { type SimpleGit } from 'simple-git';
  3+import type { FileChange, DiffFile, DiffHunk, DiffLine } from '$lib/types/index.ts';
  4+
  5+let git: SimpleGit;
  6+
  7+export function getGit(): SimpleGit {
  8+	if (!git) {
  9+		const repoDir = process.env.LCR_REPO_DIR || process.cwd();
 10+		git = simpleGit(repoDir);
 11+	}
 12+	return git;
 13+}
 14+
 15+export function getRepoDir(): string {
 16+	return process.env.LCR_REPO_DIR || process.cwd();
 17+}
 18+
 19+export async function getStatus(): Promise<FileChange[]> {
 20+	const g = getGit();
 21+	const status = await g.status();
 22+	const files: FileChange[] = [];
 23+	const seen = new Set<string>();
 24+
 25+	// Helper to add a file only once
 26+	function addFile(path: string, fileStatus: FileChange['status'], staged: boolean, oldPath?: string) {
 27+		if (seen.has(path)) return;
 28+		seen.add(path);
 29+		files.push({ path, oldPath, status: fileStatus, staged, approved: staged, additions: 0, deletions: 0 });
 30+	}
 31+
 32+	// Staged new files
 33+	for (const f of status.created) {
 34+		addFile(f, 'added', true);
 35+	}
 36+	// Untracked files (not staged)
 37+	for (const f of status.not_added) {
 38+		addFile(f, 'added', false);
 39+	}
 40+	for (const f of status.modified) {
 41+		addFile(f, 'modified', false);
 42+	}
 43+	for (const f of status.deleted) {
 44+		addFile(f, 'deleted', false);
 45+	}
 46+	for (const f of status.renamed) {
 47+		addFile(f.to, 'renamed', false, f.from);
 48+	}
 49+	// Mark staged files
 50+	for (const f of status.staged) {
 51+		const existing = files.find((x) => x.path === f);
 52+		if (existing) {
 53+			existing.staged = true;
 54+		}
 55+	}
 56+
 57+	// Get numstat for additions/deletions
 58+	try {
 59+		const hasCommits = await hasHead();
 60+		if (hasCommits) {
 61+			const diffStat = await g.diff(['HEAD', '--numstat']);
 62+			for (const line of diffStat.split('\n')) {
 63+				const match = line.match(/^(\d+|-)\t(\d+|-)\t(.+)$/);
 64+				if (match) {
 65+					const [, adds, dels, path] = match;
 66+					const file = files.find((f) => f.path === path);
 67+					if (file) {
 68+						file.additions = adds === '-' ? 0 : parseInt(adds, 10);
 69+						file.deletions = dels === '-' ? 0 : parseInt(dels, 10);
 70+					}
 71+				}
 72+			}
 73+		}
 74+	} catch {
 75+		// numstat may fail for initial commits
 76+	}
 77+
 78+	return files;
 79+}
 80+
 81+async function hasHead(): Promise<boolean> {
 82+	const g = getGit();
 83+	try {
 84+		await g.revparse(['HEAD']);
 85+		return true;
 86+	} catch {
 87+		return false;
 88+	}
 89+}
 90+
 91+export async function getDiff(filePath?: string): Promise<string> {
 92+	const g = getGit();
 93+	const hasCommits = await hasHead();
 94+	let result = '';
 95+
 96+	if (hasCommits) {
 97+		// Show all changes (staged + unstaged) relative to HEAD
 98+		const args = ['HEAD'];
 99+		if (filePath) args.push('--', filePath);
100+		try {
101+			result = await g.diff(args);
102+		} catch {
103+			result = '';
104+		}
105+	}
106+
107+	// Also get untracked files as diffs
108+	const status = await g.status();
109+	const untrackedFiles = filePath
110+		? status.not_added.filter((f) => f === filePath)
111+		: status.not_added;
112+
113+	for (const f of untrackedFiles) {
114+		try {
115+			const { readFile } = await import('fs/promises');
116+			const { join } = await import('path');
117+			const content = await readFile(join(getRepoDir(), f), 'utf-8');
118+			const lines = content.split('\n');
119+			// Build a synthetic diff for untracked files
120+			const header = `diff --git a/${f} b/${f}\nnew file mode 100644\n--- /dev/null\n+++ b/${f}\n@@ -0,0 +1,${lines.length} @@\n`;
121+			const body = lines.map((l) => '+' + l).join('\n');
122+			result += '\n' + header + body + '\n';
123+		} catch {
124+			// Skip files that can't be read (binary, etc.)
125+		}
126+	}
127+
128+	// For repos with no commits, also get staged files
129+	if (!hasCommits) {
130+		const stagedFiles = filePath
131+			? status.created.filter((f) => f === filePath)
132+			: status.created;
133+
134+		for (const f of stagedFiles) {
135+			try {
136+				const { readFile } = await import('fs/promises');
137+				const { join } = await import('path');
138+				const content = await readFile(join(getRepoDir(), f), 'utf-8');
139+				const lines = content.split('\n');
140+				const header = `diff --git a/${f} b/${f}\nnew file mode 100644\n--- /dev/null\n+++ b/${f}\n@@ -0,0 +1,${lines.length} @@\n`;
141+				const body = lines.map((l) => '+' + l).join('\n');
142+				result += '\n' + header + body + '\n';
143+			} catch {
144+				// Skip files that can't be read
145+			}
146+		}
147+	}
148+
149+	return result;
150+}
151+
152+export function parseDiff(raw: string): DiffFile[] {
153+	if (!raw.trim()) return [];
154+
155+	const files: DiffFile[] = [];
156+	const fileChunks = raw.split(/^diff --git /m).filter(Boolean);
157+
158+	for (const chunk of fileChunks) {
159+		const lines = chunk.split('\n');
160+		let path = '';
161+		let oldPath: string | undefined;
162+
163+		// Parse file header
164+		const headerMatch = lines[0]?.match(/a\/(.+?) b\/(.+)/);
165+		if (headerMatch) {
166+			oldPath = headerMatch[1];
167+			path = headerMatch[2];
168+			if (oldPath === path) oldPath = undefined;
169+		}
170+
171+		const language = getLanguage(path);
172+		const hunks: DiffHunk[] = [];
173+		let currentHunk: DiffHunk | null = null;
174+		let oldLineNum = 0;
175+		let newLineNum = 0;
176+
177+		for (const line of lines) {
178+			const hunkMatch = line.match(/^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@(.*)/);
179+			if (hunkMatch) {
180+				const oldStart = parseInt(hunkMatch[1], 10);
181+				const oldLines = hunkMatch[2] ? parseInt(hunkMatch[2], 10) : 1;
182+				const newStart = parseInt(hunkMatch[3], 10);
183+				const newLines = hunkMatch[4] ? parseInt(hunkMatch[4], 10) : 1;
184+				currentHunk = {
185+					header: line,
186+					oldStart,
187+					oldLines,
188+					newStart,
189+					newLines,
190+					lines: []
191+				};
192+				hunks.push(currentHunk);
193+				oldLineNum = oldStart;
194+				newLineNum = newStart;
195+				continue;
196+			}
197+
198+			if (!currentHunk) continue;
199+
200+			if (line.startsWith('+')) {
201+				currentHunk.lines.push({
202+					type: 'add',
203+					content: line.substring(1),
204+					newNumber: newLineNum++
205+				});
206+			} else if (line.startsWith('-')) {
207+				currentHunk.lines.push({
208+					type: 'del',
209+					content: line.substring(1),
210+					oldNumber: oldLineNum++
211+				});
212+			} else if (line.startsWith(' ') || line === '') {
213+				// Only treat as context if we're inside a hunk and it looks like a context line
214+				if (line.startsWith(' ')) {
215+					currentHunk.lines.push({
216+						type: 'context',
217+						content: line.substring(1),
218+						oldNumber: oldLineNum++,
219+						newNumber: newLineNum++
220+					});
221+				}
222+			} else if (line.startsWith('\\')) {
223+				// "No newline at end of file" marker — skip
224+			}
225+		}
226+
227+		if (path) {
228+			files.push({ path, oldPath, hunks, language });
229+		}
230+	}
231+
232+	return files;
233+}
234+
235+function getLanguage(filePath: string): string {
236+	const ext = filePath.split('.').pop()?.toLowerCase() || '';
237+	const map: Record<string, string> = {
238+		ts: 'typescript',
239+		tsx: 'tsx',
240+		js: 'javascript',
241+		jsx: 'jsx',
242+		py: 'python',
243+		rb: 'ruby',
244+		go: 'go',
245+		rs: 'rust',
246+		java: 'java',
247+		kt: 'kotlin',
248+		kts: 'kotlin',
249+		swift: 'swift',
250+		cs: 'csharp',
251+		fs: 'fsharp',
252+		php: 'php',
253+		r: 'r',
254+		lua: 'lua',
255+		dart: 'dart',
256+		scala: 'scala',
257+		groovy: 'groovy',
258+		pl: 'perl',
259+		pm: 'perl',
260+		ex: 'elixir',
261+		exs: 'elixir',
262+		erl: 'erlang',
263+		hs: 'haskell',
264+		clj: 'clojure',
265+		css: 'css',
266+		scss: 'scss',
267+		less: 'less',
268+		html: 'html',
269+		htm: 'html',
270+		xml: 'xml',
271+		svg: 'xml',
272+		svelte: 'svelte',
273+		vue: 'vue',
274+		json: 'json',
275+		jsonc: 'jsonc',
276+		yaml: 'yaml',
277+		yml: 'yaml',
278+		toml: 'toml',
279+		ini: 'ini',
280+		md: 'markdown',
281+		mdx: 'mdx',
282+		sh: 'bash',
283+		bash: 'bash',
284+		zsh: 'bash',
285+		fish: 'fish',
286+		ps1: 'powershell',
287+		sql: 'sql',
288+		graphql: 'graphql',
289+		gql: 'graphql',
290+		proto: 'proto',
291+		c: 'c',
292+		cpp: 'cpp',
293+		cc: 'cpp',
294+		cxx: 'cpp',
295+		h: 'c',
296+		hpp: 'cpp',
297+		m: 'objective-c',
298+		mm: 'objective-cpp',
299+		zig: 'zig',
300+		nim: 'nim',
301+		tf: 'hcl',
302+		dockerfile: 'dockerfile',
303+		makefile: 'makefile',
304+		cmake: 'cmake'
305+	};
306+	// Also check filename (no extension)
307+	const filename = filePath.split('/').pop()?.toLowerCase() || '';
308+	const filenameMap: Record<string, string> = {
309+		dockerfile: 'dockerfile',
310+		makefile: 'makefile',
311+		gemfile: 'ruby',
312+		rakefile: 'ruby',
313+		podfile: 'ruby'
314+	};
315+	return map[ext] || filenameMap[filename] || 'text';
316+}
317+
318+export async function getFileContent(filePath: string): Promise<string> {
319+	const g = getGit();
320+	try {
321+		return await g.show(['HEAD:' + filePath]);
322+	} catch {
323+		return '';
324+	}
325+}
326+
327+export async function stageFile(filePath: string): Promise<void> {
328+	const g = getGit();
329+	await g.add(filePath);
330+}
331+
332+export async function unstageFile(filePath: string): Promise<void> {
333+	const g = getGit();
334+	const hasCommits = await hasHead();
335+	if (hasCommits) {
336+		await g.raw(['reset', 'HEAD', '--', filePath]);
337+	} else {
338+		await g.raw(['rm', '--cached', filePath]);
339+	}
340+}
341+
342+export async function stageAll(): Promise<void> {
343+	const g = getGit();
344+	await g.add('-A');
345+}
346+
347+export async function commitStaged(message: string): Promise<string> {
348+	const g = getGit();
349+	const result = await g.commit(message);
350+	return result.commit;
351+}
352+
353+export async function commitAll(message: string): Promise<string> {
354+	const g = getGit();
355+	await g.add('-A');
356+	const result = await g.commit(message);
357+	return result.commit;
358+}
359+
360+export async function commit(message: string): Promise<string> {
361+	const g = getGit();
362+	const result = await g.commit(message);
363+	return result.commit;
364+}
365+
366+export type DiffMode = 'full' | 'unstaged';
367+
368+export async function getDiffByMode(mode: DiffMode, filePath?: string): Promise<string> {
369+	if (mode === 'unstaged') {
370+		return getUnstagedDiff(filePath);
371+	}
372+	return getDiff(filePath);
373+}
374+
375+async function getUnstagedDiff(filePath?: string): Promise<string> {
376+	const g = getGit();
377+	// git diff (no args) shows working tree vs index (unstaged changes only)
378+	const args: string[] = [];
379+	if (filePath) args.push('--', filePath);
380+	try {
381+		let result = await g.diff(args);
382+
383+		// Also include untracked files (they are always "unstaged")
384+		const status = await g.status();
385+		const untrackedFiles = filePath
386+			? status.not_added.filter((f) => f === filePath)
387+			: status.not_added;
388+
389+		for (const f of untrackedFiles) {
390+			try {
391+				const { readFile } = await import('fs/promises');
392+				const { join } = await import('path');
393+				const content = await readFile(join(getRepoDir(), f), 'utf-8');
394+				const lines = content.split('\n');
395+				const header = `diff --git a/${f} b/${f}\nnew file mode 100644\n--- /dev/null\n+++ b/${f}\n@@ -0,0 +1,${lines.length} @@\n`;
396+				const body = lines.map((l) => '+' + l).join('\n');
397+				result += '\n' + header + body + '\n';
398+			} catch {
399+				// Skip binary files
400+			}
401+		}
402+
403+		return result;
404+	} catch {
405+		return '';
406+	}
407+}
408+
409+export async function getRepoInfo(): Promise<{ branch: string; repoName: string }> {
410+	const g = getGit();
411+	const branch = await g.revparse(['--abbrev-ref', 'HEAD']);
412+	const repoDir = getRepoDir();
413+	const repoName = repoDir.split('/').pop() || 'unknown';
414+	return { branch: branch.trim(), repoName };
415+}
A src/lib/server/highlighter.ts
+74, -0
 1@@ -0,0 +1,74 @@
 2+import { createHighlighter, type Highlighter, type BundledLanguage } from 'shiki';
 3+
 4+let highlighter: Highlighter | null = null;
 5+
 6+async function getHighlighter(): Promise<Highlighter> {
 7+	if (!highlighter) {
 8+		highlighter = await createHighlighter({
 9+			themes: ['github-dark'],
10+			langs: [] // Start empty, load on demand
11+		});
12+	}
13+	return highlighter;
14+}
15+
16+/**
17+ * Highlight a full file and return a map of line number → HTML string.
18+ * Line numbers are 1-indexed.
19+ */
20+export async function highlightLines(
21+	code: string,
22+	language: string
23+): Promise<Map<number, string>> {
24+	const lineMap = new Map<number, string>();
25+
26+	if (language === 'text' || !code.trim()) {
27+		return lineMap;
28+	}
29+
30+	try {
31+		const hl = await getHighlighter();
32+
33+		// Load language on demand if not already loaded
34+		const loadedLangs = hl.getLoadedLanguages();
35+		if (!loadedLangs.includes(language as BundledLanguage)) {
36+			try {
37+				await hl.loadLanguage(language as BundledLanguage);
38+			} catch {
39+				// Language not supported by Shiki — return unhighlighted
40+				return lineMap;
41+			}
42+		}
43+
44+		const result = hl.codeToTokens(code, {
45+			lang: language as BundledLanguage,
46+			theme: 'github-dark'
47+		});
48+
49+		for (let i = 0; i < result.tokens.length; i++) {
50+			const lineTokens = result.tokens[i];
51+			let html = '';
52+			for (const token of lineTokens) {
53+				const escaped = escapeHtml(token.content);
54+				if (token.color) {
55+					html += `<span style="color:${token.color}">${escaped}</span>`;
56+				} else {
57+					html += escaped;
58+				}
59+			}
60+			lineMap.set(i + 1, html);
61+		}
62+	} catch {
63+		// Fallback: no highlighting
64+	}
65+
66+	return lineMap;
67+}
68+
69+function escapeHtml(text: string): string {
70+	return text
71+		.replace(/&/g, '&amp;')
72+		.replace(/</g, '&lt;')
73+		.replace(/>/g, '&gt;')
74+		.replace(/"/g, '&quot;');
75+}
A src/lib/server/pty-session.ts
+128, -0
  1@@ -0,0 +1,128 @@
  2+import { join } from 'path';
  3+import { EventEmitter } from 'events';
  4+import { findClaude } from './export.ts';
  5+import { getRepoDir } from './git.ts';
  6+
  7+// node-pty is a native module — dynamic import to avoid bundling issues
  8+let pty: typeof import('node-pty') | null = null;
  9+async function getPty() {
 10+	if (!pty) {
 11+		pty = await import('node-pty');
 12+	}
 13+	return pty;
 14+}
 15+
 16+type IPty = import('node-pty').IPty;
 17+
 18+const emitter = new EventEmitter();
 19+emitter.setMaxListeners(50);
 20+
 21+let ptyProcess: IPty | null = null;
 22+let outputBuffer: string[] = [];
 23+const MAX_BUFFER = 50000; // characters to keep for reconnection
 24+
 25+export async function startSession(cols?: number, rows?: number): Promise<void> {
 26+	if (ptyProcess) return; // already running
 27+
 28+	try {
 29+		const nodePty = await getPty();
 30+		const claudePath = findClaude();
 31+		const repoDir = getRepoDir();
 32+		const shell = process.env.SHELL || '/bin/zsh';
 33+		const homeBin = join(process.env.HOME || '', '.local', 'bin');
 34+
 35+		// Filter out undefined env values — node-pty/posix_spawnp requires all strings
 36+		const cleanEnv: Record<string, string> = {};
 37+		for (const [k, v] of Object.entries(process.env)) {
 38+			if (v !== undefined) cleanEnv[k] = v;
 39+		}
 40+		cleanEnv.PATH = `${homeBin}:/usr/local/bin:/opt/homebrew/bin:${cleanEnv.PATH || ''}`;
 41+		cleanEnv.TERM = 'xterm-256color';
 42+
 43+		ptyProcess = nodePty.spawn(shell, ['-l'], {
 44+			name: 'xterm-256color',
 45+			cols: cols || 80,
 46+			rows: rows || 24,
 47+			cwd: repoDir,
 48+			env: cleanEnv
 49+		});
 50+
 51+		ptyProcess.onData((data) => {
 52+			outputBuffer.push(data);
 53+			while (outputBuffer.join('').length > MAX_BUFFER) {
 54+				outputBuffer.shift();
 55+			}
 56+			emitter.emit('data', data);
 57+		});
 58+
 59+		ptyProcess.onExit(({ exitCode }) => {
 60+			ptyProcess = null;
 61+			emitter.emit('exit', exitCode);
 62+		});
 63+
 64+		// Give shell a moment to init, then start claude
 65+		setTimeout(() => {
 66+			if (ptyProcess) {
 67+				ptyProcess.write(`${claudePath}\r`);
 68+			}
 69+		}, 500);
 70+	} catch (err) {
 71+		console.error('Failed to start PTY session:', err);
 72+		emitter.emit('data', `\r\n\x1b[31mFailed to start terminal: ${err}\x1b[0m\r\n`);
 73+	}
 74+}
 75+
 76+export function sendInput(data: string): void {
 77+	if (ptyProcess) {
 78+		ptyProcess.write(data);
 79+	}
 80+}
 81+
 82+export function sendReviewPrompt(prompt: string): void {
 83+	if (!ptyProcess) return;
 84+
 85+	// Write the prompt text, then send Enter after a short delay
 86+	// to ensure the terminal has processed all the text
 87+	ptyProcess.write(prompt);
 88+	setTimeout(() => {
 89+		if (ptyProcess) {
 90+			ptyProcess.write('\r');
 91+		}
 92+	}, 200);
 93+}
 94+
 95+export function resize(cols: number, rows: number): void {
 96+	if (ptyProcess) {
 97+		ptyProcess.resize(cols, rows);
 98+	}
 99+}
100+
101+export function killSession(): void {
102+	if (ptyProcess) {
103+		ptyProcess.kill();
104+		ptyProcess = null;
105+	}
106+	outputBuffer = [];
107+}
108+
109+export function isAlive(): boolean {
110+	return ptyProcess !== null;
111+}
112+
113+export function getBufferedOutput(): string {
114+	return outputBuffer.join('');
115+}
116+
117+export function onData(listener: (data: string) => void): () => void {
118+	emitter.on('data', listener);
119+	return () => emitter.off('data', listener);
120+}
121+
122+export function onExit(listener: (code: number) => void): () => void {
123+	emitter.on('exit', listener);
124+	return () => emitter.off('exit', listener);
125+}
126+
127+// Cleanup on process exit
128+process.on('SIGTERM', () => killSession());
129+process.on('SIGINT', () => killSession());
A src/lib/server/review-store.ts
+121, -0
  1@@ -0,0 +1,121 @@
  2+import type { ReviewThread, ReviewComment, Review } from '$lib/types/index.ts';
  3+import { randomUUID } from 'crypto';
  4+
  5+const threads = new Map<string, ReviewThread>();
  6+let reviewStatus: 'pending' | 'approved' | 'changes_requested' = 'pending';
  7+let reviewSummary: string | undefined;
  8+
  9+export function addComment(
 10+	filePath: string,
 11+	lineNumber: number,
 12+	side: 'old' | 'new',
 13+	body: string,
 14+	type: 'comment' | 'suggestion' | 'question' = 'comment',
 15+	threadId?: string,
 16+	originalLineContent?: string
 17+): ReviewThread {
 18+	const comment: ReviewComment = {
 19+		id: randomUUID(),
 20+		filePath,
 21+		lineNumber,
 22+		side,
 23+		body,
 24+		type,
 25+		createdAt: new Date().toISOString()
 26+	};
 27+
 28+	if (threadId && threads.has(threadId)) {
 29+		const thread = threads.get(threadId)!;
 30+		thread.comments.push(comment);
 31+		return thread;
 32+	}
 33+
 34+	const thread: ReviewThread = {
 35+		id: randomUUID(),
 36+		filePath,
 37+		lineNumber,
 38+		side,
 39+		comments: [comment],
 40+		resolved: false,
 41+		outdated: false,
 42+		originalLineContent
 43+	};
 44+	threads.set(thread.id, thread);
 45+	return thread;
 46+}
 47+
 48+export function getThreads(filePath?: string): ReviewThread[] {
 49+	const all = Array.from(threads.values());
 50+	if (filePath) return all.filter((t) => t.filePath === filePath);
 51+	return all;
 52+}
 53+
 54+export function getThread(threadId: string): ReviewThread | undefined {
 55+	return threads.get(threadId);
 56+}
 57+
 58+export function resolveThread(threadId: string): boolean {
 59+	const thread = threads.get(threadId);
 60+	if (!thread) return false;
 61+	thread.resolved = !thread.resolved;
 62+	return true;
 63+}
 64+
 65+export function deleteComment(commentId: string): boolean {
 66+	for (const [threadId, thread] of threads) {
 67+		const idx = thread.comments.findIndex((c) => c.id === commentId);
 68+		if (idx !== -1) {
 69+			thread.comments.splice(idx, 1);
 70+			if (thread.comments.length === 0) {
 71+				threads.delete(threadId);
 72+			}
 73+			return true;
 74+		}
 75+	}
 76+	return false;
 77+}
 78+
 79+export function deleteThread(threadId: string): boolean {
 80+	return threads.delete(threadId);
 81+}
 82+
 83+export function getReview(): Review {
 84+	return {
 85+		id: 'current',
 86+		threads: Array.from(threads.values()),
 87+		status: reviewStatus,
 88+		createdAt: new Date().toISOString(),
 89+		summary: reviewSummary
 90+	};
 91+}
 92+
 93+export function setReviewStatus(status: 'pending' | 'approved' | 'changes_requested'): void {
 94+	reviewStatus = status;
 95+}
 96+
 97+export function setReviewSummary(summary: string): void {
 98+	reviewSummary = summary;
 99+}
100+
101+export function clearAll(): void {
102+	threads.clear();
103+	reviewStatus = 'pending';
104+	reviewSummary = undefined;
105+}
106+
107+export function markOutdated(threadId: string, outdated: boolean): boolean {
108+	const thread = threads.get(threadId);
109+	if (!thread) return false;
110+	thread.outdated = outdated;
111+	return true;
112+}
113+
114+export function getStats(): { totalThreads: number; totalComments: number; filesWithComments: number } {
115+	const all = Array.from(threads.values());
116+	const files = new Set(all.map((t) => t.filePath));
117+	return {
118+		totalThreads: all.length,
119+		totalComments: all.reduce((sum, t) => sum + t.comments.length, 0),
120+		filesWithComments: files.size
121+	};
122+}
A src/lib/server/vite-ws-plugin.ts
+36, -0
 1@@ -0,0 +1,36 @@
 2+import type { Plugin } from 'vite';
 3+import { createServer } from 'http';
 4+
 5+export function terminalWebSocket(): Plugin {
 6+	let wsServer: ReturnType<typeof createServer> | null = null;
 7+
 8+	return {
 9+		name: 'terminal-websocket',
10+		configureServer(server) {
11+			server.httpServer?.once('listening', () => {
12+				const addr = server.httpServer?.address();
13+				const vitePort = typeof addr === 'object' && addr ? addr.port : 5173;
14+				const wsPort = parseInt(process.env.VITE_LCR_WS_PORT || String(vitePort + 1000), 10);
15+
16+				wsServer = createServer();
17+
18+				import('./ws-handler.ts').then(({ attachWebSocket }) => {
19+					attachWebSocket(wsServer!, vitePort);
20+					wsServer!.listen(wsPort, '127.0.0.1', () => {
21+						console.log(`  Terminal WebSocket on ws://localhost:${wsPort}/ws/terminal`);
22+					});
23+					wsServer!.on('error', (err: any) => {
24+						if (err.code === 'EADDRINUSE') {
25+							console.error(`  Terminal WebSocket port ${wsPort} in use. Set LCR_WS_PORT to a different port.`);
26+						}
27+					});
28+				}).catch((err) => {
29+					console.error('Failed to attach terminal WebSocket:', err);
30+				});
31+			});
32+		},
33+		closeBundle() {
34+			wsServer?.close();
35+		}
36+	};
37+}
A src/lib/server/ws-handler.ts
+132, -0
  1@@ -0,0 +1,132 @@
  2+import type { Server as HttpServer } from 'http';
  3+import { WebSocketServer, type WebSocket } from 'ws';
  4+import {
  5+	startSession,
  6+	sendInput,
  7+	sendReviewPrompt,
  8+	resize,
  9+	killSession,
 10+	isAlive,
 11+	getBufferedOutput,
 12+	onData,
 13+	onExit
 14+} from './pty-session.ts';
 15+
 16+export function attachWebSocket(server: HttpServer, appPort?: number): void {
 17+	const wss = new WebSocketServer({ noServer: true });
 18+
 19+	server.on('upgrade', (req, socket, head) => {
 20+		if (req.url === '/ws/terminal') {
 21+			wss.handleUpgrade(req, socket, head, (ws) => {
 22+				wss.emit('connection', ws, req);
 23+			});
 24+		}
 25+		// Don't destroy other upgrade requests — Vite HMR uses WebSocket too
 26+	});
 27+
 28+	wss.on('connection', async (ws: WebSocket) => {
 29+		// Don't start session yet — wait for first resize message with correct dimensions.
 30+		// If session is already running, send buffered output immediately.
 31+		const buffered = isAlive() ? getBufferedOutput() : null;
 32+		if (buffered) {
 33+			ws.send(JSON.stringify({ type: 'output', data: buffered }));
 34+		}
 35+
 36+		// Forward PTY output to WebSocket
 37+		const unsubData = onData((data) => {
 38+			if (ws.readyState === ws.OPEN) {
 39+				ws.send(JSON.stringify({ type: 'output', data }));
 40+			}
 41+		});
 42+
 43+		const unsubExit = onExit((code) => {
 44+			if (ws.readyState === ws.OPEN) {
 45+				ws.send(JSON.stringify({ type: 'exit', code }));
 46+			}
 47+		});
 48+
 49+		// Handle messages from client
 50+		ws.on('message', async (raw) => {
 51+			try {
 52+				const msg = JSON.parse(raw.toString());
 53+
 54+				switch (msg.type) {
 55+					case 'input':
 56+						sendInput(msg.data);
 57+						break;
 58+
 59+					case 'review': {
 60+						// Fetch the review prompt via HTTP to ensure we use the same
 61+						// module instance as the SvelteKit API routes
 62+						const port = appPort || (() => { const a = server.address(); return typeof a === 'object' && a ? a.port : 5173; })();
 63+						try {
 64+							const res = await fetch(`http://localhost:${port}/api/export`, {
 65+								method: 'POST',
 66+								headers: { 'Content-Type': 'application/json' },
 67+								body: JSON.stringify({ format: 'clipboard' })
 68+							});
 69+							const result = await res.json();
 70+							if (!result.success || !result.text || result.text === 'No review comments.') {
 71+								ws.send(JSON.stringify({
 72+									type: 'output',
 73+									data: '\r\n\x1b[33m⚠ No review comments to send. Add comments first.\x1b[0m\r\n'
 74+								}));
 75+								break;
 76+							}
 77+							if (!isAlive()) {
 78+								// Session should already be started by resize on connect.
 79+								// If somehow not, start with defaults and wait.
 80+								await startSession(80, 24);
 81+								await new Promise((r) => setTimeout(r, 2000));
 82+							}
 83+							sendReviewPrompt(result.text);
 84+						} catch (err) {
 85+							ws.send(JSON.stringify({
 86+								type: 'output',
 87+								data: `\r\n\x1b[31mFailed to fetch review: ${err}\x1b[0m\r\n`
 88+							}));
 89+						}
 90+						break;
 91+					}
 92+
 93+					case 'resize':
 94+						if (msg.cols && msg.rows) {
 95+							if (!isAlive()) {
 96+								// First resize — start session with correct dimensions
 97+								try {
 98+									await startSession(msg.cols, msg.rows);
 99+									const buf = getBufferedOutput();
100+									if (buf) ws.send(JSON.stringify({ type: 'output', data: buf }));
101+								} catch (err) {
102+									ws.send(JSON.stringify({
103+										type: 'output',
104+										data: `\r\n\x1b[31mFailed to start terminal: ${err}\x1b[0m\r\n`
105+									}));
106+								}
107+							} else {
108+								resize(msg.cols, msg.rows);
109+							}
110+						}
111+						break;
112+
113+					case 'stop':
114+						killSession();
115+						break;
116+
117+					case 'restart':
118+						killSession();
119+						// Ask client to send dimensions so we restart with correct size
120+						ws.send(JSON.stringify({ type: 'request-resize' }));
121+						break;
122+				}
123+			} catch (err) {
124+				console.error('WS message error:', err);
125+			}
126+		});
127+
128+		ws.on('close', () => {
129+			unsubData();
130+			unsubExit();
131+		});
132+	});
133+}
A src/lib/stores/files.ts
+117, -0
  1@@ -0,0 +1,117 @@
  2+import { writable, derived } from 'svelte/store';
  3+import type { FileChange, DiffFile } from '$lib/types/index.ts';
  4+import { checkOutdated } from './review.ts';
  5+
  6+export type DiffMode = 'full' | 'unstaged';
  7+
  8+export const files = writable<FileChange[]>([]);
  9+export const selectedFile = writable<string | null>(null);
 10+export const diffFiles = writable<DiffFile[]>([]);
 11+export const loading = writable(false);
 12+export const diffMode = writable<DiffMode>('full');
 13+
 14+export const selectedDiff = derived([diffFiles, selectedFile], ([$diffFiles, $selectedFile]) => {
 15+	if (!$selectedFile) return null;
 16+	return $diffFiles.find((f) => f.path === $selectedFile) || null;
 17+});
 18+
 19+export const fileCommentCounts = writable<Record<string, number>>({});
 20+
 21+export const approvedCount = derived(files, ($files) => {
 22+	const approved = $files.filter((f) => f.approved).length;
 23+	return { approved, total: $files.length };
 24+});
 25+
 26+export const diffStats = derived(files, ($files) => {
 27+	let additions = 0;
 28+	let deletions = 0;
 29+	let unapprovedAdditions = 0;
 30+	let unapprovedDeletions = 0;
 31+	for (const f of $files) {
 32+		additions += f.additions;
 33+		deletions += f.deletions;
 34+		if (!f.approved) {
 35+			unapprovedAdditions += f.additions;
 36+			unapprovedDeletions += f.deletions;
 37+		}
 38+	}
 39+	return { additions, deletions, unapprovedAdditions, unapprovedDeletions };
 40+});
 41+
 42+export const selectedFileData = derived([files, selectedFile], ([$files, $selectedFile]) => {
 43+	if (!$selectedFile) return null;
 44+	return $files.find((f) => f.path === $selectedFile) || null;
 45+});
 46+
 47+export async function loadFiles() {
 48+	loading.set(true);
 49+	try {
 50+		const res = await fetch('/api/files');
 51+		const data = await res.json();
 52+		files.set(data);
 53+
 54+		// Auto-select first file
 55+		if (data.length > 0) {
 56+			const current = await new Promise<string | null>((resolve) => {
 57+				selectedFile.subscribe((v) => resolve(v))();
 58+			});
 59+			if (!current) {
 60+				selectedFile.set(data[0].path);
 61+			}
 62+		}
 63+	} finally {
 64+		loading.set(false);
 65+	}
 66+}
 67+
 68+export async function loadAllDiffs(mode?: DiffMode) {
 69+	const currentMode = mode || await new Promise<DiffMode>((resolve) => {
 70+		diffMode.subscribe((v) => resolve(v))();
 71+	});
 72+	const res = await fetch(`/api/diff?mode=${currentMode}`);
 73+	const data = await res.json();
 74+	if (data.files) {
 75+		diffFiles.set(data.files);
 76+	}
 77+	// Check if any comment threads are outdated after refreshing diff
 78+	checkOutdated().catch(() => {});
 79+}
 80+
 81+export async function approveFile(path: string) {
 82+	const res = await fetch('/api/files', {
 83+		method: 'POST',
 84+		headers: { 'Content-Type': 'application/json' },
 85+		body: JSON.stringify({ path, action: 'approve' })
 86+	});
 87+	const data = await res.json();
 88+	if (Array.isArray(data)) {
 89+		files.set(data);
 90+		// Reload diffs since staging changed
 91+		await loadAllDiffs();
 92+	}
 93+}
 94+
 95+export async function unapproveFile(path: string) {
 96+	const res = await fetch('/api/files', {
 97+		method: 'POST',
 98+		headers: { 'Content-Type': 'application/json' },
 99+		body: JSON.stringify({ path, action: 'unapprove' })
100+	});
101+	const data = await res.json();
102+	if (Array.isArray(data)) {
103+		files.set(data);
104+		await loadAllDiffs();
105+	}
106+}
107+
108+export async function toggleApproval(path: string) {
109+	const currentFiles = await new Promise<FileChange[]>((resolve) => {
110+		files.subscribe((v) => resolve(v))();
111+	});
112+	const file = currentFiles.find((f) => f.path === path);
113+	if (file?.approved) {
114+		await unapproveFile(path);
115+	} else {
116+		await approveFile(path);
117+	}
118+}
A src/lib/stores/review.ts
+132, -0
  1@@ -0,0 +1,132 @@
  2+import { writable, derived } from 'svelte/store';
  3+import type { ReviewThread, Review } from '$lib/types/index.ts';
  4+import { fileCommentCounts } from './files.ts';
  5+
  6+export const threads = writable<ReviewThread[]>([]);
  7+export const reviewStatus = writable<Review['status']>('pending');
  8+
  9+export const threadsByFile = derived(threads, ($threads) => {
 10+	const map = new Map<string, ReviewThread[]>();
 11+	for (const t of $threads) {
 12+		const existing = map.get(t.filePath) || [];
 13+		existing.push(t);
 14+		map.set(t.filePath, existing);
 15+	}
 16+	return map;
 17+});
 18+
 19+export const stats = derived(threads, ($threads) => {
 20+	const files = new Set($threads.map((t) => t.filePath));
 21+	return {
 22+		totalThreads: $threads.length,
 23+		totalComments: $threads.reduce((sum, t) => sum + t.comments.length, 0),
 24+		filesWithComments: files.size,
 25+		unresolvedThreads: $threads.filter((t) => !t.resolved).length
 26+	};
 27+});
 28+
 29+export async function loadComments(filePath?: string) {
 30+	const params = filePath ? `?file=${encodeURIComponent(filePath)}` : '';
 31+	const res = await fetch(`/api/comments${params}`);
 32+	const data = await res.json();
 33+	if (data.threads) {
 34+		if (filePath) {
 35+			// Merge with existing threads for other files
 36+			threads.update((existing) => {
 37+				const otherFiles = existing.filter((t) => t.filePath !== filePath);
 38+				return [...otherFiles, ...data.threads];
 39+			});
 40+		} else {
 41+			threads.set(data.threads);
 42+		}
 43+	}
 44+	// Update comment counts
 45+	updateCommentCounts();
 46+}
 47+
 48+function updateCommentCounts() {
 49+	threads.subscribe(($threads) => {
 50+		const counts: Record<string, number> = {};
 51+		for (const t of $threads) {
 52+			counts[t.filePath] = (counts[t.filePath] || 0) + 1;
 53+		}
 54+		fileCommentCounts.set(counts);
 55+	})();
 56+}
 57+
 58+export async function addComment(
 59+	filePath: string,
 60+	lineNumber: number,
 61+	side: 'old' | 'new',
 62+	text: string,
 63+	type: 'comment' | 'suggestion' | 'question' = 'comment',
 64+	threadId?: string,
 65+	originalLineContent?: string
 66+) {
 67+	const res = await fetch('/api/comments', {
 68+		method: 'POST',
 69+		headers: { 'Content-Type': 'application/json' },
 70+		body: JSON.stringify({ filePath, lineNumber, side, text, type, threadId, originalLineContent })
 71+	});
 72+	const thread = await res.json();
 73+	if (thread.id) {
 74+		threads.update((existing) => {
 75+			const idx = existing.findIndex((t) => t.id === thread.id);
 76+			if (idx >= 0) {
 77+				existing[idx] = thread;
 78+				return [...existing];
 79+			}
 80+			return [...existing, thread];
 81+		});
 82+		updateCommentCounts();
 83+	}
 84+	return thread;
 85+}
 86+
 87+export async function resolveThread(threadId: string) {
 88+	await fetch('/api/comments', {
 89+		method: 'PATCH',
 90+		headers: { 'Content-Type': 'application/json' },
 91+		body: JSON.stringify({ threadId, action: 'resolve' })
 92+	});
 93+	threads.update((existing) => {
 94+		const thread = existing.find((t) => t.id === threadId);
 95+		if (thread) thread.resolved = !thread.resolved;
 96+		return [...existing];
 97+	});
 98+}
 99+
100+export async function deleteThread(threadId: string) {
101+	await fetch('/api/comments', {
102+		method: 'DELETE',
103+		headers: { 'Content-Type': 'application/json' },
104+		body: JSON.stringify({ threadId })
105+	});
106+	threads.update((existing) => existing.filter((t) => t.id !== threadId));
107+	updateCommentCounts();
108+}
109+
110+export async function checkOutdated() {
111+	const res = await fetch('/api/comments', {
112+		method: 'PATCH',
113+		headers: { 'Content-Type': 'application/json' },
114+		body: JSON.stringify({ action: 'check-outdated' })
115+	});
116+	const data = await res.json();
117+	if (data.threads) {
118+		threads.set(data.threads);
119+		updateCommentCounts();
120+	}
121+	return data.outdatedCount || 0;
122+}
123+
124+export async function submitReview(status: Review['status'], summary?: string) {
125+	const res = await fetch('/api/review', {
126+		method: 'POST',
127+		headers: { 'Content-Type': 'application/json' },
128+		body: JSON.stringify({ status, summary })
129+	});
130+	const review = await res.json();
131+	reviewStatus.set(review.status);
132+	return review;
133+}
A src/lib/stores/terminal.ts
+66, -0
 1@@ -0,0 +1,66 @@
 2+import { writable, get } from 'svelte/store';
 3+
 4+export const terminalOpen = writable(false);
 5+export const terminalHeight = writable(300);
 6+
 7+// Pending review to send once WS connects
 8+let pendingReview = false;
 9+
10+function getWsInstance(): WebSocket | null {
11+	if (typeof globalThis === 'undefined') return null;
12+	return (globalThis as any).__lcr_ws || null;
13+}
14+
15+export function getWs(): WebSocket | null {
16+	return getWsInstance();
17+}
18+
19+export function hasPendingReview(): boolean {
20+	if (pendingReview) {
21+		pendingReview = false;
22+		return true;
23+	}
24+	return false;
25+}
26+
27+export async function sendReviewToClaude(): Promise<{ success: boolean; message: string }> {
28+	const ws = getWsInstance();
29+
30+	// If WS is already connected, send immediately
31+	if (ws?.readyState === WebSocket.OPEN) {
32+		terminalOpen.set(true);
33+		ws.send(JSON.stringify({ type: 'review' }));
34+		return { success: true, message: 'Review sent.' };
35+	}
36+
37+	// Otherwise, open the terminal and queue the review
38+	// The TerminalPanel will check for pending review on WS connect
39+	pendingReview = true;
40+	terminalOpen.set(true);
41+	return { success: true, message: 'Opening terminal...' };
42+}
43+
44+export async function stopClaudeSession(): Promise<void> {
45+	const ws = getWsInstance();
46+	if (ws?.readyState === WebSocket.OPEN) {
47+		ws.send(JSON.stringify({ type: 'stop' }));
48+	}
49+}
50+
51+export async function restartSession(): Promise<void> {
52+	const ws = getWsInstance();
53+	if (ws?.readyState === WebSocket.OPEN) {
54+		ws.send(JSON.stringify({ type: 'restart' }));
55+	}
56+}
57+
58+export function toggleTerminal() {
59+	terminalOpen.update((v) => !v);
60+}
61+
62+// Persist terminal height to localStorage
63+if (typeof window !== 'undefined') {
64+	const saved = localStorage.getItem('lcr-terminal-height');
65+	if (saved) terminalHeight.set(Number(saved));
66+	terminalHeight.subscribe((h) => localStorage.setItem('lcr-terminal-height', String(h)));
67+}
A src/lib/stores/ui.ts
+14, -0
 1@@ -0,0 +1,14 @@
 2+import { writable } from 'svelte/store';
 3+
 4+export type ViewMode = 'unified' | 'split';
 5+
 6+export const viewMode = writable<ViewMode>('unified');
 7+export const sidebarOpen = writable(true);
 8+export const showCommitDialog = writable(false);
 9+export const showExportDialog = writable(false);
10+export const commentingLine = writable<{
11+	filePath: string;
12+	lineNumber: number;
13+	side: 'old' | 'new';
14+	originalLineContent?: string;
15+} | null>(null);
A src/lib/types/diff.ts
+33, -0
 1@@ -0,0 +1,33 @@
 2+export interface FileChange {
 3+	path: string;
 4+	oldPath?: string;
 5+	status: 'added' | 'modified' | 'deleted' | 'renamed';
 6+	staged: boolean;
 7+	approved: boolean;
 8+	additions: number;
 9+	deletions: number;
10+}
11+
12+export interface DiffFile {
13+	path: string;
14+	oldPath?: string;
15+	hunks: DiffHunk[];
16+	language: string;
17+}
18+
19+export interface DiffHunk {
20+	header: string;
21+	oldStart: number;
22+	oldLines: number;
23+	newStart: number;
24+	newLines: number;
25+	lines: DiffLine[];
26+}
27+
28+export interface DiffLine {
29+	type: 'add' | 'del' | 'context';
30+	content: string;
31+	html?: string;
32+	oldNumber?: number;
33+	newNumber?: number;
34+}
A src/lib/types/index.ts
+2, -0
1@@ -0,0 +1,2 @@
2+export * from './diff.ts';
3+export * from './review.ts';
A src/lib/types/review.ts
+53, -0
 1@@ -0,0 +1,53 @@
 2+export interface ReviewComment {
 3+	id: string;
 4+	filePath: string;
 5+	lineNumber: number;
 6+	side: 'old' | 'new';
 7+	body: string;
 8+	type: 'comment' | 'suggestion' | 'question';
 9+	createdAt: string;
10+}
11+
12+export interface ReviewThread {
13+	id: string;
14+	filePath: string;
15+	lineNumber: number;
16+	side: 'old' | 'new';
17+	comments: ReviewComment[];
18+	resolved: boolean;
19+	outdated: boolean;
20+	originalLineContent?: string;
21+}
22+
23+export interface Review {
24+	id: string;
25+	threads: ReviewThread[];
26+	status: 'pending' | 'approved' | 'changes_requested';
27+	createdAt: string;
28+	summary?: string;
29+}
30+
31+export interface ReviewExport {
32+	version: '1.0';
33+	repository: string;
34+	branch: string;
35+	exportedAt: string;
36+	review: {
37+		status: string;
38+		summary?: string;
39+		files: ReviewExportFile[];
40+	};
41+}
42+
43+export interface ReviewExportFile {
44+	path: string;
45+	status: string;
46+	threads: ReviewExportThread[];
47+}
48+
49+export interface ReviewExportThread {
50+	lineNumber: number;
51+	side: string;
52+	codeContext: string;
53+	comments: { body: string; type: string }[];
54+}
A src/routes/+layout.svelte
+13, -0
 1@@ -0,0 +1,13 @@
 2+<script lang="ts">
 3+	import './layout.css';
 4+
 5+	let { children } = $props();
 6+</script>
 7+
 8+<svelte:head>
 9+	<title>Local Code Review</title>
10+</svelte:head>
11+
12+<div class="h-screen w-screen overflow-hidden bg-zinc-900 text-zinc-200">
13+	{@render children()}
14+</div>
A src/routes/+page.svelte
+132, -0
  1@@ -0,0 +1,132 @@
  2+<script lang="ts">
  3+	import { onMount } from 'svelte';
  4+	import FileTree from '$lib/components/FileTree.svelte';
  5+	import DiffView from '$lib/components/DiffView.svelte';
  6+	import Toolbar from '$lib/components/Toolbar.svelte';
  7+	import CommitDialog from '$lib/components/CommitDialog.svelte';
  8+	import ExportDialog from '$lib/components/ExportDialog.svelte';
  9+	import TerminalPanel from '$lib/components/TerminalPanel.svelte';
 10+	import { files, selectedFile, selectedDiff, selectedFileData, loadFiles, loadAllDiffs, toggleApproval } from '$lib/stores/files.ts';
 11+	import { loadComments } from '$lib/stores/review.ts';
 12+	import { sidebarOpen } from '$lib/stores/ui.ts';
 13+	import { toggleTerminal } from '$lib/stores/terminal.ts';
 14+
 15+	let loading = $state(true);
 16+	let error = $state<string | null>(null);
 17+
 18+	onMount(async () => {
 19+		try {
 20+			await loadFiles();
 21+			await loadAllDiffs();
 22+			await loadComments();
 23+		} catch (err) {
 24+			error = String(err);
 25+		} finally {
 26+			loading = false;
 27+		}
 28+	});
 29+
 30+	function handleKeydown(e: KeyboardEvent) {
 31+		if (e.target instanceof HTMLTextAreaElement || e.target instanceof HTMLInputElement) return;
 32+
 33+		const fileList = $files;
 34+		const currentIdx = fileList.findIndex((f) => f.path === $selectedFile);
 35+
 36+		if (e.key === 'j' && currentIdx < fileList.length - 1) {
 37+			e.preventDefault();
 38+			$selectedFile = fileList[currentIdx + 1].path;
 39+		}
 40+		if (e.key === 'k' && currentIdx > 0) {
 41+			e.preventDefault();
 42+			$selectedFile = fileList[currentIdx - 1].path;
 43+		}
 44+		if (e.key === 'b') {
 45+			e.preventDefault();
 46+			$sidebarOpen = !$sidebarOpen;
 47+		}
 48+		if (e.key === '`') {
 49+			e.preventDefault();
 50+			toggleTerminal();
 51+		}
 52+	}
 53+</script>
 54+
 55+<svelte:window onkeydown={handleKeydown} />
 56+
 57+<div class="h-full flex flex-col">
 58+	<Toolbar />
 59+
 60+	<div class="flex-1 flex overflow-hidden">
 61+		<!-- Sidebar -->
 62+		{#if $sidebarOpen}
 63+			<aside class="w-72 shrink-0 bg-zinc-800/50 border-r border-zinc-700 overflow-hidden flex flex-col">
 64+				<FileTree />
 65+			</aside>
 66+		{/if}
 67+
 68+		<!-- Main diff area -->
 69+		<main class="flex-1 overflow-y-auto">
 70+			{#if loading}
 71+				<div class="flex items-center justify-center h-full text-zinc-500">
 72+					<div class="text-center">
 73+						<div class="text-2xl mb-2 animate-pulse">⏳</div>
 74+						<p class="text-sm">Loading changes...</p>
 75+					</div>
 76+				</div>
 77+			{:else if error}
 78+				<div class="flex items-center justify-center h-full text-red-400">
 79+					<div class="text-center max-w-md">
 80+						<div class="text-2xl mb-2">⚠️</div>
 81+						<p class="text-sm">{error}</p>
 82+					</div>
 83+				</div>
 84+			{:else if $files.length === 0}
 85+				<div class="flex items-center justify-center h-full text-zinc-500">
 86+					<div class="text-center">
 87+						<div class="text-4xl mb-3">✨</div>
 88+						<p class="font-medium text-zinc-300">No uncommitted changes</p>
 89+						<p class="text-sm mt-1">Make some changes and come back to review them</p>
 90+					</div>
 91+				</div>
 92+			{:else if $selectedDiff}
 93+				<!-- File header -->
 94+				<div class="sticky top-0 z-10 bg-zinc-900/95 backdrop-blur border-b border-zinc-700 px-4 py-2 flex items-center gap-2">
 95+					<button
 96+						class="shrink-0 w-5 h-5 rounded border flex items-center justify-center transition-colors {$selectedFileData?.approved ? 'bg-green-600 border-green-600 text-white' : 'border-zinc-500 hover:border-green-400 text-transparent hover:text-green-400'}"
 97+						onclick={() => toggleApproval($selectedDiff!.path)}
 98+						title={$selectedFileData?.approved ? 'Unapprove file' : 'Approve file'}
 99+					>
100+						<svg class="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="3">
101+							<path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7" />
102+						</svg>
103+					</button>
104+					<span class="font-mono text-sm text-zinc-200">{$selectedDiff.path}</span>
105+					<span class="text-xs text-zinc-500">({$selectedDiff.language})</span>
106+					{#if $selectedFileData}
107+						<span class="text-xs tabular-nums">
108+							{#if $selectedFileData.additions > 0}
109+								<span class="text-green-400">+{$selectedFileData.additions}</span>
110+							{/if}
111+							{#if $selectedFileData.deletions > 0}
112+								<span class="text-red-400 ml-1">-{$selectedFileData.deletions}</span>
113+							{/if}
114+						</span>
115+					{/if}
116+					{#if $selectedFileData?.approved}
117+						<span class="text-xs px-2 py-0.5 rounded-full bg-green-500/20 text-green-400">Approved</span>
118+					{/if}
119+				</div>
120+				<DiffView file={$selectedDiff} />
121+			{:else}
122+				<div class="flex items-center justify-center h-full text-zinc-500">
123+					<p class="text-sm">Select a file from the sidebar</p>
124+				</div>
125+			{/if}
126+		</main>
127+	</div>
128+
129+	<TerminalPanel />
130+</div>
131+
132+<CommitDialog />
133+<ExportDialog />
A src/routes/api/comments/+server.ts
+105, -0
  1@@ -0,0 +1,105 @@
  2+import { json } from '@sveltejs/kit';
  3+import {
  4+	addComment,
  5+	getThreads,
  6+	resolveThread,
  7+	deleteComment,
  8+	deleteThread,
  9+	getStats,
 10+	markOutdated
 11+} from '$lib/server/review-store.ts';
 12+import { getRepoDir } from '$lib/server/git.ts';
 13+import { readFile } from 'fs/promises';
 14+import { join } from 'path';
 15+import type { RequestHandler } from './$types.ts';
 16+
 17+export const GET: RequestHandler = async ({ url }) => {
 18+	const filePath = url.searchParams.get('file') || undefined;
 19+	const threads = getThreads(filePath);
 20+	const stats = getStats();
 21+	return json({ threads, stats });
 22+};
 23+
 24+export const POST: RequestHandler = async ({ request }) => {
 25+	const body = await request.json();
 26+	const { filePath, lineNumber, side, text, type, threadId, originalLineContent } = body;
 27+
 28+	if (!filePath || lineNumber === undefined || !text) {
 29+		return json({ error: 'Missing required fields' }, { status: 400 });
 30+	}
 31+
 32+	const thread = addComment(filePath, lineNumber, side || 'new', text, type || 'comment', threadId, originalLineContent);
 33+	return json(thread);
 34+};
 35+
 36+export const PATCH: RequestHandler = async ({ request }) => {
 37+	const body = await request.json();
 38+	const { threadId, action } = body;
 39+
 40+	if (action === 'resolve' && threadId) {
 41+		resolveThread(threadId);
 42+		return json({ success: true });
 43+	}
 44+
 45+	if (action === 'check-outdated') {
 46+		const threads = getThreads();
 47+		const repoDir = getRepoDir();
 48+		const fileCache = new Map<string, string[]>();
 49+		let outdatedCount = 0;
 50+
 51+		for (const thread of threads) {
 52+			if (!thread.originalLineContent || thread.resolved) continue;
 53+
 54+			// Read file content (cached)
 55+			let lines = fileCache.get(thread.filePath);
 56+			if (!lines) {
 57+				try {
 58+					const content = await readFile(join(repoDir, thread.filePath), 'utf-8');
 59+					lines = content.split('\n');
 60+					fileCache.set(thread.filePath, lines);
 61+				} catch {
 62+					continue;
 63+				}
 64+			}
 65+
 66+			// Check if the line content still matches
 67+			const lineIdx = thread.lineNumber - 1;
 68+			if (lineIdx >= 0 && lineIdx < lines.length) {
 69+				const currentContent = lines[lineIdx].trim();
 70+				const originalContent = thread.originalLineContent.trim();
 71+				const isOutdated = currentContent !== originalContent;
 72+				if (isOutdated !== thread.outdated) {
 73+					markOutdated(thread.id, isOutdated);
 74+					if (isOutdated) outdatedCount++;
 75+				}
 76+			} else {
 77+				// Line no longer exists — outdated
 78+				if (!thread.outdated) {
 79+					markOutdated(thread.id, true);
 80+					outdatedCount++;
 81+				}
 82+			}
 83+		}
 84+
 85+		return json({ success: true, outdatedCount, threads: getThreads() });
 86+	}
 87+
 88+	return json({ error: 'Unknown action' }, { status: 400 });
 89+};
 90+
 91+export const DELETE: RequestHandler = async ({ request }) => {
 92+	const body = await request.json();
 93+	const { commentId, threadId } = body;
 94+
 95+	if (threadId) {
 96+		deleteThread(threadId);
 97+		return json({ success: true });
 98+	}
 99+
100+	if (commentId) {
101+		deleteComment(commentId);
102+		return json({ success: true });
103+	}
104+
105+	return json({ error: 'Missing commentId or threadId' }, { status: 400 });
106+};
A src/routes/api/commit/+server.ts
+29, -0
 1@@ -0,0 +1,29 @@
 2+import { json } from '@sveltejs/kit';
 3+import { commitStaged, commitAll } from '$lib/server/git.ts';
 4+import { clearAll } from '$lib/server/review-store.ts';
 5+import type { RequestHandler } from './$types.ts';
 6+
 7+export const POST: RequestHandler = async ({ request }) => {
 8+	try {
 9+		const body = await request.json();
10+		const { message, mode } = body;
11+
12+		if (!message?.trim()) {
13+			return json({ error: 'Commit message is required' }, { status: 400 });
14+		}
15+
16+		let hash: string;
17+		if (mode === 'approved') {
18+			// Commit only staged (approved) files
19+			hash = await commitStaged(message.trim());
20+		} else {
21+			// Commit all changes
22+			hash = await commitAll(message.trim());
23+		}
24+
25+		clearAll();
26+		return json({ success: true, hash });
27+	} catch (err) {
28+		return json({ error: String(err) }, { status: 500 });
29+	}
30+};
A src/routes/api/context/+server.ts
+61, -0
 1@@ -0,0 +1,61 @@
 2+import { json } from '@sveltejs/kit';
 3+import { getRepoDir } from '$lib/server/git.ts';
 4+import { highlightLines } from '$lib/server/highlighter.ts';
 5+import { readFile } from 'fs/promises';
 6+import { join } from 'path';
 7+import type { RequestHandler } from './$types.ts';
 8+
 9+function getLanguage(filePath: string): string {
10+	const ext = filePath.split('.').pop()?.toLowerCase() || '';
11+	// Simplified — the full map is in git.ts but we just need the extension here
12+	const map: Record<string, string> = {
13+		ts: 'typescript', tsx: 'tsx', js: 'javascript', jsx: 'jsx',
14+		py: 'python', rb: 'ruby', go: 'go', rs: 'rust', java: 'java',
15+		swift: 'swift', kt: 'kotlin', cs: 'csharp', php: 'php',
16+		css: 'css', scss: 'scss', html: 'html', svelte: 'svelte', vue: 'vue',
17+		json: 'json', yaml: 'yaml', yml: 'yaml', md: 'markdown',
18+		sh: 'bash', sql: 'sql', c: 'c', cpp: 'cpp', h: 'c', hpp: 'cpp',
19+		m: 'objective-c', dart: 'dart', lua: 'lua', r: 'r', scala: 'scala',
20+		ex: 'elixir', hs: 'haskell', toml: 'toml', xml: 'xml'
21+	};
22+	return map[ext] || 'text';
23+}
24+
25+export const GET: RequestHandler = async ({ url }) => {
26+	try {
27+		const filePath = url.searchParams.get('file');
28+		const startLine = parseInt(url.searchParams.get('start') || '1', 10);
29+		const endLine = parseInt(url.searchParams.get('end') || '20', 10);
30+
31+		if (!filePath) {
32+			return json({ error: 'file parameter required' }, { status: 400 });
33+		}
34+
35+		const fullPath = join(getRepoDir(), filePath);
36+		const content = await readFile(fullPath, 'utf-8');
37+		const allLines = content.split('\n');
38+		const language = getLanguage(filePath);
39+
40+		// Highlight the full file
41+		const highlighted = await highlightLines(content, language);
42+
43+		// Extract requested range (1-indexed)
44+		const start = Math.max(1, startLine);
45+		const end = Math.min(allLines.length, endLine);
46+		const lines = [];
47+
48+		for (let i = start; i <= end; i++) {
49+			lines.push({
50+				type: 'context' as const,
51+				content: allLines[i - 1],
52+				html: highlighted.get(i),
53+				oldNumber: i,
54+				newNumber: i
55+			});
56+		}
57+
58+		return json({ lines, totalLines: allLines.length });
59+	} catch (err) {
60+		return json({ error: String(err) }, { status: 500 });
61+	}
62+};
A src/routes/api/diff/+server.ts
+66, -0
 1@@ -0,0 +1,66 @@
 2+import { json } from '@sveltejs/kit';
 3+import { getDiffByMode, parseDiff, getFileContent } from '$lib/server/git.ts';
 4+import type { DiffMode } from '$lib/server/git.ts';
 5+import type { DiffFile } from '$lib/types/index.ts';
 6+import { highlightLines } from '$lib/server/highlighter.ts';
 7+import { readFile } from 'fs/promises';
 8+import { join } from 'path';
 9+import { getRepoDir } from '$lib/server/git.ts';
10+import type { RequestHandler } from './$types.ts';
11+
12+async function applyHighlighting(files: DiffFile[]): Promise<void> {
13+	for (const file of files) {
14+		// Get the full new file content for accurate highlighting
15+		let newContent = '';
16+		let oldContent = '';
17+
18+		try {
19+			// Try reading the working tree version (new content)
20+			newContent = await readFile(join(getRepoDir(), file.path), 'utf-8');
21+		} catch {
22+			// File might be deleted
23+		}
24+
25+		try {
26+			// Get the HEAD version (old content)
27+			oldContent = await getFileContent(file.path);
28+		} catch {
29+			// File might be new
30+		}
31+
32+		// Highlight both versions
33+		const [newHighlighted, oldHighlighted] = await Promise.all([
34+			newContent ? highlightLines(newContent, file.language) : Promise.resolve(new Map<number, string>()),
35+			oldContent ? highlightLines(oldContent, file.language) : Promise.resolve(new Map<number, string>())
36+		]);
37+
38+		// Apply highlighted HTML to diff lines
39+		for (const hunk of file.hunks) {
40+			for (const line of hunk.lines) {
41+				if (line.type === 'add' && line.newNumber !== undefined) {
42+					line.html = newHighlighted.get(line.newNumber);
43+				} else if (line.type === 'del' && line.oldNumber !== undefined) {
44+					line.html = oldHighlighted.get(line.oldNumber);
45+				} else if (line.type === 'context' && line.newNumber !== undefined) {
46+					line.html = newHighlighted.get(line.newNumber);
47+				}
48+			}
49+		}
50+	}
51+}
52+
53+export const GET: RequestHandler = async ({ url }) => {
54+	try {
55+		const filePath = url.searchParams.get('file') || undefined;
56+		const mode = (url.searchParams.get('mode') || 'full') as DiffMode;
57+		const raw = await getDiffByMode(mode, filePath);
58+		const parsed = parseDiff(raw);
59+
60+		// Apply syntax highlighting
61+		await applyHighlighting(parsed);
62+
63+		return json({ files: parsed, raw });
64+	} catch (err) {
65+		return json({ error: String(err) }, { status: 500 });
66+	}
67+};
A src/routes/api/export/+server.ts
+33, -0
 1@@ -0,0 +1,33 @@
 2+import { json } from '@sveltejs/kit';
 3+import { exportJSON, exportMarkdown, exportToClaude, getExportPrompt } from '$lib/server/export.ts';
 4+import type { RequestHandler } from './$types.ts';
 5+
 6+export const POST: RequestHandler = async ({ request }) => {
 7+	try {
 8+		const body = await request.json();
 9+		const { format } = body;
10+
11+		switch (format) {
12+			case 'json': {
13+				const path = await exportJSON();
14+				return json({ success: true, path });
15+			}
16+			case 'markdown': {
17+				const path = await exportMarkdown();
18+				return json({ success: true, path });
19+			}
20+			case 'claude': {
21+				const result = await exportToClaude();
22+				return json(result);
23+			}
24+			case 'clipboard': {
25+				const text = getExportPrompt();
26+				return json({ success: true, text });
27+			}
28+			default:
29+				return json({ error: 'Unknown format' }, { status: 400 });
30+		}
31+	} catch (err) {
32+		return json({ error: String(err) }, { status: 500 });
33+	}
34+};
A src/routes/api/files/+server.ts
+37, -0
 1@@ -0,0 +1,37 @@
 2+import { json } from '@sveltejs/kit';
 3+import { getStatus, stageFile, unstageFile } from '$lib/server/git.ts';
 4+import type { RequestHandler } from './$types.ts';
 5+
 6+export const GET: RequestHandler = async () => {
 7+	try {
 8+		const files = await getStatus();
 9+		return json(files);
10+	} catch (err) {
11+		return json({ error: String(err) }, { status: 500 });
12+	}
13+};
14+
15+export const POST: RequestHandler = async ({ request }) => {
16+	try {
17+		const body = await request.json();
18+		const { path, action } = body;
19+
20+		if (!path || !action) {
21+			return json({ error: 'Missing path or action' }, { status: 400 });
22+		}
23+
24+		if (action === 'approve') {
25+			await stageFile(path);
26+		} else if (action === 'unapprove') {
27+			await unstageFile(path);
28+		} else {
29+			return json({ error: 'Unknown action' }, { status: 400 });
30+		}
31+
32+		// Return updated file list
33+		const files = await getStatus();
34+		return json(files);
35+	} catch (err) {
36+		return json({ error: String(err) }, { status: 500 });
37+	}
38+};
A src/routes/api/review/+server.ts
+21, -0
 1@@ -0,0 +1,21 @@
 2+import { json } from '@sveltejs/kit';
 3+import { getReview, setReviewStatus, setReviewSummary } from '$lib/server/review-store.ts';
 4+import type { RequestHandler } from './$types.ts';
 5+
 6+export const GET: RequestHandler = async () => {
 7+	return json(getReview());
 8+};
 9+
10+export const POST: RequestHandler = async ({ request }) => {
11+	const body = await request.json();
12+	const { status, summary } = body;
13+
14+	if (status) {
15+		setReviewStatus(status);
16+	}
17+	if (summary !== undefined) {
18+		setReviewSummary(summary);
19+	}
20+
21+	return json(getReview());
22+};
A src/routes/layout.css
+27, -0
 1@@ -0,0 +1,27 @@
 2+@import 'tailwindcss';
 3+
 4+html, body {
 5+	margin: 0;
 6+	padding: 0;
 7+	height: 100%;
 8+	overflow: hidden;
 9+}
10+
11+/* Scrollbar styling */
12+::-webkit-scrollbar {
13+	width: 8px;
14+	height: 8px;
15+}
16+
17+::-webkit-scrollbar-track {
18+	background: transparent;
19+}
20+
21+::-webkit-scrollbar-thumb {
22+	background: #52525b;
23+	border-radius: 4px;
24+}
25+
26+::-webkit-scrollbar-thumb:hover {
27+	background: #71717a;
28+}
A static/robots.txt
+3, -0
1@@ -0,0 +1,3 @@
2+# allow crawling everything by default
3+User-agent: *
4+Disallow:
A svelte.config.js
+19, -0
 1@@ -0,0 +1,19 @@
 2+import adapter from '@sveltejs/adapter-node';
 3+import { relative, sep } from 'node:path';
 4+
 5+/** @type {import('@sveltejs/kit').Config} */
 6+const config = {
 7+	compilerOptions: {
 8+		// defaults to rune mode for the project, execept for `node_modules`. Can be removed in svelte 6.
 9+		runes: ({ filename }) => {
10+			const relativePath = relative(import.meta.dirname, filename);
11+			const pathSegments = relativePath.toLowerCase().split(sep);
12+			const isExternalLibrary = pathSegments.includes('node_modules');
13+
14+			return isExternalLibrary ? undefined : true;
15+		}
16+	},
17+	kit: { adapter: adapter() }
18+};
19+
20+export default config;
A tsconfig.json
+20, -0
 1@@ -0,0 +1,20 @@
 2+{
 3+	"extends": "./.svelte-kit/tsconfig.json",
 4+	"compilerOptions": {
 5+		"rewriteRelativeImportExtensions": true,
 6+		"allowJs": true,
 7+		"checkJs": true,
 8+		"esModuleInterop": true,
 9+		"forceConsistentCasingInFileNames": true,
10+		"resolveJsonModule": true,
11+		"skipLibCheck": true,
12+		"sourceMap": true,
13+		"strict": true,
14+		"moduleResolution": "bundler"
15+	}
16+	// Path aliases are handled by https://svelte.dev/docs/kit/configuration#alias
17+	// except $lib which is handled by https://svelte.dev/docs/kit/configuration#files
18+	//
19+	// To make changes to top-level options such as include and exclude, we recommend extending
20+	// the generated config; see https://svelte.dev/docs/kit/configuration#typescript
21+}
A vite.config.ts
+13, -0
 1@@ -0,0 +1,13 @@
 2+import tailwindcss from '@tailwindcss/vite';
 3+import { sveltekit } from '@sveltejs/kit/vite';
 4+import { defineConfig } from 'vite';
 5+import { terminalWebSocket } from './src/lib/server/vite-ws-plugin.ts';
 6+
 7+export default defineConfig({
 8+	plugins: [tailwindcss(), sveltekit(), terminalWebSocket()],
 9+	ssr: {
10+		// node-pty is a native module, don't try to bundle it
11+		noExternal: [],
12+		external: ['node-pty']
13+	}
14+});