diff --git a/.gitignore b/.gitignore index 1b70c3d..3e528b5 100644 --- a/.gitignore +++ b/.gitignore @@ -34,3 +34,9 @@ server/tasks.json # Claude Code exports *.txt + +# Claude Code local settings +.claude/ + +# Windows artifacts +nul diff --git a/README.md b/README.md index 09c0fd0..bdcb23e 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,25 @@ A Chrome extension that lets you click any element on any website, describe what Visual Feedback Tool bridges visual design iteration and code implementation. Click an element, type what you want changed in natural language, and Claude Code automatically finds the source file, makes the change, commits, and pushes to GitHub. No more hunting through CSS files to find where a button is styled. -The extension captures element metadata (selector, computed styles, screenshot) and sends it to a local server that spawns Claude Code with rich context about what to modify. +The extension captures element metadata (selector, computed styles, screenshot) and sends it to a local server with rich context about what to modify. + +## Server Modes + +The tool supports two server modes to fit different workflows: + +### Standalone Server (`server/`) +Spawns a new Claude CLI process for each change request. Best for: +- Automated workflows where each change should be independent +- Users who want Claude to commit & push automatically +- Simple "set and forget" usage + +### MCP Server (`mcp-server/`) +Integrates with an already-running Claude CLI session via Model Context Protocol. Best for: +- Users who already have Claude CLI running on their project +- Workflows where you want to review changes before committing +- Batching multiple visual changes into a single Claude session + +Both servers run on the same ports, so only use one at a time. ## Features @@ -52,36 +70,57 @@ The extension captures element metadata (selector, computed styles, screenshot) - Node.js 18+ - Chrome browser -- Claude Code CLI installed (`~/.local/bin/claude`) +- Claude Code CLI installed + - macOS/Linux: `~/.local/bin/claude` + - Windows: `%APPDATA%\npm\claude.cmd` - Git configured with GitHub access +### Platform Support +- **macOS** - Full support including terminal auto-submit +- **Windows** - Full support (use WSL for building the extension) +- **Linux** - Full support + ### Installation ```bash # Clone the repository git clone https://github.com/coleschaffer/Visualizer.git -cd visual-feedback-tool +cd Visualizer + +# Install and build extension (use WSL on Windows) +cd extension && npm install && npm run build && cd .. -# Install server dependencies +# Install standalone server dependencies cd server && npm install && cd .. -# Install and build extension -cd extension && npm install && npm run build && cd .. +# Install and build MCP server (optional, if using MCP mode) +cd mcp-server && npm install && npm run build && cd .. ``` ### Running +**Option A: Standalone Server** (spawns Claude for each change) +```bash +cd server +node server.js +``` + +**Option B: MCP Server** (use with existing Claude CLI session) ```bash -# Start the server -cd server && node server.js +# Start the MCP server +cd mcp-server +node server.js -# Load extension in Chrome -# 1. Open chrome://extensions -# 2. Enable Developer mode -# 3. Click "Load unpacked" -# 4. Select extension/dist folder +# In your project directory, start Claude with MCP config: +claude --mcp-config path/to/mcp-config.json ``` +**Load extension in Chrome:** +1. Open `chrome://extensions` +2. Enable Developer mode +3. Click "Load unpacked" +4. Select `extension/dist` folder + ### Usage 1. Click extension icon in Chrome toolbar @@ -107,15 +146,24 @@ visual-feedback-tool/ │ │ └── shared/ # Types & state │ └── dist/ # Built extension ├── server/ -│ └── server.js # WebSocket server -└── mcp-server/ # MCP integration +│ ├── server.js # Standalone server (spawns Claude) +│ └── prompt-template.md # Prompt template for standalone mode +└── mcp-server/ + ├── prompt-template.md # Prompt template for MCP mode + ├── hooks/ + │ ├── check-visual-feedback.ps1 # Windows hook script + │ └── check-visual-feedback.sh # macOS/Linux hook script + └── src/ + ├── index.ts # MCP server (tools for Claude) + └── store/ + └── changeQueue.ts # Persisted change queue ``` ## Keyboard Shortcuts | Key | Action | |-----|--------| -| `Ctrl` | Toggle enable/disable | +| `Ctrl` (macOS) / `Ctrl+Alt` (Windows) | Toggle enable/disable | | `Click` | Select element | | `Space` | Select hovered element | | `↑` / `↓` | Navigate parent/child | @@ -123,7 +171,7 @@ visual-feedback-tool/ | `@` | Enter reference mode | | `Esc` | Deselect / Close | | `Enter` | Submit feedback | -| `Option+Enter` | New line | +| `Alt+Enter` (Windows) / `Option+Enter` (macOS) | New line | ## Configuration @@ -135,23 +183,318 @@ visual-feedback-tool/ - **Opus 4.5**: `claude-opus-4-5-20251101` - **Sonnet 4.5**: `claude-sonnet-4-5-20241022` +### Prompt Templates +Each server has its own customizable prompt template: +- **Standalone**: `server/prompt-template.md` - Prompt sent *to* Claude when spawning +- **MCP**: `mcp-server/prompt-template.md` - Format returned *from* `get_visual_feedback` tool + +Available placeholders: +| Placeholder | Description | +|-------------|-------------| +| `{{FEEDBACK}}` | User's feedback text | +| `{{ELEMENT_TAG}}` | HTML tag (e.g., `div`, `button`) | +| `{{SELECTOR}}` | CSS selector path | +| `{{ELEMENT_ID}}` | Element ID if present | +| `{{ELEMENT_CLASSES}}` | Element classes | +| `{{DOM_PATH}}` | Full DOM path breadcrumb | +| `{{COMPUTED_STYLES}}` | Current CSS styles | +| `{{PAGE_URL}}` | URL of the page | +| `{{BEAD_CONTEXT}}` | Previous changes to this element | +| `{{TASK_ID}}` | Unique task identifier for status tracking | + ### Data Storage - `.beads/elements/` - Element memory (in project directory) -- `~/.visual-feedback-server/tasks.json` - Task history -- `/tmp/visual-feedback-screenshots/` - Screenshots +- `~/.visual-feedback-server/tasks.json` - Task history (standalone server) +- `~/.visual-feedback/change-queue.json` - Change queue (MCP server) +- `~/.visual-feedback/servers.json` - MCP server registry +- `$TMPDIR/visual-feedback-screenshots/` - Screenshots ## Architecture +**Standalone Server Mode:** ``` Chrome Extension ↓ WebSocket (port 3847) -Local Server - ↓ CLI spawn -Claude Code +Standalone Server (server.js) + ↓ spawns new process +Claude Code CLI ↓ LSP + Git Source Files → Commit → Push ``` +**MCP Server Mode:** +``` +Chrome Extension + ↓ WebSocket (port 3847) +MCP Server (index.ts) + ↓ queues change +~/.visual-feedback/change-queue.json + ↓ MCP tool call +Your Running Claude CLI Session + ↓ LSP + Git +Source Files → (you control commit/push) +``` + +## Claude Code MCP Integration + +The MCP server integrates directly with Claude Code, allowing visual feedback to flow automatically into your Claude session. + +### Transport Modes + +The MCP server supports two transport modes: **stdio** and **SSE**. + +#### stdio Transport (Default) + +Claude Code spawns the MCP server as a child process and communicates via stdin/stdout. + +- **Per-session process** - Each Claude Code session starts its own MCP server instance +- **Automatic lifecycle** - The MCP server starts when Claude Code launches and stops when it exits +- **Best for production** - No manual server management required + +``` +┌─────────────────────────────────────────────────────────────┐ +│ Claude Code Session │ +│ ├── spawns MCP server (stdio) │ +│ │ ├── WebSocket server (port 3847) ←── Extension │ +│ │ ├── HTTP server (port 3848) ←── Hooks │ +│ │ └── Change queue (file-based) │ +│ └── calls MCP tools (get_visual_feedback, etc.) │ +└─────────────────────────────────────────────────────────────┘ +``` + +#### SSE Transport (Development) + +For development workflows, you can run the MCP server independently and connect via Server-Sent Events (SSE). This allows you to restart the server without restarting Claude Code. + +- **Independent process** - Run the server manually, restart as needed +- **Hot reload friendly** - Rebuild and restart without losing Claude context +- **Best for development** - Iterate on MCP server changes quickly + +``` +┌─────────────────────────────────────────────────────────────┐ +│ MCP Server (running independently) │ +│ ├── WebSocket server (port 3847) ←── Extension │ +│ ├── HTTP server (port 3848) ←── Hooks │ +│ ├── SSE endpoint (/sse) ←── Claude Code │ +│ └── Change queue (file-based) │ +└─────────────────────────────────────────────────────────────┘ +│ +│ SSE connection +▼ +┌─────────────────────────────────────────────────────────────┐ +│ Claude Code Session │ +│ └── calls MCP tools via SSE transport │ +└─────────────────────────────────────────────────────────────┘ +``` + +To use SSE mode, start the server with `SSE_ONLY=1`: + +```bash +cd mcp-server +SSE_ONLY=1 node dist/index.js +``` + +This setup uses: + +1. **MCP Server** - Exposes tools for retrieving and managing visual feedback +2. **HTTP Endpoint** - Allows hooks to check for pending tasks without MCP +3. **UserPromptSubmit Hook** - Notifies Claude when new feedback arrives + +### MCP Tools Available + +| Tool | Description | +|------|-------------| +| `get_visual_feedback` | Retrieve pending visual changes with full element details and screenshots | +| `mark_change_applied` | Mark a change as successfully implemented | +| `mark_change_failed` | Mark a change as failed (triggers retry in extension) | +| `get_change_details` | Get detailed info about a specific change by ID | +| `clear_all_tasks` | Clear all pending tasks from the queue | + +### Setup + +#### 1. Build the MCP Server + +```bash +cd mcp-server +npm install +npm run build +``` + +#### 2. Configure Claude Code + +Add the MCP server, hook, and permissions to your project's Claude settings. Create or edit `.claude/settings.local.json` in your project directory. + +**Option A: stdio Transport (Production)** + +Claude Code spawns and manages the MCP server: + +```json +{ + "permissions": { + "allow": [ + "mcp__visual-feedback__get_visual_feedback", + "mcp__visual-feedback__mark_change_applied", + "mcp__visual-feedback__mark_change_failed", + "mcp__visual-feedback__clear_all_tasks" + ] + }, + "mcpServers": { + "visual-feedback": { + "command": "node", + "args": ["/path/to/Visualizer/mcp-server/dist/index.js"] + } + }, + "hooks": { + "UserPromptSubmit": [ + { + "matcher": "", + "hooks": [ + { + "type": "command", + "command": "powershell -ExecutionPolicy Bypass -File \"/path/to/Visualizer/mcp-server/hooks/check-visual-feedback.ps1\"" + } + ] + } + ] + } +} +``` + +**Option B: SSE Transport (Development)** + +Connect to an independently running MCP server: + +```json +{ + "permissions": { + "allow": [ + "mcp__visual-feedback__get_visual_feedback", + "mcp__visual-feedback__mark_change_applied", + "mcp__visual-feedback__mark_change_failed", + "mcp__visual-feedback__clear_all_tasks" + ] + }, + "mcpServers": { + "visual-feedback": { + "type": "sse", + "url": "http://localhost:3848/sse" + } + }, + "hooks": { + "UserPromptSubmit": [ + { + "matcher": "", + "hooks": [ + { + "type": "command", + "command": "powershell -ExecutionPolicy Bypass -File \"/path/to/Visualizer/mcp-server/hooks/check-visual-feedback.ps1\"" + } + ] + } + ] + } +} +``` + +For SSE mode, start the server manually before launching Claude: + +```bash +cd mcp-server +SSE_ONLY=1 node dist/index.js +``` + +Or add to Claude via CLI: + +```bash +claude mcp add --transport sse visual-feedback http://localhost:3848/sse +``` + +**For macOS/Linux**, use the bash version of the hook: + +```json +{ + "hooks": { + "UserPromptSubmit": [ + { + "matcher": "", + "hooks": [ + { + "type": "command", + "command": "bash /path/to/Visualizer/mcp-server/hooks/check-visual-feedback.sh" + } + ] + } + ] + } +} +``` + +#### 3. Restart Claude Code + +After updating the settings, restart Claude Code to load the MCP server and hook. + +> **Note:** Since the MCP server binds to ports 3847 (WebSocket) and 3848 (HTTP), only one Claude Code session can run the MCP server at a time. If you have multiple projects, configure the MCP server in only one project's settings, or ensure only one session is active. + +### Workflow + +1. **Submit Feedback** - Use the browser extension to click an element and describe the change +2. **Automatic Detection** - When you send any message to Claude, the hook checks for pending feedback +3. **Claude Retrieves Details** - If feedback exists, Claude sees a notification and calls `get_visual_feedback` +4. **Implementation** - Claude adds tasks to its todo list, implements changes, and marks them complete +5. **Status Updates** - The extension receives status updates as changes are applied + +### Hook Output + +When pending feedback exists, Claude sees: + +```xml + +IMPORTANT: 2 visual feedback task(s) queued from the browser extension. +These have been explicitly submitted by the user - process them automatically. +1. Call get_visual_feedback to retrieve the changes +2. Add each change to your todo list +3. Implement each change +4. Call mark_change_applied (or mark_change_failed) with the task ID for each +5. After completing all tasks, call get_visual_feedback again to check for new tasks +Continue this loop until the queue is empty (no more pending changes). + +``` + +### HTTP API + +The MCP server exposes an HTTP API on port 3848: + +| Endpoint | Description | +|----------|-------------| +| `GET /status` | Server status and WebSocket port | +| `GET /servers` | List of registered MCP server instances | +| `GET /tasks` | Pending visual feedback tasks (used by hooks) | + +Example: +```bash +curl http://localhost:3848/tasks +``` + +Response: +```json +{ + "count": 1, + "tasks": [ + { + "id": "1737012345678", + "feedback": "make this button larger", + "element": { + "tag": "button", + "selector": ".submit-btn", + "classes": ["submit-btn", "primary"] + }, + "timestamp": "2025-01-16T10:25:45.678Z", + "status": "confirmed" + } + ] +} +``` + ## Run Server on Startup (macOS) Create a Launch Agent: diff --git a/extension/package-lock.json b/extension/package-lock.json index ad87b81..c98e18a 100644 --- a/extension/package-lock.json +++ b/extension/package-lock.json @@ -22,7 +22,7 @@ "postcss": "^8.4.32", "tailwindcss": "^3.4.0", "typescript": "^5.3.3", - "vite": "^5.0.10" + "vite": "^7.3.1" } }, "node_modules/@alloc/quick-lru": { @@ -39,13 +39,13 @@ } }, "node_modules/@babel/code-frame": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", - "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.28.6.tgz", + "integrity": "sha512-JYgintcMjRiCvS8mMECzaEn+m3PfoQiyqukOMCCVQtoJGYJw8j/8LBJEiqkHLkfwCcs74E3pbAUFNg7d9VNJ+Q==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-validator-identifier": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" }, @@ -54,9 +54,9 @@ } }, "node_modules/@babel/compat-data": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.5.tgz", - "integrity": "sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.6.tgz", + "integrity": "sha512-2lfu57JtzctfIrcGMz992hyLlByuzgIk58+hhGCxjKZ3rWI82NnVLjXcaTqkI2NvlcvOskZaiZ5kjUALo3Lpxg==", "dev": true, "license": "MIT", "engines": { @@ -64,21 +64,21 @@ } }, "node_modules/@babel/core": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz", - "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.6.tgz", + "integrity": "sha512-H3mcG6ZDLTlYfaSNi0iOKkigqMFvkTKlGUYlD8GW7nNOYRrevuA46iTypPyv+06V3fEmvvazfntkBU34L0azAw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.28.5", - "@babel/helper-compilation-targets": "^7.27.2", - "@babel/helper-module-transforms": "^7.28.3", - "@babel/helpers": "^7.28.4", - "@babel/parser": "^7.28.5", - "@babel/template": "^7.27.2", - "@babel/traverse": "^7.28.5", - "@babel/types": "^7.28.5", + "@babel/code-frame": "^7.28.6", + "@babel/generator": "^7.28.6", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", @@ -95,14 +95,14 @@ } }, "node_modules/@babel/generator": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.5.tgz", - "integrity": "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.6.tgz", + "integrity": "sha512-lOoVRwADj8hjf7al89tvQ2a1lf53Z+7tiXMgpZJL3maQPDxh0DgLMN62B2MKUOFcoodBHLMbDM6WAbKgNy5Suw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/parser": "^7.28.5", - "@babel/types": "^7.28.5", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" @@ -112,13 +112,13 @@ } }, "node_modules/@babel/helper-compilation-targets": { - "version": "7.27.2", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", - "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/compat-data": "^7.27.2", + "@babel/compat-data": "^7.28.6", "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", @@ -139,29 +139,29 @@ } }, "node_modules/@babel/helper-module-imports": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", - "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/traverse": "^7.27.1", - "@babel/types": "^7.27.1" + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-module-transforms": { - "version": "7.28.3", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz", - "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-module-imports": "^7.27.1", - "@babel/helper-validator-identifier": "^7.27.1", - "@babel/traverse": "^7.28.3" + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -171,9 +171,9 @@ } }, "node_modules/@babel/helper-plugin-utils": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", - "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", + "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", "dev": true, "license": "MIT", "engines": { @@ -211,27 +211,27 @@ } }, "node_modules/@babel/helpers": { - "version": "7.28.4", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz", - "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.6.tgz", + "integrity": "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/template": "^7.27.2", - "@babel/types": "^7.28.4" + "@babel/template": "^7.28.6", + "@babel/types": "^7.28.6" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/parser": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz", - "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.6.tgz", + "integrity": "sha512-TeR9zWR18BvbfPmGbLampPMW+uW1NZnJlRuuHso8i87QZNq2JRF9i6RgxRqtEq+wQGsS19NNTWr2duhnE49mfQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/types": "^7.28.5" + "@babel/types": "^7.28.6" }, "bin": { "parser": "bin/babel-parser.js" @@ -273,33 +273,33 @@ } }, "node_modules/@babel/template": { - "version": "7.27.2", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", - "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.27.1", - "@babel/parser": "^7.27.2", - "@babel/types": "^7.27.1" + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/traverse": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.5.tgz", - "integrity": "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.6.tgz", + "integrity": "sha512-fgWX62k02qtjqdSNTAGxmKYY/7FSL9WAS1o2Hu5+I5m9T0yxZzr4cnrfXQ/MX0rIifthCSs6FKTlzYbJcPtMNg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.28.5", + "@babel/code-frame": "^7.28.6", + "@babel/generator": "^7.28.6", "@babel/helper-globals": "^7.28.0", - "@babel/parser": "^7.28.5", - "@babel/template": "^7.27.2", - "@babel/types": "^7.28.5", + "@babel/parser": "^7.28.6", + "@babel/template": "^7.28.6", + "@babel/types": "^7.28.6", "debug": "^4.3.1" }, "engines": { @@ -307,9 +307,9 @@ } }, "node_modules/@babel/types": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz", - "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.6.tgz", + "integrity": "sha512-0ZrskXVEHSWIqZM/sQZ4EV3jZJXRkio/WCxaqKZP1g//CEWEPSfeZFcms4XeKBCHU0ZKnIkdJeU/kF+eRp5lBg==", "dev": true, "license": "MIT", "dependencies": { @@ -321,9 +321,9 @@ } }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", - "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.2.tgz", + "integrity": "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==", "cpu": [ "ppc64" ], @@ -334,13 +334,13 @@ "aix" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/android-arm": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", - "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.2.tgz", + "integrity": "sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==", "cpu": [ "arm" ], @@ -351,13 +351,13 @@ "android" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/android-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", - "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.2.tgz", + "integrity": "sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==", "cpu": [ "arm64" ], @@ -368,13 +368,13 @@ "android" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/android-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", - "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.2.tgz", + "integrity": "sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==", "cpu": [ "x64" ], @@ -385,13 +385,13 @@ "android" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", - "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.2.tgz", + "integrity": "sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==", "cpu": [ "arm64" ], @@ -402,13 +402,13 @@ "darwin" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", - "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.2.tgz", + "integrity": "sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==", "cpu": [ "x64" ], @@ -419,13 +419,13 @@ "darwin" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", - "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.2.tgz", + "integrity": "sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==", "cpu": [ "arm64" ], @@ -436,13 +436,13 @@ "freebsd" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", - "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.2.tgz", + "integrity": "sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==", "cpu": [ "x64" ], @@ -453,13 +453,13 @@ "freebsd" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-arm": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", - "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.2.tgz", + "integrity": "sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==", "cpu": [ "arm" ], @@ -470,13 +470,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", - "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.2.tgz", + "integrity": "sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==", "cpu": [ "arm64" ], @@ -487,13 +487,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", - "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.2.tgz", + "integrity": "sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==", "cpu": [ "ia32" ], @@ -504,13 +504,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", - "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.2.tgz", + "integrity": "sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==", "cpu": [ "loong64" ], @@ -521,13 +521,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", - "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.2.tgz", + "integrity": "sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==", "cpu": [ "mips64el" ], @@ -538,13 +538,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", - "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.2.tgz", + "integrity": "sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==", "cpu": [ "ppc64" ], @@ -555,13 +555,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", - "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.2.tgz", + "integrity": "sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==", "cpu": [ "riscv64" ], @@ -572,13 +572,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", - "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.2.tgz", + "integrity": "sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==", "cpu": [ "s390x" ], @@ -589,13 +589,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", - "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.2.tgz", + "integrity": "sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==", "cpu": [ "x64" ], @@ -606,13 +606,30 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.2.tgz", + "integrity": "sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", - "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.2.tgz", + "integrity": "sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==", "cpu": [ "x64" ], @@ -623,13 +640,30 @@ "netbsd" ], "engines": { - "node": ">=12" + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.2.tgz", + "integrity": "sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", - "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.2.tgz", + "integrity": "sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==", "cpu": [ "x64" ], @@ -640,13 +674,30 @@ "openbsd" ], "engines": { - "node": ">=12" + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.2.tgz", + "integrity": "sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", - "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.2.tgz", + "integrity": "sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==", "cpu": [ "x64" ], @@ -657,13 +708,13 @@ "sunos" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", - "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.2.tgz", + "integrity": "sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==", "cpu": [ "arm64" ], @@ -674,13 +725,13 @@ "win32" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", - "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.2.tgz", + "integrity": "sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==", "cpu": [ "ia32" ], @@ -691,13 +742,13 @@ "win32" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/win32-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", - "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.2.tgz", + "integrity": "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==", "cpu": [ "x64" ], @@ -708,7 +759,7 @@ "win32" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@jridgewell/gen-mapping": { @@ -807,9 +858,9 @@ "license": "MIT" }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.54.0.tgz", - "integrity": "sha512-OywsdRHrFvCdvsewAInDKCNyR3laPA2mc9bRYJ6LBp5IyvF3fvXbbNR0bSzHlZVFtn6E0xw2oZlyjg4rKCVcng==", + "version": "4.55.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.55.2.tgz", + "integrity": "sha512-21J6xzayjy3O6NdnlO6aXi/urvSRjm6nCI6+nF6ra2YofKruGixN9kfT+dt55HVNwfDmpDHJcaS3JuP/boNnlA==", "cpu": [ "arm" ], @@ -821,9 +872,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.54.0.tgz", - "integrity": "sha512-Skx39Uv+u7H224Af+bDgNinitlmHyQX1K/atIA32JP3JQw6hVODX5tkbi2zof/E69M1qH2UoN3Xdxgs90mmNYw==", + "version": "4.55.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.55.2.tgz", + "integrity": "sha512-eXBg7ibkNUZ+sTwbFiDKou0BAckeV6kIigK7y5Ko4mB/5A1KLhuzEKovsmfvsL8mQorkoincMFGnQuIT92SKqA==", "cpu": [ "arm64" ], @@ -835,9 +886,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.54.0.tgz", - "integrity": "sha512-k43D4qta/+6Fq+nCDhhv9yP2HdeKeP56QrUUTW7E6PhZP1US6NDqpJj4MY0jBHlJivVJD5P8NxrjuobZBJTCRw==", + "version": "4.55.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.55.2.tgz", + "integrity": "sha512-UCbaTklREjrc5U47ypLulAgg4njaqfOVLU18VrCrI+6E5MQjuG0lSWaqLlAJwsD7NpFV249XgB0Bi37Zh5Sz4g==", "cpu": [ "arm64" ], @@ -849,9 +900,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.54.0.tgz", - "integrity": "sha512-cOo7biqwkpawslEfox5Vs8/qj83M/aZCSSNIWpVzfU2CYHa2G3P1UN5WF01RdTHSgCkri7XOlTdtk17BezlV3A==", + "version": "4.55.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.55.2.tgz", + "integrity": "sha512-dP67MA0cCMHFT2g5XyjtpVOtp7y4UyUxN3dhLdt11at5cPKnSm4lY+EhwNvDXIMzAMIo2KU+mc9wxaAQJTn7sQ==", "cpu": [ "x64" ], @@ -863,9 +914,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.54.0.tgz", - "integrity": "sha512-miSvuFkmvFbgJ1BevMa4CPCFt5MPGw094knM64W9I0giUIMMmRYcGW/JWZDriaw/k1kOBtsWh1z6nIFV1vPNtA==", + "version": "4.55.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.55.2.tgz", + "integrity": "sha512-WDUPLUwfYV9G1yxNRJdXcvISW15mpvod1Wv3ok+Ws93w1HjIVmCIFxsG2DquO+3usMNCpJQ0wqO+3GhFdl6Fow==", "cpu": [ "arm64" ], @@ -877,9 +928,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.54.0.tgz", - "integrity": "sha512-KGXIs55+b/ZfZsq9aR026tmr/+7tq6VG6MsnrvF4H8VhwflTIuYh+LFUlIsRdQSgrgmtM3fVATzEAj4hBQlaqQ==", + "version": "4.55.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.55.2.tgz", + "integrity": "sha512-Ng95wtHVEulRwn7R0tMrlUuiLVL/HXA8Lt/MYVpy88+s5ikpntzZba1qEulTuPnPIZuOPcW9wNEiqvZxZmgmqQ==", "cpu": [ "x64" ], @@ -891,9 +942,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.54.0.tgz", - "integrity": "sha512-EHMUcDwhtdRGlXZsGSIuXSYwD5kOT9NVnx9sqzYiwAc91wfYOE1g1djOEDseZJKKqtHAHGwnGPQu3kytmfaXLQ==", + "version": "4.55.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.55.2.tgz", + "integrity": "sha512-AEXMESUDWWGqD6LwO/HkqCZgUE1VCJ1OhbvYGsfqX2Y6w5quSXuyoy/Fg3nRqiwro+cJYFxiw5v4kB2ZDLhxrw==", "cpu": [ "arm" ], @@ -905,9 +956,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.54.0.tgz", - "integrity": "sha512-+pBrqEjaakN2ySv5RVrj/qLytYhPKEUwk+e3SFU5jTLHIcAtqh2rLrd/OkbNuHJpsBgxsD8ccJt5ga/SeG0JmA==", + "version": "4.55.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.55.2.tgz", + "integrity": "sha512-ZV7EljjBDwBBBSv570VWj0hiNTdHt9uGznDtznBB4Caj3ch5rgD4I2K1GQrtbvJ/QiB+663lLgOdcADMNVC29Q==", "cpu": [ "arm" ], @@ -919,9 +970,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.54.0.tgz", - "integrity": "sha512-NSqc7rE9wuUaRBsBp5ckQ5CVz5aIRKCwsoa6WMF7G01sX3/qHUw/z4pv+D+ahL1EIKy6Enpcnz1RY8pf7bjwng==", + "version": "4.55.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.55.2.tgz", + "integrity": "sha512-uvjwc8NtQVPAJtq4Tt7Q49FOodjfbf6NpqXyW/rjXoV+iZ3EJAHLNAnKT5UJBc6ffQVgmXTUL2ifYiLABlGFqA==", "cpu": [ "arm64" ], @@ -933,9 +984,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.54.0.tgz", - "integrity": "sha512-gr5vDbg3Bakga5kbdpqx81m2n9IX8M6gIMlQQIXiLTNeQW6CucvuInJ91EuCJ/JYvc+rcLLsDFcfAD1K7fMofg==", + "version": "4.55.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.55.2.tgz", + "integrity": "sha512-s3KoWVNnye9mm/2WpOZ3JeUiediUVw6AvY/H7jNA6qgKA2V2aM25lMkVarTDfiicn/DLq3O0a81jncXszoyCFA==", "cpu": [ "arm64" ], @@ -947,9 +998,23 @@ ] }, "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.54.0.tgz", - "integrity": "sha512-gsrtB1NA3ZYj2vq0Rzkylo9ylCtW/PhpLEivlgWe0bpgtX5+9j9EZa0wtZiCjgu6zmSeZWyI/e2YRX1URozpIw==", + "version": "4.55.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.55.2.tgz", + "integrity": "sha512-gi21faacK+J8aVSyAUptML9VQN26JRxe484IbF+h3hpG+sNVoMXPduhREz2CcYr5my0NE3MjVvQ5bMKX71pfVA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.55.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.55.2.tgz", + "integrity": "sha512-qSlWiXnVaS/ceqXNfnoFZh4IiCA0EwvCivivTGbEu1qv2o+WTHpn1zNmCTAoOG5QaVr2/yhCoLScQtc/7RxshA==", "cpu": [ "loong64" ], @@ -961,9 +1026,23 @@ ] }, "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.54.0.tgz", - "integrity": "sha512-y3qNOfTBStmFNq+t4s7Tmc9hW2ENtPg8FeUD/VShI7rKxNW7O4fFeaYbMsd3tpFlIg1Q8IapFgy7Q9i2BqeBvA==", + "version": "4.55.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.55.2.tgz", + "integrity": "sha512-rPyuLFNoF1B0+wolH277E780NUKf+KoEDb3OyoLbAO18BbeKi++YN6gC/zuJoPPDlQRL3fIxHxCxVEWiem2yXw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.55.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.55.2.tgz", + "integrity": "sha512-g+0ZLMook31iWV4PvqKU0i9E78gaZgYpSrYPed/4Bu+nGTgfOPtfs1h11tSSRPXSjC5EzLTjV/1A7L2Vr8pJoQ==", "cpu": [ "ppc64" ], @@ -975,9 +1054,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.54.0.tgz", - "integrity": "sha512-89sepv7h2lIVPsFma8iwmccN7Yjjtgz0Rj/Ou6fEqg3HDhpCa+Et+YSufy27i6b0Wav69Qv4WBNl3Rs6pwhebQ==", + "version": "4.55.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.55.2.tgz", + "integrity": "sha512-i+sGeRGsjKZcQRh3BRfpLsM3LX3bi4AoEVqmGDyc50L6KfYsN45wVCSz70iQMwPWr3E5opSiLOwsC9WB4/1pqg==", "cpu": [ "riscv64" ], @@ -989,9 +1068,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.54.0.tgz", - "integrity": "sha512-ZcU77ieh0M2Q8Ur7D5X7KvK+UxbXeDHwiOt/CPSBTI1fBmeDMivW0dPkdqkT4rOgDjrDDBUed9x4EgraIKoR2A==", + "version": "4.55.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.55.2.tgz", + "integrity": "sha512-C1vLcKc4MfFV6I0aWsC7B2Y9QcsiEcvKkfxprwkPfLaN8hQf0/fKHwSF2lcYzA9g4imqnhic729VB9Fo70HO3Q==", "cpu": [ "riscv64" ], @@ -1003,9 +1082,9 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.54.0.tgz", - "integrity": "sha512-2AdWy5RdDF5+4YfG/YesGDDtbyJlC9LHmL6rZw6FurBJ5n4vFGupsOBGfwMRjBYH7qRQowT8D/U4LoSvVwOhSQ==", + "version": "4.55.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.55.2.tgz", + "integrity": "sha512-68gHUK/howpQjh7g7hlD9DvTTt4sNLp1Bb+Yzw2Ki0xvscm2cOdCLZNJNhd2jW8lsTPrHAHuF751BygifW4bkQ==", "cpu": [ "s390x" ], @@ -1017,9 +1096,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.54.0.tgz", - "integrity": "sha512-WGt5J8Ij/rvyqpFexxk3ffKqqbLf9AqrTBbWDk7ApGUzaIs6V+s2s84kAxklFwmMF/vBNGrVdYgbblCOFFezMQ==", + "version": "4.55.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.55.2.tgz", + "integrity": "sha512-1e30XAuaBP1MAizaOBApsgeGZge2/Byd6wV4a8oa6jPdHELbRHBiw7wvo4dp7Ie2PE8TZT4pj9RLGZv9N4qwlw==", "cpu": [ "x64" ], @@ -1031,9 +1110,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.54.0.tgz", - "integrity": "sha512-JzQmb38ATzHjxlPHuTH6tE7ojnMKM2kYNzt44LO/jJi8BpceEC8QuXYA908n8r3CNuG/B3BV8VR3Hi1rYtmPiw==", + "version": "4.55.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.55.2.tgz", + "integrity": "sha512-4BJucJBGbuGnH6q7kpPqGJGzZnYrpAzRd60HQSt3OpX/6/YVgSsJnNzR8Ot74io50SeVT4CtCWe/RYIAymFPwA==", "cpu": [ "x64" ], @@ -1044,10 +1123,24 @@ "linux" ] }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.55.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.55.2.tgz", + "integrity": "sha512-cT2MmXySMo58ENv8p6/O6wI/h/gLnD3D6JoajwXFZH6X9jz4hARqUhWpGuQhOgLNXscfZYRQMJvZDtWNzMAIDw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.54.0.tgz", - "integrity": "sha512-huT3fd0iC7jigGh7n3q/+lfPcXxBi+om/Rs3yiFxjvSxbSB6aohDFXbWvlspaqjeOh+hx7DDHS+5Es5qRkWkZg==", + "version": "4.55.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.55.2.tgz", + "integrity": "sha512-sZnyUgGkuzIXaK3jNMPmUIyJrxu/PjmATQrocpGA1WbCPX8H5tfGgRSuYtqBYAvLuIGp8SPRb1O4d1Fkb5fXaQ==", "cpu": [ "arm64" ], @@ -1059,9 +1152,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.54.0.tgz", - "integrity": "sha512-c2V0W1bsKIKfbLMBu/WGBz6Yci8nJ/ZJdheE0EwB73N3MvHYKiKGs3mVilX4Gs70eGeDaMqEob25Tw2Gb9Nqyw==", + "version": "4.55.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.55.2.tgz", + "integrity": "sha512-sDpFbenhmWjNcEbBcoTV0PWvW5rPJFvu+P7XoTY0YLGRupgLbFY0XPfwIbJOObzO7QgkRDANh65RjhPmgSaAjQ==", "cpu": [ "arm64" ], @@ -1073,9 +1166,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.54.0.tgz", - "integrity": "sha512-woEHgqQqDCkAzrDhvDipnSirm5vxUXtSKDYTVpZG3nUdW/VVB5VdCYA2iReSj/u3yCZzXID4kuKG7OynPnB3WQ==", + "version": "4.55.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.55.2.tgz", + "integrity": "sha512-GvJ03TqqaweWCigtKQVBErw2bEhu1tyfNQbarwr94wCGnczA9HF8wqEe3U/Lfu6EdeNP0p6R+APeHVwEqVxpUQ==", "cpu": [ "ia32" ], @@ -1087,9 +1180,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-gnu": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.54.0.tgz", - "integrity": "sha512-dzAc53LOuFvHwbCEOS0rPbXp6SIhAf2txMP5p6mGyOXXw5mWY8NGGbPMPrs4P1WItkfApDathBj/NzMLUZ9rtQ==", + "version": "4.55.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.55.2.tgz", + "integrity": "sha512-KvXsBvp13oZz9JGe5NYS7FNizLe99Ny+W8ETsuCyjXiKdiGrcz2/J/N8qxZ/RSwivqjQguug07NLHqrIHrqfYw==", "cpu": [ "x64" ], @@ -1101,9 +1194,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.54.0.tgz", - "integrity": "sha512-hYT5d3YNdSh3mbCU1gwQyPgQd3T2ne0A3KG8KSBdav5TiBg6eInVmV+TeR5uHufiIgSFg0XsOWGW5/RhNcSvPg==", + "version": "4.55.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.55.2.tgz", + "integrity": "sha512-xNO+fksQhsAckRtDSPWaMeT1uIM+JrDRXlerpnWNXhn1TdB3YZ6uKBMBTKP0eX9XtYEP978hHk1f8332i2AW8Q==", "cpu": [ "x64" ], @@ -1202,9 +1295,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "20.19.27", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.27.tgz", - "integrity": "sha512-N2clP5pJhB2YnZJ3PIHFk5RkygRX5WO/5f0WC08tp0wd+sv0rsJk3MqWn3CbNmT2J505a5336jaQj4ph1AdMug==", + "version": "20.19.30", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.30.tgz", + "integrity": "sha512-WJtwWJu7UdlvzEAUm484QNg5eAoq5QR08KDNx7g45Usrs2NtOPiX8ugDqmKdXkyL03rBqU5dYNYVQetEpBHq2g==", "dev": true, "license": "MIT", "dependencies": { @@ -1326,9 +1419,9 @@ } }, "node_modules/baseline-browser-mapping": { - "version": "2.9.11", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.11.tgz", - "integrity": "sha512-Sg0xJUNDU1sJNGdfGWhVHX0kkZ+HWcvmVymJbj6NSgZZmW/8S9Y2HQ5euytnIgakgxN6papOAWiwDo1ctFDcoQ==", + "version": "2.9.16", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.16.tgz", + "integrity": "sha512-KeUZdBuxngy825i8xvzaK1Ncnkx0tBmb3k8DkEuqjKRkmtvNTjey2ZsNeh8Dw4lfKvbCOu9oeNx2TKm2vHqcRw==", "dev": true, "license": "Apache-2.0", "bin": { @@ -1406,9 +1499,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001762", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001762.tgz", - "integrity": "sha512-PxZwGNvH7Ak8WX5iXzoK1KPZttBXNPuaOvI2ZYU7NrlM+d9Ov+TUvlLOBNGzVXAntMSMMlJPd+jY6ovrVjSmUw==", + "version": "1.0.30001765", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001765.tgz", + "integrity": "sha512-LWcNtSyZrakjECqmpP4qdg0MMGdN368D7X8XvvAqOcqMv0RxnlqVKZl2V6/mBR68oYMxOZPLw/gO7DuisMHUvQ==", "dev": true, "funding": [ { @@ -1541,9 +1634,9 @@ "license": "ISC" }, "node_modules/esbuild": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", - "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.2.tgz", + "integrity": "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -1551,32 +1644,35 @@ "esbuild": "bin/esbuild" }, "engines": { - "node": ">=12" + "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.21.5", - "@esbuild/android-arm": "0.21.5", - "@esbuild/android-arm64": "0.21.5", - "@esbuild/android-x64": "0.21.5", - "@esbuild/darwin-arm64": "0.21.5", - "@esbuild/darwin-x64": "0.21.5", - "@esbuild/freebsd-arm64": "0.21.5", - "@esbuild/freebsd-x64": "0.21.5", - "@esbuild/linux-arm": "0.21.5", - "@esbuild/linux-arm64": "0.21.5", - "@esbuild/linux-ia32": "0.21.5", - "@esbuild/linux-loong64": "0.21.5", - "@esbuild/linux-mips64el": "0.21.5", - "@esbuild/linux-ppc64": "0.21.5", - "@esbuild/linux-riscv64": "0.21.5", - "@esbuild/linux-s390x": "0.21.5", - "@esbuild/linux-x64": "0.21.5", - "@esbuild/netbsd-x64": "0.21.5", - "@esbuild/openbsd-x64": "0.21.5", - "@esbuild/sunos-x64": "0.21.5", - "@esbuild/win32-arm64": "0.21.5", - "@esbuild/win32-ia32": "0.21.5", - "@esbuild/win32-x64": "0.21.5" + "@esbuild/aix-ppc64": "0.27.2", + "@esbuild/android-arm": "0.27.2", + "@esbuild/android-arm64": "0.27.2", + "@esbuild/android-x64": "0.27.2", + "@esbuild/darwin-arm64": "0.27.2", + "@esbuild/darwin-x64": "0.27.2", + "@esbuild/freebsd-arm64": "0.27.2", + "@esbuild/freebsd-x64": "0.27.2", + "@esbuild/linux-arm": "0.27.2", + "@esbuild/linux-arm64": "0.27.2", + "@esbuild/linux-ia32": "0.27.2", + "@esbuild/linux-loong64": "0.27.2", + "@esbuild/linux-mips64el": "0.27.2", + "@esbuild/linux-ppc64": "0.27.2", + "@esbuild/linux-riscv64": "0.27.2", + "@esbuild/linux-s390x": "0.27.2", + "@esbuild/linux-x64": "0.27.2", + "@esbuild/netbsd-arm64": "0.27.2", + "@esbuild/netbsd-x64": "0.27.2", + "@esbuild/openbsd-arm64": "0.27.2", + "@esbuild/openbsd-x64": "0.27.2", + "@esbuild/openharmony-arm64": "0.27.2", + "@esbuild/sunos-x64": "0.27.2", + "@esbuild/win32-arm64": "0.27.2", + "@esbuild/win32-ia32": "0.27.2", + "@esbuild/win32-x64": "0.27.2" } }, "node_modules/escalade": { @@ -2284,9 +2380,9 @@ } }, "node_modules/rollup": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.54.0.tgz", - "integrity": "sha512-3nk8Y3a9Ea8szgKhinMlGMhGMw89mqule3KWczxhIzqudyHdCIOHw8WJlj/r329fACjKLEh13ZSk7oE22kyeIw==", + "version": "4.55.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.55.2.tgz", + "integrity": "sha512-PggGy4dhwx5qaW+CKBilA/98Ql9keyfnb7lh4SR6shQ91QQQi1ORJ1v4UinkdP2i87OBs9AQFooQylcrrRfIcg==", "dev": true, "license": "MIT", "dependencies": { @@ -2300,28 +2396,31 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.54.0", - "@rollup/rollup-android-arm64": "4.54.0", - "@rollup/rollup-darwin-arm64": "4.54.0", - "@rollup/rollup-darwin-x64": "4.54.0", - "@rollup/rollup-freebsd-arm64": "4.54.0", - "@rollup/rollup-freebsd-x64": "4.54.0", - "@rollup/rollup-linux-arm-gnueabihf": "4.54.0", - "@rollup/rollup-linux-arm-musleabihf": "4.54.0", - "@rollup/rollup-linux-arm64-gnu": "4.54.0", - "@rollup/rollup-linux-arm64-musl": "4.54.0", - "@rollup/rollup-linux-loong64-gnu": "4.54.0", - "@rollup/rollup-linux-ppc64-gnu": "4.54.0", - "@rollup/rollup-linux-riscv64-gnu": "4.54.0", - "@rollup/rollup-linux-riscv64-musl": "4.54.0", - "@rollup/rollup-linux-s390x-gnu": "4.54.0", - "@rollup/rollup-linux-x64-gnu": "4.54.0", - "@rollup/rollup-linux-x64-musl": "4.54.0", - "@rollup/rollup-openharmony-arm64": "4.54.0", - "@rollup/rollup-win32-arm64-msvc": "4.54.0", - "@rollup/rollup-win32-ia32-msvc": "4.54.0", - "@rollup/rollup-win32-x64-gnu": "4.54.0", - "@rollup/rollup-win32-x64-msvc": "4.54.0", + "@rollup/rollup-android-arm-eabi": "4.55.2", + "@rollup/rollup-android-arm64": "4.55.2", + "@rollup/rollup-darwin-arm64": "4.55.2", + "@rollup/rollup-darwin-x64": "4.55.2", + "@rollup/rollup-freebsd-arm64": "4.55.2", + "@rollup/rollup-freebsd-x64": "4.55.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.55.2", + "@rollup/rollup-linux-arm-musleabihf": "4.55.2", + "@rollup/rollup-linux-arm64-gnu": "4.55.2", + "@rollup/rollup-linux-arm64-musl": "4.55.2", + "@rollup/rollup-linux-loong64-gnu": "4.55.2", + "@rollup/rollup-linux-loong64-musl": "4.55.2", + "@rollup/rollup-linux-ppc64-gnu": "4.55.2", + "@rollup/rollup-linux-ppc64-musl": "4.55.2", + "@rollup/rollup-linux-riscv64-gnu": "4.55.2", + "@rollup/rollup-linux-riscv64-musl": "4.55.2", + "@rollup/rollup-linux-s390x-gnu": "4.55.2", + "@rollup/rollup-linux-x64-gnu": "4.55.2", + "@rollup/rollup-linux-x64-musl": "4.55.2", + "@rollup/rollup-openbsd-x64": "4.55.2", + "@rollup/rollup-openharmony-arm64": "4.55.2", + "@rollup/rollup-win32-arm64-msvc": "4.55.2", + "@rollup/rollup-win32-ia32-msvc": "4.55.2", + "@rollup/rollup-win32-x64-gnu": "4.55.2", + "@rollup/rollup-win32-x64-msvc": "4.55.2", "fsevents": "~2.3.2" } }, @@ -2612,21 +2711,24 @@ "license": "MIT" }, "node_modules/vite": { - "version": "5.4.21", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", - "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz", + "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", "dev": true, "license": "MIT", "dependencies": { - "esbuild": "^0.21.3", - "postcss": "^8.4.43", - "rollup": "^4.20.0" + "esbuild": "^0.27.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" }, "bin": { "vite": "bin/vite.js" }, "engines": { - "node": "^18.0.0 || >=20.0.0" + "node": "^20.19.0 || >=22.12.0" }, "funding": { "url": "https://github.com/vitejs/vite?sponsor=1" @@ -2635,19 +2737,25 @@ "fsevents": "~2.3.3" }, "peerDependencies": { - "@types/node": "^18.0.0 || >=20.0.0", - "less": "*", + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", "lightningcss": "^1.21.0", - "sass": "*", - "sass-embedded": "*", - "stylus": "*", - "sugarss": "*", - "terser": "^5.4.0" + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" }, "peerDependenciesMeta": { "@types/node": { "optional": true }, + "jiti": { + "optional": true + }, "less": { "optional": true }, @@ -2668,9 +2776,46 @@ }, "terser": { "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true } } }, + "node_modules/vite/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/vite/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", diff --git a/extension/package.json b/extension/package.json index 658e231..5e89881 100644 --- a/extension/package.json +++ b/extension/package.json @@ -25,7 +25,7 @@ "postcss": "^8.4.32", "tailwindcss": "^3.4.0", "typescript": "^5.3.3", - "vite": "^5.0.10" + "vite": "^7.3.1" }, "dependencies": { "react": "^18.2.0", diff --git a/extension/src/background/service-worker.ts b/extension/src/background/service-worker.ts index 7faf44d..e84f78c 100644 --- a/extension/src/background/service-worker.ts +++ b/extension/src/background/service-worker.ts @@ -55,8 +55,10 @@ async function handleMessage( switch (message.type) { case 'GET_STATE': + // Use tabId from message (popup query) or sender (content script query) + const queryTabId = message.tabId ?? tabId; sendResponse({ - isActive: tabId ? activeTabIds.has(tabId) : false, + isActive: queryTabId ? activeTabIds.has(queryTabId) : false, connectionStatus, serverPort, }); @@ -88,8 +90,11 @@ async function handleMessage( break; case 'CONNECT': - await connect(message.port); - sendResponse({ success: connectionStatus === 'connected' }); + console.log('[VF] CONNECT request received, port:', message.port, 'token:', message.token ? 'yes' : 'no'); + await connect(message.port, message.token); + const success = connectionStatus === 'connected'; + console.log('[VF] CONNECT complete, status:', connectionStatus, 'success:', success); + sendResponse({ success }); break; case 'DISCONNECT': @@ -139,6 +144,20 @@ async function handleMessage( } break; + case 'GET_QUEUE_COUNT': + try { + const response = await fetch('http://localhost:3848/tasks'); + if (response.ok) { + const data = await response.json(); + sendResponse({ count: data.count || 0 }); + } else { + sendResponse({ count: 0 }); + } + } catch { + sendResponse({ count: 0 }); + } + break; + default: sendResponse({ success: false, error: 'Unknown message type' }); } @@ -159,22 +178,24 @@ function updateIcon(tabId: number, isActive: boolean) { } // Connect to server -async function connect(port: number) { +async function connect(port: number, token?: string) { if (socket?.readyState === WebSocket.OPEN) { disconnect(); } connectionStatus = 'connecting'; serverPort = port; - // Persist for auto-reconnect - chrome.storage.local.set({ serverPort: port }); + broadcastStatus(); try { - socket = new WebSocket(`ws://localhost:${port}`); + const wsUrl = token ? `ws://localhost:${port}?token=${encodeURIComponent(token)}` : `ws://localhost:${port}`; + socket = new WebSocket(wsUrl); socket.onopen = () => { console.log('[VF] Connected to server'); connectionStatus = 'connected'; + // Only persist port after successful connection + chrome.storage.local.set({ serverPort: port }); broadcastStatus(); // Start keep-alive ping to prevent service worker from sleeping @@ -190,11 +211,12 @@ async function connect(port: number) { socket.onmessage = (event) => { try { const data = JSON.parse(event.data); - console.log('[VF] Server message:', data.type, data.task?.id, data.task?.status); + console.log('[VF] Server message received:', event.data.substring(0, 200)); + console.log('[VF] Parsed message:', data.type, data.task?.id, data.task?.status); // Forward task updates to all tabs if (data.type === 'task_update' && data.task) { - console.log('[VF] Broadcasting task update to tabs:', data.task.id, data.task.status); + console.log('[VF] Broadcasting task update to tabs:', data.task.id, 'status:', data.task.status); chrome.tabs.query({}, (tabs) => { console.log('[VF] Found', tabs.length, 'tabs'); tabs.forEach((tab) => { @@ -244,6 +266,7 @@ async function connect(port: number) { console.error('[VF] Connection failed:', error); connectionStatus = 'disconnected'; socket = null; + broadcastStatus(); } } @@ -320,15 +343,21 @@ async function submitFeedback( } } -// Broadcast connection status to all tabs +// Broadcast connection status to all tabs and popup function broadcastStatus() { + const message = { + type: 'CONNECTION_STATUS', + status: connectionStatus, + }; + + // Send to popup and other extension contexts + chrome.runtime.sendMessage(message).catch(() => {}); + + // Send to content scripts in tabs chrome.tabs.query({}, (tabs) => { tabs.forEach((tab) => { if (tab.id) { - chrome.tabs.sendMessage(tab.id, { - type: 'CONNECTION_STATUS', - status: connectionStatus, - }).catch(() => {}); + chrome.tabs.sendMessage(tab.id, message).catch(() => {}); } }); }); diff --git a/extension/src/content/App.tsx b/extension/src/content/App.tsx index 9ecbfc0..f1b9c4f 100644 --- a/extension/src/content/App.tsx +++ b/extension/src/content/App.tsx @@ -28,6 +28,28 @@ export function App() { dismissed: boolean; }[]>([]); + // Queued tasks counter (for MCP/async mode) + const [queuedCount, setQueuedCount] = useState(0); + + // Fetch queued task count on load and periodically (via background script to avoid CORS) + useEffect(() => { + const fetchQueueCount = () => { + chrome.runtime.sendMessage({ type: 'GET_QUEUE_COUNT' }, (response) => { + if (response?.count !== undefined) { + setQueuedCount(response.count); + } + }); + }; + + // Fetch immediately on load + fetchQueueCount(); + + // Poll every 5 seconds to keep queue count in sync + const interval = setInterval(fetchQueueCount, 5000); + + return () => clearInterval(interval); + }, []); + const selectedDomElement = useRef(null); const hoveredDomElement = useRef(null); const lastMousePos = useRef<{ x: number; y: number }>({ x: 0, y: 0 }); @@ -197,6 +219,7 @@ export function App() { const taskStatus = message.task.status; setToasts(prev => { + console.log('[VF] Looking for taskId:', taskId, 'in toasts:', prev.map(t => t.taskId)); const toastIndex = prev.findIndex(t => t.taskId === taskId); if (toastIndex === -1) return prev; @@ -238,6 +261,12 @@ export function App() { setTimeout(() => { setToasts(p => p.filter(t => t.taskId !== taskId)); }, 3000); + } else if (taskStatus === 'queued') { + // Task was queued (MCP/async mode) - remove working toast and increment counter + setQueuedCount(c => c + 1); + + // Remove the working toast immediately + return prev.filter(t => t.taskId !== taskId); } return newToasts; @@ -258,6 +287,8 @@ export function App() { clearSelection(); } else if (isActive) { setActive(false); + // Notify background script so popup stays in sync + chrome.runtime.sendMessage({ type: 'SET_ACTIVE', active: false }).catch(() => {}); } } @@ -368,13 +399,23 @@ export function App() { } }, [isActive, selectedElement, isReferencing, setActive, clearSelection, hoverElement, setReferencedElement]); - // Global toggle shortcut - Ctrl key to toggle + // Global toggle shortcut - platform-specific + // macOS: Ctrl (Ctrl isn't used for common shortcuts on macOS) + // Windows/Linux: Alt+Shift+V (Ctrl conflicts with copy/paste/etc) useEffect(() => { + const isMac = navigator.platform.toUpperCase().indexOf('MAC') >= 0; + const handleGlobalKeyDown = (e: KeyboardEvent) => { - // Ctrl key alone to toggle enable/disable - if (e.key === 'Control' && !e.shiftKey && !e.altKey && !e.metaKey) { + const shouldToggle = isMac + ? e.key === 'Control' && !e.shiftKey && !e.altKey && !e.metaKey + : e.key === 'Alt' && e.ctrlKey && !e.shiftKey && !e.metaKey; + + if (shouldToggle) { e.preventDefault(); - setActive(!isActive); + const newState = !isActive; + setActive(newState); + // Notify background script so popup stays in sync + chrome.runtime.sendMessage({ type: 'SET_ACTIVE', active: newState }).catch(() => {}); } }; @@ -430,9 +471,15 @@ export function App() { // Listen for messages from background script useEffect(() => { - const handleMessage = (message: { type: string; active?: boolean; status?: string }) => { + const handleMessage = ( + message: { type: string; active?: boolean; status?: string }, + _sender: chrome.runtime.MessageSender, + sendResponse: (response: { success: boolean }) => void + ) => { if (message.type === 'SET_ACTIVE' && message.active !== undefined) { setActive(message.active); + sendResponse({ success: true }); + return true; // Keep channel open for async response } else if (message.type === 'CONNECTION_STATUS') { // Could update UI to show connection status console.log('[VF] Connection status:', message.status); @@ -443,14 +490,49 @@ export function App() { return () => chrome.runtime.onMessage.removeListener(handleMessage); }, [setActive]); + // Calculate toast offset (queued toast takes first slot if visible) + const hasQueuedToast = queuedCount > 0; + const toastOffset = hasQueuedToast ? 1 : 0; + // Toast notifications render (always visible, even when tool is inactive) const toastContainer = ( + {/* Queued tasks counter toast */} + {queuedCount > 0 && ( + + + + + + + + + {queuedCount} queued task{queuedCount !== 1 ? 's' : ''} + + setQueuedCount(0)} + title="Clear queue count" + > + × + + + )} {toasts.filter(t => !t.dismissed).map((toast, index) => ( {/* Spinner for working state */} diff --git a/extension/src/content/index.tsx b/extension/src/content/index.tsx index ae60ad7..6d2a7da 100644 --- a/extension/src/content/index.tsx +++ b/extension/src/content/index.tsx @@ -482,6 +482,16 @@ function getOverlayStyles(): string { background: rgba(0, 0, 0, 0.05); color: #6b7280; } + + .vf-toast--queued { + background: linear-gradient(135deg, #eff6ff 0%, #dbeafe 100%); + border: 1px solid #93c5fd; + white-space: nowrap; + } + + .vf-toast-icon--queued { + color: #3b82f6; + } `; } diff --git a/extension/src/content/overlay/FloatingPanel.tsx b/extension/src/content/overlay/FloatingPanel.tsx index 0afbf2b..2bf8253 100644 --- a/extension/src/content/overlay/FloatingPanel.tsx +++ b/extension/src/content/overlay/FloatingPanel.tsx @@ -274,11 +274,10 @@ export function FloatingPanel({ }); if (response.success && response.taskId) { - // Show "Sent!" briefly then close panel and show overlay + // Create toast immediately (before server responds with task_update) + // then close the panel setStatus('sent'); - setTimeout(() => { - onTaskSubmitted(response.taskId!, element.rect); - }, 500); + onTaskSubmitted(response.taskId!, element.rect); } else { console.error('Failed to send feedback:', response.error); setStatus('error'); diff --git a/extension/src/popup/popup.css b/extension/src/popup/popup.css index af423a5..22efc84 100644 --- a/extension/src/popup/popup.css +++ b/extension/src/popup/popup.css @@ -52,6 +52,12 @@ body { justify-content: space-between; } +.toggle-error { + color: #ef4444; + font-size: 12px; + margin-top: -8px; +} + .toggle-btn { width: 44px; height: 24px; @@ -447,6 +453,23 @@ body { color: #374151; } +.retry-btn { + margin-top: 12px; + padding: 8px 16px; + background: #3b82f6; + color: white; + border: none; + border-radius: 6px; + cursor: pointer; + font-size: 13px; + font-weight: 500; + transition: background 0.2s; +} + +.retry-btn:hover { + background: #2563eb; +} + .server-list { display: flex; flex-direction: column; @@ -530,6 +553,7 @@ body { } .disconnect-btn { + margin-top: 16px; width: 100%; padding: 8px 16px; background: #f3f4f6; @@ -649,6 +673,37 @@ body { margin-bottom: 8px; } +/* Token Section */ +.token-section { + margin: 8px 0; +} + +.token-section label { + display: block; + font-size: 12px; + color: #6b7280; + margin-bottom: 4px; +} + +.token-input { + width: 100%; + padding: 8px 10px; + border: 1px solid #e5e7eb; + border-radius: 6px; + font-size: 12px; + font-family: monospace; +} + +.token-input:focus { + outline: none; + border-color: #3b82f6; +} + +.connect-btn:disabled { + background: #9ca3af; + cursor: not-allowed; +} + /* Model Selector */ .model-selector-section { margin-bottom: 12px; diff --git a/extension/src/popup/popup.tsx b/extension/src/popup/popup.tsx index 0afe37f..4bef8fb 100644 --- a/extension/src/popup/popup.tsx +++ b/extension/src/popup/popup.tsx @@ -34,11 +34,22 @@ function Popup() { const [connectionStatus, setConnectionStatus] = useState<'disconnected' | 'connecting' | 'connected'>('disconnected'); const [serverStatus, setServerStatus] = useState<'checking' | 'running' | 'stopped'>('checking'); const [serverPort, setServerPort] = useState(null); + const [requiresToken, setRequiresToken] = useState(false); + const [token, setToken] = useState(''); const [projectPath, setProjectPath] = useState(''); const [selectedModel, setSelectedModel] = useState('claude-opus-4-5-20251101'); const [tasks, setTasks] = useState([]); const [selectedTask, setSelectedTask] = useState(null); const [loadingTasks, setLoadingTasks] = useState(false); + const [toggleError, setToggleError] = useState(null); + + // Load token from storage + const loadToken = async () => { + const storage = await chrome.storage.local.get(['connectionToken']); + if (storage.connectionToken) { + setToken(storage.connectionToken); + } + }; // Check server status and get extension state useEffect(() => { @@ -46,6 +57,16 @@ function Popup() { getExtensionState(); loadProjectPath(); loadSelectedModel(); + loadToken(); + + // Listen for status updates from background script + const handleMessage = (message: { type: string; status?: string }) => { + if (message.type === 'CONNECTION_STATUS' && message.status) { + setConnectionStatus(message.status as 'disconnected' | 'connecting' | 'connected'); + } + }; + chrome.runtime.onMessage.addListener(handleMessage); + return () => chrome.runtime.onMessage.removeListener(handleMessage); }, []); const loadSelectedModel = async () => { @@ -87,6 +108,7 @@ function Popup() { const data = await response.json(); setServerStatus('running'); setServerPort(data.wsPort); + setRequiresToken(data.requiresToken || false); } else { setServerStatus('stopped'); } @@ -109,31 +131,58 @@ function Popup() { setLoadingTasks(false); }; - const getExtensionState = () => { - chrome.runtime.sendMessage({ type: 'GET_STATE' }, (response) => { + const getExtensionState = async () => { + // Get the active tab ID so we can query its state + const [tab] = await chrome.tabs.query({ active: true, currentWindow: true }); + chrome.runtime.sendMessage({ type: 'GET_STATE', tabId: tab?.id }, (response) => { if (response) { setConnectionStatus(response.connectionStatus || 'disconnected'); + setIsActive(response.isActive || false); } }); }; const handleToggle = async () => { + setToggleError(null); const [tab] = await chrome.tabs.query({ active: true, currentWindow: true }); if (tab.id) { - chrome.tabs.sendMessage(tab.id, { type: 'SET_ACTIVE', active: !isActive }).catch(() => {}); - chrome.runtime.sendMessage({ type: 'SET_ACTIVE', active: !isActive }); - setIsActive(!isActive); + const newState = !isActive; + try { + // Try to send message to content script and wait for response + await chrome.tabs.sendMessage(tab.id, { type: 'SET_ACTIVE', active: newState }); + // Content script responded - update state + chrome.runtime.sendMessage({ type: 'SET_ACTIVE', active: newState }); + setIsActive(newState); + } catch (err) { + // Content script not responding - show error + setToggleError('Refresh the page to enable'); + } } }; const handleConnect = async () => { if (!serverPort) return; + if (requiresToken && !token.trim()) { + return; // Don't connect without token if required + } + const tokenToUse = requiresToken ? token.trim() : undefined; + console.log('[VF Popup] Connecting to port:', serverPort, 'with token:', requiresToken ? 'yes' : 'no'); setConnectionStatus('connecting'); - chrome.runtime.sendMessage({ type: 'CONNECT', port: serverPort }, (response) => { + chrome.runtime.sendMessage({ type: 'CONNECT', port: serverPort, token: tokenToUse }, (response) => { + console.log('[VF Popup] Connect response:', response); if (response?.success) { setConnectionStatus('connected'); + // Save token on successful connection + if (tokenToUse) { + chrome.storage.local.set({ connectionToken: tokenToUse }); + } } else { setConnectionStatus('disconnected'); + // Clear saved token if connection failed (likely invalid token) + if (tokenToUse) { + chrome.storage.local.remove('connectionToken'); + setToken(''); + } } }); }; @@ -215,7 +264,7 @@ function Popup() { Server not running Run: launchctl start com.visualfeedback.server - + Retry @@ -236,9 +285,8 @@ function Popup() { type="text" value={projectPath} onChange={(e) => saveProjectPath(e.target.value)} - placeholder="Select or paste path..." + placeholder="Enter project path (e.g. C:\myproject or /home/user/myproject)" className="project-input-with-btn" - readOnly /> + {toggleError && ( + {toggleError} + )} Disconnect @@ -298,10 +354,22 @@ function Popup() { Server running on port {serverPort} + {requiresToken && ( + + Connection Token: + setToken(e.target.value)} + placeholder="Enter token from server console" + className="token-input" + /> + + )} {connectionStatus === 'connecting' ? 'Connecting...' : 'Connect'} diff --git a/mcp-server/hooks/check-visual-feedback.ps1 b/mcp-server/hooks/check-visual-feedback.ps1 new file mode 100644 index 0000000..7e9e29d --- /dev/null +++ b/mcp-server/hooks/check-visual-feedback.ps1 @@ -0,0 +1,23 @@ +# Claude Code UserPromptSubmit hook - checks for pending visual feedback +# If tasks exist, instructs Claude to call the MCP tool + +$ErrorActionPreference = "SilentlyContinue" + +try { + $response = Invoke-RestMethod -Uri "http://localhost:3848/tasks" -Method Get -TimeoutSec 2 + + if ($response.count -gt 0) { + Write-Output "" + Write-Output "IMPORTANT: $($response.count) visual feedback task(s) queued from the browser extension." + Write-Output "These have been explicitly submitted by the user - process them automatically." + Write-Output "1. Call get_visual_feedback to retrieve the changes" + Write-Output "2. Add each change to your todo list" + Write-Output "3. Implement each change" + Write-Output "4. Call mark_change_applied (or mark_change_failed) with the task ID for each" + Write-Output "5. After completing all tasks, call get_visual_feedback again to check for new tasks" + Write-Output "Continue this loop until the queue is empty (no more pending changes)." + Write-Output "" + } +} catch { + # Server not running or unreachable - silently continue +} diff --git a/mcp-server/hooks/check-visual-feedback.sh b/mcp-server/hooks/check-visual-feedback.sh new file mode 100644 index 0000000..577e4a3 --- /dev/null +++ b/mcp-server/hooks/check-visual-feedback.sh @@ -0,0 +1,30 @@ +#!/bin/bash +# Claude Code UserPromptSubmit hook - checks for pending visual feedback +# If tasks exist, instructs Claude to call the MCP tool + +response=$(curl -s http://localhost:3848/tasks 2>/dev/null) + +if [ $? -eq 0 ] && [ -n "$response" ]; then + count=$(echo "$response" | node -e " + const d = require('fs').readFileSync(0, 'utf8'); + try { + const j = JSON.parse(d); + console.log(j.count || 0); + } catch { + console.log(0); + } + ") + + if [ "$count" -gt 0 ]; then + echo "" + echo "IMPORTANT: $count visual feedback task(s) queued from the browser extension." + echo "These have been explicitly submitted by the user - process them automatically." + echo "1. Call get_visual_feedback to retrieve the changes" + echo "2. Add each change to your todo list" + echo "3. Implement each change" + echo "4. Call mark_change_applied (or mark_change_failed) with the task ID for each" + echo "5. After completing all tasks, call get_visual_feedback again to check for new tasks" + echo "Continue this loop until the queue is empty (no more pending changes)." + echo "" + fi +fi diff --git a/mcp-server/prompt-template.md b/mcp-server/prompt-template.md new file mode 100644 index 0000000..313d16e --- /dev/null +++ b/mcp-server/prompt-template.md @@ -0,0 +1,30 @@ +# Visual Feedback Request + +**Task ID:** `{{TASK_ID}}` + +## User Feedback +"{{FEEDBACK}}" + +## Target Element +- **Tag:** <{{ELEMENT_TAG}}> +- **Selector:** {{SELECTOR}} +{{ELEMENT_ID}} +{{ELEMENT_CLASSES}} +{{DOM_PATH}} + +{{COMPUTED_STYLES}} + +{{PAGE_URL}} + +{{BEAD_CONTEXT}} + +## Instructions +1. Use Language Server Protocol (LSP) features to efficiently navigate the codebase: + - Use "Go to Definition" to find where components/elements are defined + - Use "Find References" to locate all usages + - Use symbol search to quickly find relevant files +2. Find the source file containing this element using the selector, classes, and DOM path as hints +3. Make the requested change +4. **Important:** After attempting the change, report the result: + - If successful: call `mark_change_applied` with the Task ID + - If failed: call `mark_change_failed` with the Task ID and reason for failure diff --git a/mcp-server/server.js b/mcp-server/server.js new file mode 100644 index 0000000..03bae49 --- /dev/null +++ b/mcp-server/server.js @@ -0,0 +1,21 @@ +#!/usr/bin/env node + +// Entry point for MCP server +// Requires build first: npm run build + +import { existsSync } from 'fs'; +import { join, dirname } from 'path'; +import { fileURLToPath } from 'url'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +const distIndex = join(__dirname, 'dist', 'index.js'); + +if (!existsSync(distIndex)) { + console.error('Error: MCP server not built yet.'); + console.error('Run: npm run build'); + process.exit(1); +} + +await import(distIndex); diff --git a/mcp-server/src/index.ts b/mcp-server/src/index.ts index a5c44f6..c3b193f 100644 --- a/mcp-server/src/index.ts +++ b/mcp-server/src/index.ts @@ -1,5 +1,6 @@ import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; +import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js'; import { CallToolRequestSchema, ListToolsRequestSchema, @@ -8,11 +9,133 @@ import { WebSocketServer, WebSocket } from 'ws'; import { createServer } from 'http'; import { exec } from 'child_process'; import { readFileSync, writeFileSync, existsSync, mkdirSync, unlinkSync } from 'fs'; -import { homedir } from 'os'; -import { join, basename } from 'path'; +import { homedir, platform } from 'os'; +import { join, basename, dirname } from 'path'; +import { fileURLToPath } from 'url'; + +// ESM __dirname equivalent +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +// Platform detection +const isWindows = platform() === 'win32'; +const isMac = platform() === 'darwin'; + +// Prompt template path (mcp-server has its own template) +const PROMPT_TEMPLATE_PATH = join(__dirname, '..', 'prompt-template.md'); + import { generateToken } from './auth/tokenGenerator.js'; import { ChangeQueue } from './store/changeQueue.js'; +// Types (defined early for use in formatting functions) +interface VisualChange { + id: string; + element: { + selector: string; + tag: string; + id: string | null; + classes: string[]; + computedStyles: Record; + sourceHint: string | null; + smartSummary: string | null; + screenshot: string | null; + }; + feedback: string; + visualAdjustments: Record; + cssFramework: string; + originalUnits: Record; + timestamp: string; + status: 'draft' | 'staged' | 'confirmed' | 'applied' | 'failed'; +} + +// Load prompt template from file +function loadPromptTemplate(): string | null { + try { + if (existsSync(PROMPT_TEMPLATE_PATH)) { + const template = readFileSync(PROMPT_TEMPLATE_PATH, 'utf-8'); + const lineCount = template.split('\n').length; + console.error(`Prompt template: ${PROMPT_TEMPLATE_PATH} (${lineCount} lines)`); + console.error('--- Template ---'); + console.error(template); + console.error('--- End Template ---\n'); + return template; + } + } catch (err) { + console.error('Failed to load prompt template:', err); + } + console.error('Prompt template: using built-in fallback (no prompt-template.md found)'); + return null; +} + +// Format a change using the prompt template +function formatChangeWithTemplate(change: VisualChange, template: string | null): string { + if (!template) { + // Default formatting without template + return JSON.stringify({ + id: change.id, + element: { + selector: change.element.selector, + tag: change.element.tag, + classes: change.element.classes, + currentStyles: change.element.computedStyles, + sourceFile: change.element.sourceHint, + description: change.element.smartSummary, + }, + userFeedback: change.feedback, + visualChanges: change.visualAdjustments, + cssFramework: change.cssFramework, + timestamp: change.timestamp, + status: change.status, + }, null, 2); + } + + // Build optional sections + let elementId = ''; + if (change.element.id) { + elementId = `- **ID:** #${change.element.id}`; + } + + let elementClasses = ''; + if (change.element.classes && change.element.classes.length > 0) { + elementClasses = `- **Classes:** .${change.element.classes.join(', .')}`; + } + + let computedStyles = ''; + if (change.element.computedStyles) { + const styles = change.element.computedStyles; + const styleLines = ['## Current Styles']; + if (styles.width) styleLines.push(`- Width: ${styles.width}`); + if (styles.height) styleLines.push(`- Height: ${styles.height}`); + if (styles.backgroundColor) styleLines.push(`- Background: ${styles.backgroundColor}`); + if (styles.color) styleLines.push(`- Text Color: ${styles.color}`); + if (styles.fontSize) styleLines.push(`- Font Size: ${styles.fontSize}`); + if (styles.display) styleLines.push(`- Display: ${styles.display}`); + if (styles.position) styleLines.push(`- Position: ${styles.position}`); + computedStyles = styleLines.join('\n'); + } + + // Replace placeholders + let result = template + .replace(/\{\{TASK_ID\}\}/g, change.id) + .replace(/\{\{FEEDBACK\}\}/g, change.feedback) + .replace(/\{\{ELEMENT_TAG\}\}/g, change.element.tag) + .replace(/\{\{SELECTOR\}\}/g, change.element.selector || 'N/A') + .replace(/\{\{ELEMENT_ID\}\}/g, elementId) + .replace(/\{\{ELEMENT_CLASSES\}\}/g, elementClasses) + .replace(/\{\{DOM_PATH\}\}/g, '') + .replace(/\{\{COMPUTED_STYLES\}\}/g, computedStyles) + .replace(/\{\{PAGE_URL\}\}/g, '') + .replace(/\{\{BEAD_CONTEXT\}\}/g, ''); + + // Clean up empty lines from unused placeholders + result = result.replace(/\n{3,}/g, '\n\n'); + + return result.trim(); +} + +// Load template on startup +const promptTemplate = loadPromptTemplate(); + // Global error handlers to prevent crashes process.on('uncaughtException', (error) => { console.error('Uncaught exception:', error); @@ -90,7 +213,9 @@ process.on('exit', unregisterServer); process.on('SIGINT', () => { unregisterServer(); process.exit(); }); process.on('SIGTERM', () => { unregisterServer(); process.exit(); }); -// Auto-submit feedback to the running Claude Code terminal using AppleScript +// Auto-submit feedback to the running Claude Code terminal +// On macOS: Uses AppleScript to type into Terminal/iTerm2 +// On Windows/Linux: Logs the message (terminal automation not supported) function autoSubmitToTerminal(feedback: string, selector: string, classes: string[], tag: string) { // Build element representation with full class names const classAttr = classes.length > 0 ? ` class="${classes.join(' ')}"` : ''; @@ -101,7 +226,15 @@ function autoSubmitToTerminal(feedback: string, selector: string, classes: strin console.error('📤 Submitting to Claude Code terminal...'); console.error(` Message: ${message}`); - // Use AppleScript to type into Terminal and press Enter + if (!isMac) { + // On Windows/Linux, terminal automation is not supported + // The message is queued and can be retrieved via get_visual_feedback MCP tool + console.error('ℹ️ Terminal auto-submit not available on this platform'); + console.error(' Use get_visual_feedback MCP tool to retrieve pending changes'); + return; + } + + // Use AppleScript to type into Terminal and press Enter (macOS only) const script = ` tell application "Terminal" activate @@ -144,7 +277,11 @@ function autoSubmitToTerminal(feedback: string, selector: string, classes: strin function autoApplyChanges(changeId: string, feedback: string, selector: string) { // Single line prompt to avoid shell escaping issues const prompt = `VISUAL FEEDBACK: "${feedback}" on element ${selector}. Use get_visual_feedback tool, find the source file, make the edit, then call mark_change_applied with changeId "${changeId}". Do not ask for confirmation.`; - const claudePath = process.env.HOME + '/.local/bin/claude'; + + // Platform-specific Claude path + const claudePath = isWindows + ? join(homedir(), 'AppData', 'Roaming', 'npm', 'claude.cmd') + : join(homedir(), '.local', 'bin', 'claude'); const workDir = process.cwd(); console.error('🔄 Spawning Claude to apply changes...'); @@ -154,10 +291,17 @@ function autoApplyChanges(changeId: string, feedback: string, selector: string) // Use spawn with args array to properly handle the prompt const { spawn } = require('child_process'); + + // Platform-specific environment + const spawnEnv = isWindows + ? { ...process.env } + : { ...process.env, PATH: process.env.PATH + ':/usr/local/bin:/opt/homebrew/bin' }; + const child = spawn(claudePath, ['-p', prompt, '--dangerously-skip-permissions'], { cwd: workDir, - env: { ...process.env, PATH: process.env.PATH + ':/usr/local/bin:/opt/homebrew/bin' }, + env: spawnEnv, stdio: ['ignore', 'pipe', 'pipe'], + shell: isWindows, }); child.stdout?.on('data', (data: Buffer) => { @@ -195,37 +339,41 @@ function autoApplyChanges(changeId: string, feedback: string, selector: string) }); } -// Types -interface VisualChange { - id: string; - element: { - selector: string; - tag: string; - id: string | null; - classes: string[]; - computedStyles: Record; - sourceHint: string | null; - smartSummary: string | null; - screenshot: string | null; - }; - feedback: string; - visualAdjustments: Record; - cssFramework: string; - originalUnits: Record; - timestamp: string; - status: 'draft' | 'staged' | 'confirmed' | 'applied' | 'failed'; -} - // Initialize change queue const changeQueue = new ChangeQueue(); -// Generate and display token on startup -const TOKEN = generateToken(); +// Load or generate token (persisted for consistent connections) +const TOKEN_FILE = join(REGISTRY_DIR, 'token'); + +function loadOrCreateToken(): string { + try { + mkdirSync(REGISTRY_DIR, { recursive: true }); + if (existsSync(TOKEN_FILE)) { + const savedToken = readFileSync(TOKEN_FILE, 'utf-8').trim(); + if (savedToken.length >= 16) { + return savedToken; + } + } + } catch { + // Fall through to generate new token + } + + const newToken = generateToken(); + try { + writeFileSync(TOKEN_FILE, newToken); + } catch (err) { + console.error('Warning: Could not save token:', err); + } + return newToken; +} + +const TOKEN = loadOrCreateToken(); console.error(`\n${'='.repeat(60)}`); console.error('Visual Feedback MCP Server Started'); console.error(`${'='.repeat(60)}`); console.error(`\nConnection Token: ${TOKEN}`); console.error('\nEnter this token in the Visual Feedback extension to connect.'); +console.error(`(Token is saved to ${TOKEN_FILE})`); console.error(`${'='.repeat(60)}\n`); // WebSocket server for extension communication @@ -288,11 +436,14 @@ function startWebSocketServer() { // Start WebSocket server startWebSocketServer(); -// HTTP server for discovery (serves list of available servers) -const httpServer = createServer((req, res) => { - // CORS headers for extension access +// SSE transport for MCP connections +let sseTransport: SSEServerTransport | null = null; + +// HTTP server for discovery and SSE MCP transport +const httpServer = createServer(async (req, res) => { + // CORS headers for extension and MCP client access res.setHeader('Access-Control-Allow-Origin', '*'); - res.setHeader('Access-Control-Allow-Methods', 'GET, OPTIONS'); + res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS'); res.setHeader('Access-Control-Allow-Headers', 'Content-Type'); if (req.method === 'OPTIONS') { @@ -301,7 +452,52 @@ const httpServer = createServer((req, res) => { return; } - if (req.url === '/servers' && req.method === 'GET') { + // SSE endpoint for MCP transport + if (req.url === '/sse' && req.method === 'GET') { + console.error('SSE connection request received'); + + // Create SSE transport - messages will be POSTed to /messages + sseTransport = new SSEServerTransport('/messages', res); + + // Create a new MCP server for this SSE connection + const sseServer = createMcpServer(); + + try { + await sseServer.connect(sseTransport); + console.error('MCP server connected via SSE transport'); + } catch (error) { + console.error('Failed to connect MCP server via SSE:', error); + } + + // Don't end the response - SSE keeps it open + return; + } + + // Messages endpoint for SSE transport + if (req.url?.startsWith('/messages') && req.method === 'POST') { + if (!sseTransport) { + res.writeHead(400, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ error: 'No SSE connection established' })); + return; + } + + try { + await sseTransport.handlePostMessage(req, res); + } catch (error) { + console.error('Error handling SSE message:', error); + if (!res.headersSent) { + res.writeHead(500, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ error: 'Failed to handle message' })); + } + } + return; + } + + if (req.url === '/status') { + // Status endpoint for extension compatibility + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ status: 'running', wsPort: 3847, requiresToken: true, sseEndpoint: '/sse' })); + } else if (req.url === '/servers' && req.method === 'GET') { try { let servers: Record = {}; if (existsSync(REGISTRY_FILE)) { @@ -325,6 +521,29 @@ const httpServer = createServer((req, res) => { res.writeHead(500, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: 'Failed to read servers' })); } + } else if (req.url === '/tasks' && req.method === 'GET') { + // Return pending visual feedback tasks for Claude Code hooks + try { + const pendingChanges = changeQueue.getPending(false); + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ + count: pendingChanges.length, + tasks: pendingChanges.map((change) => ({ + id: change.id, + feedback: change.feedback, + element: { + tag: change.element.tag, + selector: change.element.selector, + classes: change.element.classes, + }, + timestamp: change.timestamp, + status: change.status, + })), + })); + } catch (error) { + res.writeHead(500, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ error: 'Failed to get tasks' })); + } } else { res.writeHead(404); res.end('Not found'); @@ -333,6 +552,7 @@ const httpServer = createServer((req, res) => { httpServer.listen(3848, () => { console.error('Discovery server started on port 3848'); + console.error('SSE MCP endpoint available at http://localhost:3848/sse'); }); httpServer.on('error', (error: NodeJS.ErrnoException) => { @@ -342,55 +562,124 @@ httpServer.on('error', (error: NodeJS.ErrnoException) => { }); // Handle messages from extension -function handleExtensionMessage(message: { type: string; payload?: VisualChange }, ws: WebSocket) { - if (message.type === 'get_visual_feedback' && message.payload) { - changeQueue.add(message.payload); +function handleExtensionMessage(message: { type: string; payload?: any }, ws: WebSocket) { + console.error('Received:', message.type); + + // Handle keep-alive pings silently + if (message.type === 'ping') { + return; + } + + // Handle visual feedback from extension (matches standalone server) + if (message.type === 'visual_feedback' && message.payload) { + const { id, feedback, element, projectPath, pageUrl, model } = message.payload; + const taskId = id || Date.now().toString(); console.error(`\n${'═'.repeat(50)}`); console.error('📝 VISUAL FEEDBACK RECEIVED'); console.error(`${'═'.repeat(50)}`); - console.error(`Element: ${message.payload.element.selector}`); - console.error(`Feedback: "${message.payload.feedback}"`); + console.error(`Feedback: "${feedback}"`); + console.error(`Project: ${projectPath}`); + console.error(`Model: ${model}`); + console.error(`Element: <${element.tag}> ${element.selector || ''}`); + if (pageUrl) console.error(`Page: ${pageUrl}`); console.error(`${'═'.repeat(50)}\n`); - // Acknowledge receipt + // Build change object for queue + const change: VisualChange = { + id: taskId, + element: { + selector: element.selector, + tag: element.tag, + id: element.id || null, + classes: element.classes || [], + computedStyles: element.computedStyles || {}, + sourceHint: null, + smartSummary: null, + screenshot: element.screenshot || null, + }, + feedback, + visualAdjustments: {}, + cssFramework: '', + originalUnits: {}, + timestamp: new Date().toISOString(), + status: 'confirmed', + }; + + changeQueue.add(change); + + // Build task object for status updates + const task = { + id: taskId, + feedback, + element: { + tag: element.tag, + classes: element.classes || [], + selector: element.selector, + id: element.id + }, + projectPath, + pageUrl, + model, + status: 'queued', + startedAt: new Date().toISOString(), + completedAt: null, + log: 'Task queued. Use get_visual_feedback MCP tool to retrieve.', + exitCode: null, + commitHash: null, + commitUrl: null + }; + + // Send task_update so extension toast updates (matches standalone server) + console.error('Sending task_update with status:', task.status, 'for task:', task.id); + console.error('Full task object:', JSON.stringify(task, null, 2)); + const taskUpdateMsg = JSON.stringify({ type: 'task_update', task }); + console.error('Sending message:', taskUpdateMsg.substring(0, 200) + '...'); + ws.send(taskUpdateMsg); + + // Also send success response ws.send(JSON.stringify({ success: true, - changeId: message.payload.id, + taskId, })); - // Auto-submit to the running Claude Code terminal + // Auto-submit to the running Claude Code terminal (macOS only) setTimeout(() => { autoSubmitToTerminal( - message.payload!.feedback, - message.payload!.element.selector, - message.payload!.element.classes || [], - message.payload!.element.tag || 'div' + feedback, + element.selector, + element.classes || [], + element.tag || 'div' ); }, 500); + } else if (message.type === 'get_tasks') { + // Return queued tasks + const changes = changeQueue.getPending(true); + ws.send(JSON.stringify({ type: 'tasks', tasks: changes })); } } -// MCP Server -const server = new Server( - { - name: 'visual-feedback-mcp', - version: '0.1.0', - }, - { - capabilities: { - tools: {}, +// Factory function to create MCP server with all tool handlers +function createMcpServer(): Server { + const mcpServer = new Server( + { + name: 'visual-feedback-mcp', + version: '0.1.0', }, - } -); - -// Register tools -server.setRequestHandler(ListToolsRequestSchema, async () => { - return { - tools: [ - { - name: 'get_visual_feedback', - description: `Get pending visual feedback from the browser extension. + { + capabilities: { + tools: {}, + }, + } + ); + + // Register tools + mcpServer.setRequestHandler(ListToolsRequestSchema, async () => { + return { + tools: [ + { + name: 'get_visual_feedback', + description: `Get pending visual feedback from the browser extension. Returns a list of visual changes made by the user including: - Element information (selector, tag, classes, computed styles) - User's text feedback describing what they want changed @@ -399,127 +688,159 @@ Returns a list of visual changes made by the user including: - Detected CSS framework (Tailwind, CSS Modules, etc.) Call this tool when you want to see what visual changes the user has requested.`, - inputSchema: { - type: 'object', - properties: { - includeApplied: { - type: 'boolean', - description: 'Include already applied changes in the response', - default: false, + inputSchema: { + type: 'object', + properties: { + includeApplied: { + type: 'boolean', + description: 'Include already applied changes in the response', + default: false, + }, }, }, }, - }, - { - name: 'mark_change_applied', - description: `Mark a visual change as successfully applied. + { + name: 'mark_change_applied', + description: `Mark a visual change as successfully applied. Call this after you have made the code changes to implement the user's visual feedback.`, - inputSchema: { - type: 'object', - properties: { - changeId: { - type: 'string', - description: 'The ID of the change to mark as applied', + inputSchema: { + type: 'object', + properties: { + changeId: { + type: 'string', + description: 'The ID of the change to mark as applied', + }, }, + required: ['changeId'], }, - required: ['changeId'], }, - }, - { - name: 'mark_change_failed', - description: `Mark a visual change as failed to apply. + { + name: 'mark_change_failed', + description: `Mark a visual change as failed to apply. Call this if you were unable to implement the user's visual feedback.`, - inputSchema: { - type: 'object', - properties: { - changeId: { - type: 'string', - description: 'The ID of the change to mark as failed', - }, - reason: { - type: 'string', - description: 'Reason for the failure', + inputSchema: { + type: 'object', + properties: { + changeId: { + type: 'string', + description: 'The ID of the change to mark as failed', + }, + reason: { + type: 'string', + description: 'Reason for the failure', + }, }, + required: ['changeId'], }, - required: ['changeId'], }, - }, - { - name: 'get_change_details', - description: `Get detailed information about a specific visual change.`, - inputSchema: { - type: 'object', - properties: { - changeId: { - type: 'string', - description: 'The ID of the change to get details for', + { + name: 'get_change_details', + description: `Get detailed information about a specific visual change.`, + inputSchema: { + type: 'object', + properties: { + changeId: { + type: 'string', + description: 'The ID of the change to get details for', + }, }, + required: ['changeId'], }, - required: ['changeId'], }, - }, - ], - }; -}); + { + name: 'clear_all_tasks', + description: `Clear all visual feedback tasks from the queue. +Use this to reset the queue when you want to start fresh.`, + inputSchema: { + type: 'object', + properties: {}, + }, + }, + ], + }; + }); -// Handle tool calls -server.setRequestHandler(CallToolRequestSchema, async (request) => { - const { name, arguments: args } = request.params; + // Handle tool calls + mcpServer.setRequestHandler(CallToolRequestSchema, async (request) => { + const { name, arguments: args } = request.params; + + switch (name) { + case 'get_visual_feedback': { + const includeApplied = (args as { includeApplied?: boolean })?.includeApplied ?? false; + const changes = changeQueue.getPending(includeApplied); + + if (changes.length === 0) { + return { + content: [ + { + type: 'text', + text: 'No pending visual feedback. The user has not made any visual changes in the extension yet.', + }, + ], + }; + } - switch (name) { - case 'get_visual_feedback': { - const includeApplied = (args as { includeApplied?: boolean })?.includeApplied ?? false; - const changes = changeQueue.getPending(includeApplied); + // Format each change using the prompt template (if available) + const formattedChanges = changes.map((change) => formatChangeWithTemplate(change, promptTemplate)); - if (changes.length === 0) { return { content: [ { type: 'text', - text: 'No pending visual feedback. The user has not made any visual changes in the extension yet.', + text: `Found ${changes.length} pending visual change(s):\n\n${formattedChanges.join('\n\n---\n\n')}`, }, ], }; } - const formattedChanges = changes.map((change) => ({ - id: change.id, - element: { - selector: change.element.selector, - tag: change.element.tag, - classes: change.element.classes, - currentStyles: change.element.computedStyles, - sourceFile: change.element.sourceHint, - description: change.element.smartSummary, - }, - userFeedback: change.feedback, - visualChanges: change.visualAdjustments, - cssFramework: change.cssFramework, - timestamp: change.timestamp, - status: change.status, - })); + case 'mark_change_applied': { + const { changeId } = args as { changeId: string }; + const success = changeQueue.markApplied(changeId); - return { - content: [ - { - type: 'text', - text: `Found ${changes.length} pending visual change(s):\n\n${JSON.stringify(formattedChanges, null, 2)}`, - }, - ], - }; - } + if (success) { + // Notify extension + try { + if (connectedClient?.readyState === WebSocket.OPEN) { + connectedClient.send(JSON.stringify({ + type: 'CHANGE_APPLIED', + changeId, + })); + } + } catch (e) { + console.error('Failed to notify extension:', e); + } + + return { + content: [ + { + type: 'text', + text: `Change ${changeId} marked as applied.`, + }, + ], + }; + } - case 'mark_change_applied': { - const { changeId } = args as { changeId: string }; - const success = changeQueue.markApplied(changeId); + return { + content: [ + { + type: 'text', + text: `Change ${changeId} not found.`, + }, + ], + }; + } - if (success) { - // Notify extension + case 'mark_change_failed': { + const { changeId, reason } = args as { changeId: string; reason?: string }; + const success = changeQueue.markFailed(changeId, reason); + + // Notify extension for auto-retry try { if (connectedClient?.readyState === WebSocket.OPEN) { connectedClient.send(JSON.stringify({ - type: 'CHANGE_APPLIED', + type: 'CHANGE_FAILED', changeId, + reason, })); } } catch (e) { @@ -530,91 +851,80 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => { content: [ { type: 'text', - text: `Change ${changeId} marked as applied.`, + text: success + ? `Change ${changeId} marked as failed. The extension may auto-retry.` + : `Change ${changeId} not found.`, }, ], }; } - return { - content: [ - { - type: 'text', - text: `Change ${changeId} not found.`, - }, - ], - }; - } - - case 'mark_change_failed': { - const { changeId, reason } = args as { changeId: string; reason?: string }; - const success = changeQueue.markFailed(changeId, reason); - - // Notify extension for auto-retry - try { - if (connectedClient?.readyState === WebSocket.OPEN) { - connectedClient.send(JSON.stringify({ - type: 'CHANGE_FAILED', - changeId, - reason, - })); + case 'get_change_details': { + const { changeId } = args as { changeId: string }; + const change = changeQueue.get(changeId); + + if (!change) { + return { + content: [ + { + type: 'text', + text: `Change ${changeId} not found.`, + }, + ], + }; } - } catch (e) { - console.error('Failed to notify extension:', e); - } - return { - content: [ - { - type: 'text', - text: success - ? `Change ${changeId} marked as failed. The extension may auto-retry.` - : `Change ${changeId} not found.`, - }, - ], - }; - } + return { + content: [ + { + type: 'text', + text: `Change details:\n\n${JSON.stringify(change, null, 2)}`, + }, + ], + }; + } - case 'get_change_details': { - const { changeId } = args as { changeId: string }; - const change = changeQueue.get(changeId); + case 'clear_all_tasks': { + const counts = changeQueue.getStatusCounts(); + const total = Object.values(counts).reduce((a, b) => a + b, 0); + changeQueue.clear(); - if (!change) { return { content: [ { type: 'text', - text: `Change ${changeId} not found.`, + text: `Cleared ${total} task(s) from the queue.`, }, ], }; } - return { - content: [ - { - type: 'text', - text: `Change details:\n\n${JSON.stringify(change, null, 2)}`, - }, - ], - }; + default: + return { + content: [ + { + type: 'text', + text: `Unknown tool: ${name}`, + }, + ], + isError: true, + }; } + }); - default: - return { - content: [ - { - type: 'text', - text: `Unknown tool: ${name}`, - }, - ], - isError: true, - }; - } -}); + return mcpServer; +} -// Start the MCP server +// Start the MCP server with stdio transport (for Claude Code subprocess mode) async function main() { + // Check if we should skip stdio (when running standalone for SSE only) + if (process.env.SSE_ONLY === 'true') { + console.error('Running in SSE-only mode (stdio disabled)'); + console.error('Connect via: http://localhost:3848/sse'); + return; + } + + const server = createMcpServer(); const transport = new StdioServerTransport(); await server.connect(transport); console.error('MCP server running on stdio'); diff --git a/server/package-lock.json b/server/package-lock.json new file mode 100644 index 0000000..0922f01 --- /dev/null +++ b/server/package-lock.json @@ -0,0 +1,36 @@ +{ + "name": "visual-feedback-server", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "visual-feedback-server", + "version": "1.0.0", + "dependencies": { + "ws": "^8.14.0" + } + }, + "node_modules/ws": { + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz", + "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + } + } +} diff --git a/server/prompt-template.md b/server/prompt-template.md new file mode 100644 index 0000000..313d16e --- /dev/null +++ b/server/prompt-template.md @@ -0,0 +1,30 @@ +# Visual Feedback Request + +**Task ID:** `{{TASK_ID}}` + +## User Feedback +"{{FEEDBACK}}" + +## Target Element +- **Tag:** <{{ELEMENT_TAG}}> +- **Selector:** {{SELECTOR}} +{{ELEMENT_ID}} +{{ELEMENT_CLASSES}} +{{DOM_PATH}} + +{{COMPUTED_STYLES}} + +{{PAGE_URL}} + +{{BEAD_CONTEXT}} + +## Instructions +1. Use Language Server Protocol (LSP) features to efficiently navigate the codebase: + - Use "Go to Definition" to find where components/elements are defined + - Use "Find References" to locate all usages + - Use symbol search to quickly find relevant files +2. Find the source file containing this element using the selector, classes, and DOM path as hints +3. Make the requested change +4. **Important:** After attempting the change, report the result: + - If successful: call `mark_change_applied` with the Task ID + - If failed: call `mark_change_failed` with the Task ID and reason for failure diff --git a/server/server.js b/server/server.js index 7e5fc6b..f197829 100644 --- a/server/server.js +++ b/server/server.js @@ -9,15 +9,26 @@ const os = require('os'); const WS_PORT = 3847; const HTTP_PORT = 3848; -const CLAUDE_PATH = os.homedir() + '/.local/bin/claude'; +const isWindows = os.platform() === 'win32'; +const CLAUDE_PATH = isWindows + ? path.join(os.homedir(), 'AppData', 'Roaming', 'npm', 'claude.cmd') + : path.join(os.homedir(), '.local', 'bin', 'claude'); const SCREENSHOT_DIR = path.join(os.tmpdir(), 'visual-feedback-screenshots'); const TASKS_FILE = path.join(os.homedir(), '.visual-feedback-server', 'tasks.json'); +const PROMPT_TEMPLATE_PATH = path.join(__dirname, 'prompt-template.md'); // Ensure screenshot directory exists if (!fs.existsSync(SCREENSHOT_DIR)) { fs.mkdirSync(SCREENSHOT_DIR, { recursive: true }); } +// Ensure tasks directory exists +const TASKS_DIR = path.dirname(TASKS_FILE); +if (!fs.existsSync(TASKS_DIR)) { + console.log(`Creating tasks directory: ${TASKS_DIR}`); + fs.mkdirSync(TASKS_DIR, { recursive: true }); +} + // Beads-style element memory system // Stores context about previous changes to elements for continuity @@ -127,6 +138,20 @@ function formatBeadContext(bead) { console.log('Starting Visual Feedback Server...'); +// Load and validate prompt template at startup +let promptTemplateSource = 'built-in fallback'; +if (fs.existsSync(PROMPT_TEMPLATE_PATH)) { + promptTemplateSource = PROMPT_TEMPLATE_PATH; + const template = fs.readFileSync(PROMPT_TEMPLATE_PATH, 'utf8'); + const lineCount = template.split('\n').length; + console.log(`Prompt template: ${PROMPT_TEMPLATE_PATH} (${lineCount} lines)`); + console.log('--- Template ---'); + console.log(template); + console.log('--- End Template ---\n'); +} else { + console.log('Prompt template: using built-in fallback (no prompt-template.txt found)'); +} + // Task storage with file persistence const tasks = new Map(); const MAX_TASKS = 50; @@ -186,72 +211,96 @@ function broadcastTaskUpdate(task) { console.log(`[Broadcast] Complete: ${sentCount}/${clientCount} clients received message`); } -// Build rich prompt with all context -function buildPrompt(feedback, element, pageUrl, beadContext) { - const lines = [ - `# Visual Feedback Request`, - ``, - `## User Feedback`, - `"${feedback}"`, - ``, - `## Target Element`, - `- **Tag:** <${element.tag}>`, - `- **Selector:** ${element.selector || 'N/A'}`, - ]; +// Load prompt template from file +function loadPromptTemplate() { + try { + if (fs.existsSync(PROMPT_TEMPLATE_PATH)) { + return fs.readFileSync(PROMPT_TEMPLATE_PATH, 'utf8'); + } + } catch (err) { + console.error('Failed to load prompt template:', err.message); + } + // Fallback template if file doesn't exist + return `# Visual Feedback Request +## User Feedback +"{{FEEDBACK}}" + +## Target Element +- **Tag:** <{{ELEMENT_TAG}}> +- **Selector:** {{SELECTOR}} +{{ELEMENT_ID}} +{{ELEMENT_CLASSES}} +{{DOM_PATH}} + +{{COMPUTED_STYLES}} + +{{PAGE_URL}} + +{{BEAD_CONTEXT}} + +## Instructions +1. Find the source file containing this element +2. Make the requested change`; +} + +// Build rich prompt with all context using template +function buildPrompt(feedback, element, pageUrl, beadContext, taskId) { + let template = loadPromptTemplate(); + + // Build optional sections + let elementId = ''; if (element.id) { - lines.push(`- **ID:** #${element.id}`); + elementId = `- **ID:** #${element.id}`; } + let elementClasses = ''; if (element.classes && element.classes.length > 0) { - lines.push(`- **Classes:** .${element.classes.join(', .')}`); + elementClasses = `- **Classes:** .${element.classes.join(', .')}`; } - // Add element path (breadcrumb) + let domPath = ''; if (element.path && element.path.length > 0) { const pathStr = element.path.map(p => p.selector || p.tag).join(' > '); - lines.push(`- **DOM Path:** ${pathStr}`); + domPath = `- **DOM Path:** ${pathStr}`; } - // Add computed styles if available + let computedStyles = ''; if (element.computedStyles) { const styles = element.computedStyles; - lines.push(``, `## Current Styles`); - if (styles.width) lines.push(`- Width: ${styles.width}`); - if (styles.height) lines.push(`- Height: ${styles.height}`); - if (styles.backgroundColor) lines.push(`- Background: ${styles.backgroundColor}`); - if (styles.color) lines.push(`- Text Color: ${styles.color}`); - if (styles.fontSize) lines.push(`- Font Size: ${styles.fontSize}`); - if (styles.display) lines.push(`- Display: ${styles.display}`); - if (styles.position) lines.push(`- Position: ${styles.position}`); + const styleLines = ['## Current Styles']; + if (styles.width) styleLines.push(`- Width: ${styles.width}`); + if (styles.height) styleLines.push(`- Height: ${styles.height}`); + if (styles.backgroundColor) styleLines.push(`- Background: ${styles.backgroundColor}`); + if (styles.color) styleLines.push(`- Text Color: ${styles.color}`); + if (styles.fontSize) styleLines.push(`- Font Size: ${styles.fontSize}`); + if (styles.display) styleLines.push(`- Display: ${styles.display}`); + if (styles.position) styleLines.push(`- Position: ${styles.position}`); + computedStyles = styleLines.join('\n'); } + let pageUrlSection = ''; if (pageUrl) { - lines.push(``, `## Page URL`, pageUrl); + pageUrlSection = `## Page URL\n${pageUrl}`; } - // Add bead context if available (previous changes to this element) - if (beadContext) { - lines.push(``, beadContext); - } - - lines.push( - ``, - `## Instructions`, - `1. Use Language Server Protocol (LSP) features to efficiently navigate the codebase:`, - ` - Use "Go to Definition" to find where components/elements are defined`, - ` - Use "Find References" to locate all usages`, - ` - Use symbol search to quickly find relevant files`, - `2. Find the source file containing this element using the selector, classes, and DOM path as hints`, - `3. Make the requested change`, - `4. Commit the change with a descriptive message`, - `5. Push to GitHub`, - `6. **IMPORTANT**: After pushing, output the commit hash in this exact format:`, - ` COMMIT_HASH: `, - ` This is required for tracking purposes.` - ); - - return lines.join('\n'); + // Replace placeholders + template = template + .replace(/\{\{TASK_ID\}\}/g, taskId || 'unknown') + .replace(/\{\{FEEDBACK\}\}/g, feedback) + .replace(/\{\{ELEMENT_TAG\}\}/g, element.tag) + .replace(/\{\{SELECTOR\}\}/g, element.selector || 'N/A') + .replace(/\{\{ELEMENT_ID\}\}/g, elementId) + .replace(/\{\{ELEMENT_CLASSES\}\}/g, elementClasses) + .replace(/\{\{DOM_PATH\}\}/g, domPath) + .replace(/\{\{COMPUTED_STYLES\}\}/g, computedStyles) + .replace(/\{\{PAGE_URL\}\}/g, pageUrlSection) + .replace(/\{\{BEAD_CONTEXT\}\}/g, beadContext || ''); + + // Clean up empty lines from unused placeholders + template = template.replace(/\n{3,}/g, '\n\n'); + + return template.trim(); } // Save screenshot and return path @@ -284,7 +333,7 @@ const httpServer = createServer((req, res) => { if (req.url === '/status') { res.writeHead(200, { 'Content-Type': 'application/json' }); - res.end(JSON.stringify({ status: 'running', wsPort: WS_PORT })); + res.end(JSON.stringify({ status: 'running', wsPort: WS_PORT, requiresToken: false })); } else if (req.url === '/tasks') { const taskList = Array.from(tasks.values()).reverse(); res.writeHead(200, { 'Content-Type': 'application/json' }); @@ -305,12 +354,30 @@ const httpServer = createServer((req, res) => { } }); +// WebSocket server +const wss = new WebSocketServer({ port: WS_PORT }); + +// Start both servers and print Ready when both are listening +let httpReady = false; +let wsReady = false; + +function checkReady() { + if (httpReady && wsReady) { + console.log('Ready!\n'); + } +} + httpServer.listen(HTTP_PORT, () => { console.log(`HTTP server on port ${HTTP_PORT}`); + httpReady = true; + checkReady(); }); -// WebSocket server -const wss = new WebSocketServer({ port: WS_PORT }); +wss.on('listening', () => { + console.log(`WebSocket server on port ${WS_PORT}`); + wsReady = true; + checkReady(); +}); wss.on('connection', (ws) => { console.log('Client connected'); @@ -358,7 +425,9 @@ wss.on('connection', (ws) => { commitUrl: null }; addTask(task); - broadcastTaskUpdate(task); + + // Send 'queued' status immediately to dismiss working toast + broadcastTaskUpdate({ ...task, status: 'queued' }); // Load bead context for this element (previous changes) const bead = loadElementBead(projectPath, element); @@ -368,7 +437,7 @@ wss.on('connection', (ws) => { } // Build rich prompt - const prompt = buildPrompt(feedback, element, pageUrl, beadContext); + const prompt = buildPrompt(feedback, element, pageUrl, beadContext, taskId); console.log('\n--- Prompt ---'); console.log(prompt); @@ -383,16 +452,24 @@ wss.on('connection', (ws) => { '--dangerously-skip-permissions' ]; + const spawnEnv = isWindows + ? { + ...process.env, + ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY + } + : { + HOME: os.homedir(), + PATH: '/usr/local/bin:/usr/bin:/bin:/opt/homebrew/bin:' + os.homedir() + '/.local/bin', + USER: process.env.USER, + TERM: 'xterm-256color', + ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY + }; + const child = spawn(CLAUDE_PATH, args, { cwd: projectPath, - env: { - HOME: os.homedir(), - PATH: '/usr/local/bin:/usr/bin:/bin:/opt/homebrew/bin:' + os.homedir() + '/.local/bin', - USER: process.env.USER, - TERM: 'xterm-256color', - ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY - }, - stdio: ['ignore', 'pipe', 'pipe'] + env: spawnEnv, + stdio: ['ignore', 'pipe', 'pipe'], + shell: isWindows }); child.stdout.on('data', (d) => { @@ -462,6 +539,3 @@ wss.on('connection', (ws) => { ws.on('close', () => console.log('Client disconnected')); ws.send(JSON.stringify({ type: 'ready' })); }); - -console.log(`WebSocket server on port ${WS_PORT}`); -console.log('Ready!\n');
Server not running
Run: launchctl start com.visualfeedback.server