#
tokens: 48022/50000 12/120 files (page 3/10)
lines: on (toggle) GitHub
raw markdown copy reset
This is page 3 of 10. Use http://codebase.md/hangwin/mcp-chrome?lines=true&page={x} to view the full context.

# Directory Structure

```
├── .gitattributes
├── .github
│   └── workflows
│       └── build-release.yml
├── .gitignore
├── .husky
│   ├── commit-msg
│   └── pre-commit
├── .prettierignore
├── .prettierrc.json
├── .vscode
│   └── extensions.json
├── app
│   ├── chrome-extension
│   │   ├── _locales
│   │   │   ├── de
│   │   │   │   └── messages.json
│   │   │   ├── en
│   │   │   │   └── messages.json
│   │   │   ├── ja
│   │   │   │   └── messages.json
│   │   │   ├── ko
│   │   │   │   └── messages.json
│   │   │   ├── zh_CN
│   │   │   │   └── messages.json
│   │   │   └── zh_TW
│   │   │       └── messages.json
│   │   ├── .env.example
│   │   ├── assets
│   │   │   └── vue.svg
│   │   ├── common
│   │   │   ├── constants.ts
│   │   │   ├── message-types.ts
│   │   │   └── tool-handler.ts
│   │   ├── entrypoints
│   │   │   ├── background
│   │   │   │   ├── index.ts
│   │   │   │   ├── native-host.ts
│   │   │   │   ├── semantic-similarity.ts
│   │   │   │   ├── storage-manager.ts
│   │   │   │   └── tools
│   │   │   │       ├── base-browser.ts
│   │   │   │       ├── browser
│   │   │   │       │   ├── bookmark.ts
│   │   │   │       │   ├── common.ts
│   │   │   │       │   ├── console.ts
│   │   │   │       │   ├── file-upload.ts
│   │   │   │       │   ├── history.ts
│   │   │   │       │   ├── index.ts
│   │   │   │       │   ├── inject-script.ts
│   │   │   │       │   ├── interaction.ts
│   │   │   │       │   ├── keyboard.ts
│   │   │   │       │   ├── network-capture-debugger.ts
│   │   │   │       │   ├── network-capture-web-request.ts
│   │   │   │       │   ├── network-request.ts
│   │   │   │       │   ├── screenshot.ts
│   │   │   │       │   ├── vector-search.ts
│   │   │   │       │   ├── web-fetcher.ts
│   │   │   │       │   └── window.ts
│   │   │   │       └── index.ts
│   │   │   ├── content.ts
│   │   │   ├── offscreen
│   │   │   │   ├── index.html
│   │   │   │   └── main.ts
│   │   │   └── popup
│   │   │       ├── App.vue
│   │   │       ├── components
│   │   │       │   ├── ConfirmDialog.vue
│   │   │       │   ├── icons
│   │   │       │   │   ├── BoltIcon.vue
│   │   │       │   │   ├── CheckIcon.vue
│   │   │       │   │   ├── DatabaseIcon.vue
│   │   │       │   │   ├── DocumentIcon.vue
│   │   │       │   │   ├── index.ts
│   │   │       │   │   ├── TabIcon.vue
│   │   │       │   │   ├── TrashIcon.vue
│   │   │       │   │   └── VectorIcon.vue
│   │   │       │   ├── ModelCacheManagement.vue
│   │   │       │   └── ProgressIndicator.vue
│   │   │       ├── index.html
│   │   │       ├── main.ts
│   │   │       └── style.css
│   │   ├── eslint.config.js
│   │   ├── inject-scripts
│   │   │   ├── click-helper.js
│   │   │   ├── fill-helper.js
│   │   │   ├── inject-bridge.js
│   │   │   ├── interactive-elements-helper.js
│   │   │   ├── keyboard-helper.js
│   │   │   ├── network-helper.js
│   │   │   ├── screenshot-helper.js
│   │   │   └── web-fetcher-helper.js
│   │   ├── LICENSE
│   │   ├── package.json
│   │   ├── public
│   │   │   ├── icon
│   │   │   │   ├── 128.png
│   │   │   │   ├── 16.png
│   │   │   │   ├── 32.png
│   │   │   │   ├── 48.png
│   │   │   │   └── 96.png
│   │   │   ├── libs
│   │   │   │   └── ort.min.js
│   │   │   └── wxt.svg
│   │   ├── README.md
│   │   ├── tsconfig.json
│   │   ├── utils
│   │   │   ├── content-indexer.ts
│   │   │   ├── i18n.ts
│   │   │   ├── image-utils.ts
│   │   │   ├── lru-cache.ts
│   │   │   ├── model-cache-manager.ts
│   │   │   ├── offscreen-manager.ts
│   │   │   ├── semantic-similarity-engine.ts
│   │   │   ├── simd-math-engine.ts
│   │   │   ├── text-chunker.ts
│   │   │   └── vector-database.ts
│   │   ├── workers
│   │   │   ├── ort-wasm-simd-threaded.jsep.mjs
│   │   │   ├── ort-wasm-simd-threaded.jsep.wasm
│   │   │   ├── ort-wasm-simd-threaded.mjs
│   │   │   ├── ort-wasm-simd-threaded.wasm
│   │   │   ├── simd_math_bg.wasm
│   │   │   ├── simd_math.js
│   │   │   └── similarity.worker.js
│   │   └── wxt.config.ts
│   └── native-server
│       ├── debug.sh
│       ├── install.md
│       ├── jest.config.js
│       ├── package.json
│       ├── README.md
│       ├── src
│       │   ├── cli.ts
│       │   ├── constant
│       │   │   └── index.ts
│       │   ├── file-handler.ts
│       │   ├── index.ts
│       │   ├── mcp
│       │   │   ├── mcp-server-stdio.ts
│       │   │   ├── mcp-server.ts
│       │   │   ├── register-tools.ts
│       │   │   └── stdio-config.json
│       │   ├── native-messaging-host.ts
│       │   ├── scripts
│       │   │   ├── browser-config.ts
│       │   │   ├── build.ts
│       │   │   ├── constant.ts
│       │   │   ├── postinstall.ts
│       │   │   ├── register-dev.ts
│       │   │   ├── register.ts
│       │   │   ├── run_host.bat
│       │   │   ├── run_host.sh
│       │   │   └── utils.ts
│       │   ├── server
│       │   │   ├── index.ts
│       │   │   └── server.test.ts
│       │   └── util
│       │       └── logger.ts
│       └── tsconfig.json
├── commitlint.config.cjs
├── docs
│   ├── ARCHITECTURE_zh.md
│   ├── ARCHITECTURE.md
│   ├── CHANGELOG.md
│   ├── CONTRIBUTING_zh.md
│   ├── CONTRIBUTING.md
│   ├── TOOLS_zh.md
│   ├── TOOLS.md
│   ├── TROUBLESHOOTING_zh.md
│   ├── TROUBLESHOOTING.md
│   └── WINDOWS_INSTALL_zh.md
├── eslint.config.js
├── LICENSE
├── package.json
├── packages
│   ├── shared
│   │   ├── package.json
│   │   ├── src
│   │   │   ├── constants.ts
│   │   │   ├── index.ts
│   │   │   ├── tools.ts
│   │   │   └── types.ts
│   │   └── tsconfig.json
│   └── wasm-simd
│       ├── .gitignore
│       ├── BUILD.md
│       ├── Cargo.toml
│       ├── package.json
│       ├── README.md
│       └── src
│           └── lib.rs
├── pnpm-lock.yaml
├── pnpm-workspace.yaml
├── prompt
│   ├── content-analize.md
│   ├── excalidraw-prompt.md
│   └── modify-web.md
├── README_zh.md
├── README.md
├── releases
│   ├── chrome-extension
│   │   └── latest
│   │       └── chrome-mcp-server-lastest.zip
│   └── README.md
└── test-inject-script.js
```

# Files

--------------------------------------------------------------------------------
/app/native-server/src/server/index.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import Fastify, { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify';
  2 | import cors from '@fastify/cors';
  3 | import {
  4 |   NATIVE_SERVER_PORT,
  5 |   TIMEOUTS,
  6 |   SERVER_CONFIG,
  7 |   HTTP_STATUS,
  8 |   ERROR_MESSAGES,
  9 | } from '../constant';
 10 | import { NativeMessagingHost } from '../native-messaging-host';
 11 | import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js';
 12 | import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
 13 | import { randomUUID } from 'node:crypto';
 14 | import { isInitializeRequest } from '@modelcontextprotocol/sdk/types.js';
 15 | import { getMcpServer } from '../mcp/mcp-server';
 16 | 
 17 | // Define request body type (if data needs to be retrieved from HTTP requests)
 18 | interface ExtensionRequestPayload {
 19 |   data?: any; // Data you want to pass to the extension
 20 | }
 21 | 
 22 | export class Server {
 23 |   private fastify: FastifyInstance;
 24 |   public isRunning = false; // Changed to public or provide a getter
 25 |   private nativeHost: NativeMessagingHost | null = null;
 26 |   private transportsMap: Map<string, StreamableHTTPServerTransport | SSEServerTransport> =
 27 |     new Map();
 28 | 
 29 |   constructor() {
 30 |     this.fastify = Fastify({ logger: SERVER_CONFIG.LOGGER_ENABLED });
 31 |     this.setupPlugins();
 32 |     this.setupRoutes();
 33 |   }
 34 |   /**
 35 |    * Associate NativeMessagingHost instance
 36 |    */
 37 |   public setNativeHost(nativeHost: NativeMessagingHost): void {
 38 |     this.nativeHost = nativeHost;
 39 |   }
 40 | 
 41 |   private async setupPlugins(): Promise<void> {
 42 |     await this.fastify.register(cors, {
 43 |       origin: SERVER_CONFIG.CORS_ORIGIN,
 44 |     });
 45 |   }
 46 | 
 47 |   private setupRoutes(): void {
 48 |     // for ping
 49 |     this.fastify.get(
 50 |       '/ask-extension',
 51 |       async (request: FastifyRequest<{ Body: ExtensionRequestPayload }>, reply: FastifyReply) => {
 52 | 
 53 |         if (!this.nativeHost) {
 54 |           return reply
 55 |             .status(HTTP_STATUS.INTERNAL_SERVER_ERROR)
 56 |             .send({ error: ERROR_MESSAGES.NATIVE_HOST_NOT_AVAILABLE });
 57 |         }
 58 |         if (!this.isRunning) {
 59 |           return reply
 60 |             .status(HTTP_STATUS.INTERNAL_SERVER_ERROR)
 61 |             .send({ error: ERROR_MESSAGES.SERVER_NOT_RUNNING });
 62 |         }
 63 | 
 64 |         try {
 65 |           // wait from extension message
 66 |           const extensionResponse = await this.nativeHost.sendRequestToExtensionAndWait(
 67 |             request.query,
 68 |             'process_data',
 69 |             TIMEOUTS.EXTENSION_REQUEST_TIMEOUT,
 70 |           );
 71 |           return reply.status(HTTP_STATUS.OK).send({ status: 'success', data: extensionResponse });
 72 |         } catch (error: any) {
 73 |           if (error.message.includes('timed out')) {
 74 |             return reply
 75 |               .status(HTTP_STATUS.GATEWAY_TIMEOUT)
 76 |               .send({ status: 'error', message: ERROR_MESSAGES.REQUEST_TIMEOUT });
 77 |           } else {
 78 |             return reply.status(HTTP_STATUS.INTERNAL_SERVER_ERROR).send({
 79 |               status: 'error',
 80 |               message: `Failed to get response from extension: ${error.message}`,
 81 |             });
 82 |           }
 83 |         }
 84 |       },
 85 |     );
 86 | 
 87 |     // Compatible with SSE
 88 |     this.fastify.get('/sse', async (_, reply) => {
 89 |       try {
 90 |         // Set SSE headers
 91 |         reply.raw.writeHead(HTTP_STATUS.OK, {
 92 |           'Content-Type': 'text/event-stream',
 93 |           'Cache-Control': 'no-cache',
 94 |           Connection: 'keep-alive',
 95 |         });
 96 | 
 97 |         // Create SSE transport
 98 |         const transport = new SSEServerTransport('/messages', reply.raw);
 99 |         this.transportsMap.set(transport.sessionId, transport);
100 | 
101 |         reply.raw.on('close', () => {
102 |           this.transportsMap.delete(transport.sessionId);
103 |         });
104 | 
105 |         const server = getMcpServer();
106 |         await server.connect(transport);
107 | 
108 |         // Keep connection open
109 |         reply.raw.write(':\n\n');
110 |       } catch (error) {
111 |         if (!reply.sent) {
112 |           reply.code(HTTP_STATUS.INTERNAL_SERVER_ERROR).send(ERROR_MESSAGES.INTERNAL_SERVER_ERROR);
113 |         }
114 |       }
115 |     });
116 | 
117 |     // Compatible with SSE
118 |     this.fastify.post('/messages', async (req, reply) => {
119 |       try {
120 |         const { sessionId } = req.query as any;
121 |         const transport = this.transportsMap.get(sessionId) as SSEServerTransport;
122 |         if (!sessionId || !transport) {
123 |           reply.code(HTTP_STATUS.BAD_REQUEST).send('No transport found for sessionId');
124 |           return;
125 |         }
126 | 
127 |         await transport.handlePostMessage(req.raw, reply.raw, req.body);
128 |       } catch (error) {
129 |         if (!reply.sent) {
130 |           reply.code(HTTP_STATUS.INTERNAL_SERVER_ERROR).send(ERROR_MESSAGES.INTERNAL_SERVER_ERROR);
131 |         }
132 |       }
133 |     });
134 | 
135 |     // POST /mcp: Handle client-to-server messages
136 |     this.fastify.post('/mcp', async (request, reply) => {
137 |       const sessionId = request.headers['mcp-session-id'] as string | undefined;
138 |       let transport: StreamableHTTPServerTransport | undefined = this.transportsMap.get(
139 |         sessionId || '',
140 |       ) as StreamableHTTPServerTransport;
141 |       if (transport) {
142 |         // transport found, do nothing
143 |       } else if (!sessionId && isInitializeRequest(request.body)) {
144 |         const newSessionId = randomUUID(); // Generate session ID
145 |         transport = new StreamableHTTPServerTransport({
146 |           sessionIdGenerator: () => newSessionId, // Use pre-generated ID
147 |           onsessioninitialized: (initializedSessionId) => {
148 |             // Ensure transport instance exists and session ID matches
149 |             if (transport && initializedSessionId === newSessionId) {
150 |               this.transportsMap.set(initializedSessionId, transport);
151 |             }
152 |           },
153 |         });
154 | 
155 |         transport.onclose = () => {
156 |           if (transport?.sessionId && this.transportsMap.get(transport.sessionId)) {
157 |             this.transportsMap.delete(transport.sessionId);
158 |           }
159 |         };
160 |         await getMcpServer().connect(transport);
161 |       } else {
162 |         reply.code(HTTP_STATUS.BAD_REQUEST).send({ error: ERROR_MESSAGES.INVALID_MCP_REQUEST });
163 |         return;
164 |       }
165 | 
166 |       try {
167 |         await transport.handleRequest(request.raw, reply.raw, request.body);
168 |       } catch (error) {
169 |         if (!reply.sent) {
170 |           reply
171 |             .code(HTTP_STATUS.INTERNAL_SERVER_ERROR)
172 |             .send({ error: ERROR_MESSAGES.MCP_REQUEST_PROCESSING_ERROR });
173 |         }
174 |       }
175 |     });
176 | 
177 |     this.fastify.get('/mcp', async (request, reply) => {
178 |       const sessionId = request.headers['mcp-session-id'] as string | undefined;
179 |       const transport = sessionId
180 |         ? (this.transportsMap.get(sessionId) as StreamableHTTPServerTransport)
181 |         : undefined;
182 |       if (!transport) {
183 |         reply.code(HTTP_STATUS.BAD_REQUEST).send({ error: ERROR_MESSAGES.INVALID_SSE_SESSION });
184 |         return;
185 |       }
186 | 
187 |       reply.raw.setHeader('Content-Type', 'text/event-stream');
188 |       reply.raw.setHeader('Cache-Control', 'no-cache');
189 |       reply.raw.setHeader('Connection', 'keep-alive');
190 |       reply.raw.flushHeaders(); // Ensure headers are sent immediately
191 | 
192 |       try {
193 |         // transport.handleRequest will take over the response stream
194 |         await transport.handleRequest(request.raw, reply.raw);
195 |         if (!reply.sent) {
196 |           // If transport didn't send anything (unlikely for SSE initial handshake)
197 |           reply.hijack(); // Prevent Fastify from automatically sending response
198 |         }
199 |       } catch (error) {
200 |         if (!reply.raw.writableEnded) {
201 |           reply.raw.end();
202 |         }
203 |       }
204 | 
205 |       request.socket.on('close', () => {
206 |         request.log.info(`SSE client disconnected for session: ${sessionId}`);
207 |         // transport's onclose should handle its own cleanup
208 |       });
209 |     });
210 | 
211 |     this.fastify.delete('/mcp', async (request, reply) => {
212 |       const sessionId = request.headers['mcp-session-id'] as string | undefined;
213 |       const transport = sessionId
214 |         ? (this.transportsMap.get(sessionId) as StreamableHTTPServerTransport)
215 |         : undefined;
216 | 
217 |       if (!transport) {
218 |         reply.code(HTTP_STATUS.BAD_REQUEST).send({ error: ERROR_MESSAGES.INVALID_SESSION_ID });
219 |         return;
220 |       }
221 | 
222 |       try {
223 |         await transport.handleRequest(request.raw, reply.raw);
224 |         // Assume transport.handleRequest will send response or transport.onclose will cleanup
225 |         if (!reply.sent) {
226 |           reply.code(HTTP_STATUS.NO_CONTENT).send();
227 |         }
228 |       } catch (error) {
229 |         if (!reply.sent) {
230 |           reply
231 |             .code(HTTP_STATUS.INTERNAL_SERVER_ERROR)
232 |             .send({ error: ERROR_MESSAGES.MCP_SESSION_DELETION_ERROR });
233 |         }
234 |       }
235 |     });
236 |   }
237 | 
238 |   public async start(port = NATIVE_SERVER_PORT, nativeHost: NativeMessagingHost): Promise<void> {
239 |     if (!this.nativeHost) {
240 |       this.nativeHost = nativeHost; // Ensure nativeHost is set
241 |     } else if (this.nativeHost !== nativeHost) {
242 |       this.nativeHost = nativeHost; // Update to the passed instance
243 |     }
244 | 
245 |     if (this.isRunning) {
246 |       return;
247 |     }
248 | 
249 |     try {
250 |       await this.fastify.listen({ port, host: SERVER_CONFIG.HOST });
251 |       this.isRunning = true; // Update running status
252 |       // No need to return, Promise resolves void by default
253 |     } catch (err) {
254 |       this.isRunning = false; // Startup failed, reset status
255 |       // Throw error instead of exiting directly, let caller (possibly NativeHost) handle
256 |       throw err; // or return Promise.reject(err);
257 |       // process.exit(1); // Not recommended to exit directly here
258 |     }
259 |   }
260 | 
261 |   public async stop(): Promise<void> {
262 |     if (!this.isRunning) {
263 |       return;
264 |     }
265 |     // this.nativeHost = null; // Not recommended to nullify here, association relationship may still be needed
266 |     try {
267 |       await this.fastify.close();
268 |       this.isRunning = false; // Update running status
269 |     } catch (err) {
270 |       // Even if closing fails, mark as not running, but log the error
271 |       this.isRunning = false;
272 |       throw err; // Throw error
273 |     }
274 |   }
275 | 
276 |   public getInstance(): FastifyInstance {
277 |     return this.fastify;
278 |   }
279 | }
280 | 
281 | const serverInstance = new Server();
282 | export default serverInstance;
283 | 
```

--------------------------------------------------------------------------------
/docs/TOOLS.md:
--------------------------------------------------------------------------------

```markdown
  1 | # Chrome MCP Server API Reference 📚
  2 | 
  3 | Complete reference for all available tools and their parameters.
  4 | 
  5 | ## 📋 Table of Contents
  6 | 
  7 | - [Browser Management](#browser-management)
  8 | - [Screenshots & Visual](#screenshots--visual)
  9 | - [Network Monitoring](#network-monitoring)
 10 | - [Content Analysis](#content-analysis)
 11 | - [Interaction](#interaction)
 12 | - [Data Management](#data-management)
 13 | - [Response Format](#response-format)
 14 | 
 15 | ## 📊 Browser Management
 16 | 
 17 | ### `get_windows_and_tabs`
 18 | 
 19 | List all currently open browser windows and tabs.
 20 | 
 21 | **Parameters**: None
 22 | 
 23 | **Response**:
 24 | 
 25 | ```json
 26 | {
 27 |   "windowCount": 2,
 28 |   "tabCount": 5,
 29 |   "windows": [
 30 |     {
 31 |       "windowId": 123,
 32 |       "tabs": [
 33 |         {
 34 |           "tabId": 456,
 35 |           "url": "https://example.com",
 36 |           "title": "Example Page",
 37 |           "active": true
 38 |         }
 39 |       ]
 40 |     }
 41 |   ]
 42 | }
 43 | ```
 44 | 
 45 | ### `chrome_navigate`
 46 | 
 47 | Navigate to a URL with optional viewport control.
 48 | 
 49 | **Parameters**:
 50 | 
 51 | - `url` (string, required): URL to navigate to
 52 | - `newWindow` (boolean, optional): Create new window (default: false)
 53 | - `width` (number, optional): Viewport width in pixels (default: 1280)
 54 | - `height` (number, optional): Viewport height in pixels (default: 720)
 55 | 
 56 | **Example**:
 57 | 
 58 | ```json
 59 | {
 60 |   "url": "https://example.com",
 61 |   "newWindow": true,
 62 |   "width": 1920,
 63 |   "height": 1080
 64 | }
 65 | ```
 66 | 
 67 | ### `chrome_close_tabs`
 68 | 
 69 | Close specific tabs or windows.
 70 | 
 71 | **Parameters**:
 72 | 
 73 | - `tabIds` (array, optional): Array of tab IDs to close
 74 | - `windowIds` (array, optional): Array of window IDs to close
 75 | 
 76 | **Example**:
 77 | 
 78 | ```json
 79 | {
 80 |   "tabIds": [123, 456],
 81 |   "windowIds": [789]
 82 | }
 83 | ```
 84 | 
 85 | ### `chrome_switch_tab`
 86 | 
 87 | Switch to a specific browser tab.
 88 | 
 89 | **Parameters**:
 90 | 
 91 | - `tabId` (number, required): The ID of the tab to switch to.
 92 | - `windowId` (number, optional): The ID of the window where the tab is located.
 93 | 
 94 | **Example**:
 95 | 
 96 | ```json
 97 | {
 98 |   "tabId": 456,
 99 |   "windowId": 123
100 | }
101 | ```
102 | 
103 | ### `chrome_go_back_or_forward`
104 | 
105 | Navigate browser history.
106 | 
107 | **Parameters**:
108 | 
109 | - `direction` (string, required): "back" or "forward"
110 | - `tabId` (number, optional): Specific tab ID (default: active tab)
111 | 
112 | **Example**:
113 | 
114 | ```json
115 | {
116 |   "direction": "back",
117 |   "tabId": 123
118 | }
119 | ```
120 | 
121 | ## 📸 Screenshots & Visual
122 | 
123 | ### `chrome_screenshot`
124 | 
125 | Take advanced screenshots with various options.
126 | 
127 | **Parameters**:
128 | 
129 | - `name` (string, optional): Screenshot filename
130 | - `selector` (string, optional): CSS selector for element screenshot
131 | - `width` (number, optional): Width in pixels (default: 800)
132 | - `height` (number, optional): Height in pixels (default: 600)
133 | - `storeBase64` (boolean, optional): Return base64 data (default: false)
134 | - `fullPage` (boolean, optional): Capture full page (default: true)
135 | 
136 | **Example**:
137 | 
138 | ```json
139 | {
140 |   "selector": ".main-content",
141 |   "fullPage": true,
142 |   "storeBase64": true,
143 |   "width": 1920,
144 |   "height": 1080
145 | }
146 | ```
147 | 
148 | **Response**:
149 | 
150 | ```json
151 | {
152 |   "success": true,
153 |   "base64": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAA...",
154 |   "dimensions": {
155 |     "width": 1920,
156 |     "height": 1080
157 |   }
158 | }
159 | ```
160 | 
161 | ## 🌐 Network Monitoring
162 | 
163 | ### `chrome_network_capture_start`
164 | 
165 | Start capturing network requests using webRequest API.
166 | 
167 | **Parameters**:
168 | 
169 | - `url` (string, optional): URL to navigate to and capture
170 | - `maxCaptureTime` (number, optional): Maximum capture time in ms (default: 30000)
171 | - `inactivityTimeout` (number, optional): Stop after inactivity in ms (default: 3000)
172 | - `includeStatic` (boolean, optional): Include static resources (default: false)
173 | 
174 | **Example**:
175 | 
176 | ```json
177 | {
178 |   "url": "https://api.example.com",
179 |   "maxCaptureTime": 60000,
180 |   "includeStatic": false
181 | }
182 | ```
183 | 
184 | ### `chrome_network_capture_stop`
185 | 
186 | Stop network capture and return collected data.
187 | 
188 | **Parameters**: None
189 | 
190 | **Response**:
191 | 
192 | ```json
193 | {
194 |   "success": true,
195 |   "capturedRequests": [
196 |     {
197 |       "url": "https://api.example.com/data",
198 |       "method": "GET",
199 |       "status": 200,
200 |       "requestHeaders": {...},
201 |       "responseHeaders": {...},
202 |       "responseTime": 150
203 |     }
204 |   ],
205 |   "summary": {
206 |     "totalRequests": 15,
207 |     "captureTime": 5000
208 |   }
209 | }
210 | ```
211 | 
212 | ### `chrome_network_debugger_start`
213 | 
214 | Start capturing with Chrome Debugger API (includes response bodies).
215 | 
216 | **Parameters**:
217 | 
218 | - `url` (string, optional): URL to navigate to and capture
219 | 
220 | ### `chrome_network_debugger_stop`
221 | 
222 | Stop debugger capture and return data with response bodies.
223 | 
224 | ### `chrome_network_request`
225 | 
226 | Send custom HTTP requests.
227 | 
228 | **Parameters**:
229 | 
230 | - `url` (string, required): Request URL
231 | - `method` (string, optional): HTTP method (default: "GET")
232 | - `headers` (object, optional): Request headers
233 | - `body` (string, optional): Request body
234 | 
235 | **Example**:
236 | 
237 | ```json
238 | {
239 |   "url": "https://api.example.com/data",
240 |   "method": "POST",
241 |   "headers": {
242 |     "Content-Type": "application/json"
243 |   },
244 |   "body": "{\"key\": \"value\"}"
245 | }
246 | ```
247 | 
248 | ## 🔍 Content Analysis
249 | 
250 | ### `search_tabs_content`
251 | 
252 | AI-powered semantic search across browser tabs.
253 | 
254 | **Parameters**:
255 | 
256 | - `query` (string, required): Search query
257 | 
258 | **Example**:
259 | 
260 | ```json
261 | {
262 |   "query": "machine learning tutorials"
263 | }
264 | ```
265 | 
266 | **Response**:
267 | 
268 | ```json
269 | {
270 |   "success": true,
271 |   "totalTabsSearched": 10,
272 |   "matchedTabsCount": 3,
273 |   "vectorSearchEnabled": true,
274 |   "indexStats": {
275 |     "totalDocuments": 150,
276 |     "totalTabs": 10,
277 |     "semanticEngineReady": true
278 |   },
279 |   "matchedTabs": [
280 |     {
281 |       "tabId": 123,
282 |       "url": "https://example.com/ml-tutorial",
283 |       "title": "Machine Learning Tutorial",
284 |       "semanticScore": 0.85,
285 |       "matchedSnippets": ["Introduction to machine learning..."],
286 |       "chunkSource": "content"
287 |     }
288 |   ]
289 | }
290 | ```
291 | 
292 | ### `chrome_get_web_content`
293 | 
294 | Extract HTML or text content from web pages.
295 | 
296 | **Parameters**:
297 | 
298 | - `format` (string, optional): "html" or "text" (default: "text")
299 | - `selector` (string, optional): CSS selector for specific elements
300 | - `tabId` (number, optional): Specific tab ID (default: active tab)
301 | 
302 | **Example**:
303 | 
304 | ```json
305 | {
306 |   "format": "text",
307 |   "selector": ".article-content"
308 | }
309 | ```
310 | 
311 | ### `chrome_get_interactive_elements`
312 | 
313 | Find clickable and interactive elements on the page.
314 | 
315 | **Parameters**:
316 | 
317 | - `tabId` (number, optional): Specific tab ID (default: active tab)
318 | 
319 | **Response**:
320 | 
321 | ```json
322 | {
323 |   "elements": [
324 |     {
325 |       "selector": "#submit-button",
326 |       "type": "button",
327 |       "text": "Submit",
328 |       "visible": true,
329 |       "clickable": true
330 |     }
331 |   ]
332 | }
333 | ```
334 | 
335 | ## 🎯 Interaction
336 | 
337 | ### `chrome_click_element`
338 | 
339 | Click elements using CSS selectors.
340 | 
341 | **Parameters**:
342 | 
343 | - `selector` (string, required): CSS selector for target element
344 | - `tabId` (number, optional): Specific tab ID (default: active tab)
345 | 
346 | **Example**:
347 | 
348 | ```json
349 | {
350 |   "selector": "#submit-button"
351 | }
352 | ```
353 | 
354 | ### `chrome_fill_or_select`
355 | 
356 | Fill form fields or select options.
357 | 
358 | **Parameters**:
359 | 
360 | - `selector` (string, required): CSS selector for target element
361 | - `value` (string, required): Value to fill or select
362 | - `tabId` (number, optional): Specific tab ID (default: active tab)
363 | 
364 | **Example**:
365 | 
366 | ```json
367 | {
368 |   "selector": "#email-input",
369 |   "value": "[email protected]"
370 | }
371 | ```
372 | 
373 | ### `chrome_keyboard`
374 | 
375 | Simulate keyboard input and shortcuts.
376 | 
377 | **Parameters**:
378 | 
379 | - `keys` (string, required): Key combination (e.g., "Ctrl+C", "Enter")
380 | - `selector` (string, optional): Target element selector
381 | - `delay` (number, optional): Delay between keystrokes in ms (default: 0)
382 | 
383 | **Example**:
384 | 
385 | ```json
386 | {
387 |   "keys": "Ctrl+A",
388 |   "selector": "#text-input",
389 |   "delay": 100
390 | }
391 | ```
392 | 
393 | ## 📚 Data Management
394 | 
395 | ### `chrome_history`
396 | 
397 | Search browser history with filters.
398 | 
399 | **Parameters**:
400 | 
401 | - `text` (string, optional): Search text in URL/title
402 | - `startTime` (string, optional): Start date (ISO format)
403 | - `endTime` (string, optional): End date (ISO format)
404 | - `maxResults` (number, optional): Maximum results (default: 100)
405 | - `excludeCurrentTabs` (boolean, optional): Exclude current tabs (default: true)
406 | 
407 | **Example**:
408 | 
409 | ```json
410 | {
411 |   "text": "github",
412 |   "startTime": "2024-01-01",
413 |   "maxResults": 50
414 | }
415 | ```
416 | 
417 | ### `chrome_bookmark_search`
418 | 
419 | Search bookmarks by keywords.
420 | 
421 | **Parameters**:
422 | 
423 | - `query` (string, optional): Search keywords
424 | - `maxResults` (number, optional): Maximum results (default: 100)
425 | - `folderPath` (string, optional): Search within specific folder
426 | 
427 | **Example**:
428 | 
429 | ```json
430 | {
431 |   "query": "documentation",
432 |   "maxResults": 20,
433 |   "folderPath": "Work/Resources"
434 | }
435 | ```
436 | 
437 | ### `chrome_bookmark_add`
438 | 
439 | Add new bookmarks with folder support.
440 | 
441 | **Parameters**:
442 | 
443 | - `url` (string, optional): URL to bookmark (default: current tab)
444 | - `title` (string, optional): Bookmark title (default: page title)
445 | - `parentId` (string, optional): Parent folder ID or path
446 | - `createFolder` (boolean, optional): Create folder if not exists (default: false)
447 | 
448 | **Example**:
449 | 
450 | ```json
451 | {
452 |   "url": "https://example.com",
453 |   "title": "Example Site",
454 |   "parentId": "Work/Resources",
455 |   "createFolder": true
456 | }
457 | ```
458 | 
459 | ### `chrome_bookmark_delete`
460 | 
461 | Delete bookmarks by ID or URL.
462 | 
463 | **Parameters**:
464 | 
465 | - `bookmarkId` (string, optional): Bookmark ID to delete
466 | - `url` (string, optional): URL to find and delete
467 | 
468 | **Example**:
469 | 
470 | ```json
471 | {
472 |   "url": "https://example.com"
473 | }
474 | ```
475 | 
476 | ## 📋 Response Format
477 | 
478 | All tools return responses in the following format:
479 | 
480 | ```json
481 | {
482 |   "content": [
483 |     {
484 |       "type": "text",
485 |       "text": "JSON string containing the actual response data"
486 |     }
487 |   ],
488 |   "isError": false
489 | }
490 | ```
491 | 
492 | For errors:
493 | 
494 | ```json
495 | {
496 |   "content": [
497 |     {
498 |       "type": "text",
499 |       "text": "Error message describing what went wrong"
500 |     }
501 |   ],
502 |   "isError": true
503 | }
504 | ```
505 | 
506 | ## 🔧 Usage Examples
507 | 
508 | ### Complete Workflow Example
509 | 
510 | ```javascript
511 | // 1. Navigate to a page
512 | await callTool('chrome_navigate', {
513 |   url: 'https://example.com',
514 | });
515 | 
516 | // 2. Take a screenshot
517 | const screenshot = await callTool('chrome_screenshot', {
518 |   fullPage: true,
519 |   storeBase64: true,
520 | });
521 | 
522 | // 3. Start network monitoring
523 | await callTool('chrome_network_capture_start', {
524 |   maxCaptureTime: 30000,
525 | });
526 | 
527 | // 4. Interact with the page
528 | await callTool('chrome_click_element', {
529 |   selector: '#load-data-button',
530 | });
531 | 
532 | // 5. Search content semantically
533 | const searchResults = await callTool('search_tabs_content', {
534 |   query: 'user data analysis',
535 | });
536 | 
537 | // 6. Stop network capture
538 | const networkData = await callTool('chrome_network_capture_stop');
539 | 
540 | // 7. Save bookmark
541 | await callTool('chrome_bookmark_add', {
542 |   title: 'Data Analysis Page',
543 |   parentId: 'Work/Analytics',
544 | });
545 | ```
546 | 
547 | This API provides comprehensive browser automation capabilities with AI-enhanced content analysis and semantic search features.
548 | 
```

--------------------------------------------------------------------------------
/app/chrome-extension/workers/simd_math.js:
--------------------------------------------------------------------------------

```javascript
  1 | let wasm;
  2 | 
  3 | const cachedTextDecoder = (typeof TextDecoder !== 'undefined' ? new TextDecoder('utf-8', { ignoreBOM: true, fatal: true }) : { decode: () => { throw Error('TextDecoder not available') } } );
  4 | 
  5 | if (typeof TextDecoder !== 'undefined') { cachedTextDecoder.decode(); };
  6 | 
  7 | let cachedUint8ArrayMemory0 = null;
  8 | 
  9 | function getUint8ArrayMemory0() {
 10 |     if (cachedUint8ArrayMemory0 === null || cachedUint8ArrayMemory0.byteLength === 0) {
 11 |         cachedUint8ArrayMemory0 = new Uint8Array(wasm.memory.buffer);
 12 |     }
 13 |     return cachedUint8ArrayMemory0;
 14 | }
 15 | 
 16 | function getStringFromWasm0(ptr, len) {
 17 |     ptr = ptr >>> 0;
 18 |     return cachedTextDecoder.decode(getUint8ArrayMemory0().subarray(ptr, ptr + len));
 19 | }
 20 | 
 21 | let WASM_VECTOR_LEN = 0;
 22 | 
 23 | const cachedTextEncoder = (typeof TextEncoder !== 'undefined' ? new TextEncoder('utf-8') : { encode: () => { throw Error('TextEncoder not available') } } );
 24 | 
 25 | const encodeString = (typeof cachedTextEncoder.encodeInto === 'function'
 26 |     ? function (arg, view) {
 27 |     return cachedTextEncoder.encodeInto(arg, view);
 28 | }
 29 |     : function (arg, view) {
 30 |     const buf = cachedTextEncoder.encode(arg);
 31 |     view.set(buf);
 32 |     return {
 33 |         read: arg.length,
 34 |         written: buf.length
 35 |     };
 36 | });
 37 | 
 38 | function passStringToWasm0(arg, malloc, realloc) {
 39 | 
 40 |     if (realloc === undefined) {
 41 |         const buf = cachedTextEncoder.encode(arg);
 42 |         const ptr = malloc(buf.length, 1) >>> 0;
 43 |         getUint8ArrayMemory0().subarray(ptr, ptr + buf.length).set(buf);
 44 |         WASM_VECTOR_LEN = buf.length;
 45 |         return ptr;
 46 |     }
 47 | 
 48 |     let len = arg.length;
 49 |     let ptr = malloc(len, 1) >>> 0;
 50 | 
 51 |     const mem = getUint8ArrayMemory0();
 52 | 
 53 |     let offset = 0;
 54 | 
 55 |     for (; offset < len; offset++) {
 56 |         const code = arg.charCodeAt(offset);
 57 |         if (code > 0x7F) break;
 58 |         mem[ptr + offset] = code;
 59 |     }
 60 | 
 61 |     if (offset !== len) {
 62 |         if (offset !== 0) {
 63 |             arg = arg.slice(offset);
 64 |         }
 65 |         ptr = realloc(ptr, len, len = offset + arg.length * 3, 1) >>> 0;
 66 |         const view = getUint8ArrayMemory0().subarray(ptr + offset, ptr + len);
 67 |         const ret = encodeString(arg, view);
 68 | 
 69 |         offset += ret.written;
 70 |         ptr = realloc(ptr, len, offset, 1) >>> 0;
 71 |     }
 72 | 
 73 |     WASM_VECTOR_LEN = offset;
 74 |     return ptr;
 75 | }
 76 | 
 77 | let cachedDataViewMemory0 = null;
 78 | 
 79 | function getDataViewMemory0() {
 80 |     if (cachedDataViewMemory0 === null || cachedDataViewMemory0.buffer.detached === true || (cachedDataViewMemory0.buffer.detached === undefined && cachedDataViewMemory0.buffer !== wasm.memory.buffer)) {
 81 |         cachedDataViewMemory0 = new DataView(wasm.memory.buffer);
 82 |     }
 83 |     return cachedDataViewMemory0;
 84 | }
 85 | 
 86 | export function main() {
 87 |     wasm.main();
 88 | }
 89 | 
 90 | let cachedFloat32ArrayMemory0 = null;
 91 | 
 92 | function getFloat32ArrayMemory0() {
 93 |     if (cachedFloat32ArrayMemory0 === null || cachedFloat32ArrayMemory0.byteLength === 0) {
 94 |         cachedFloat32ArrayMemory0 = new Float32Array(wasm.memory.buffer);
 95 |     }
 96 |     return cachedFloat32ArrayMemory0;
 97 | }
 98 | 
 99 | function passArrayF32ToWasm0(arg, malloc) {
100 |     const ptr = malloc(arg.length * 4, 4) >>> 0;
101 |     getFloat32ArrayMemory0().set(arg, ptr / 4);
102 |     WASM_VECTOR_LEN = arg.length;
103 |     return ptr;
104 | }
105 | 
106 | function getArrayF32FromWasm0(ptr, len) {
107 |     ptr = ptr >>> 0;
108 |     return getFloat32ArrayMemory0().subarray(ptr / 4, ptr / 4 + len);
109 | }
110 | 
111 | const SIMDMathFinalization = (typeof FinalizationRegistry === 'undefined')
112 |     ? { register: () => {}, unregister: () => {} }
113 |     : new FinalizationRegistry(ptr => wasm.__wbg_simdmath_free(ptr >>> 0, 1));
114 | 
115 | export class SIMDMath {
116 | 
117 |     __destroy_into_raw() {
118 |         const ptr = this.__wbg_ptr;
119 |         this.__wbg_ptr = 0;
120 |         SIMDMathFinalization.unregister(this);
121 |         return ptr;
122 |     }
123 | 
124 |     free() {
125 |         const ptr = this.__destroy_into_raw();
126 |         wasm.__wbg_simdmath_free(ptr, 0);
127 |     }
128 |     constructor() {
129 |         const ret = wasm.simdmath_new();
130 |         this.__wbg_ptr = ret >>> 0;
131 |         SIMDMathFinalization.register(this, this.__wbg_ptr, this);
132 |         return this;
133 |     }
134 |     /**
135 |      * @param {Float32Array} vec_a
136 |      * @param {Float32Array} vec_b
137 |      * @returns {number}
138 |      */
139 |     cosine_similarity(vec_a, vec_b) {
140 |         const ptr0 = passArrayF32ToWasm0(vec_a, wasm.__wbindgen_malloc);
141 |         const len0 = WASM_VECTOR_LEN;
142 |         const ptr1 = passArrayF32ToWasm0(vec_b, wasm.__wbindgen_malloc);
143 |         const len1 = WASM_VECTOR_LEN;
144 |         const ret = wasm.simdmath_cosine_similarity(this.__wbg_ptr, ptr0, len0, ptr1, len1);
145 |         return ret;
146 |     }
147 |     /**
148 |      * @param {Float32Array} vectors
149 |      * @param {Float32Array} query
150 |      * @param {number} vector_dim
151 |      * @returns {Float32Array}
152 |      */
153 |     batch_similarity(vectors, query, vector_dim) {
154 |         const ptr0 = passArrayF32ToWasm0(vectors, wasm.__wbindgen_malloc);
155 |         const len0 = WASM_VECTOR_LEN;
156 |         const ptr1 = passArrayF32ToWasm0(query, wasm.__wbindgen_malloc);
157 |         const len1 = WASM_VECTOR_LEN;
158 |         const ret = wasm.simdmath_batch_similarity(this.__wbg_ptr, ptr0, len0, ptr1, len1, vector_dim);
159 |         var v3 = getArrayF32FromWasm0(ret[0], ret[1]).slice();
160 |         wasm.__wbindgen_free(ret[0], ret[1] * 4, 4);
161 |         return v3;
162 |     }
163 |     /**
164 |      * @param {Float32Array} vectors_a
165 |      * @param {Float32Array} vectors_b
166 |      * @param {number} vector_dim
167 |      * @returns {Float32Array}
168 |      */
169 |     similarity_matrix(vectors_a, vectors_b, vector_dim) {
170 |         const ptr0 = passArrayF32ToWasm0(vectors_a, wasm.__wbindgen_malloc);
171 |         const len0 = WASM_VECTOR_LEN;
172 |         const ptr1 = passArrayF32ToWasm0(vectors_b, wasm.__wbindgen_malloc);
173 |         const len1 = WASM_VECTOR_LEN;
174 |         const ret = wasm.simdmath_similarity_matrix(this.__wbg_ptr, ptr0, len0, ptr1, len1, vector_dim);
175 |         var v3 = getArrayF32FromWasm0(ret[0], ret[1]).slice();
176 |         wasm.__wbindgen_free(ret[0], ret[1] * 4, 4);
177 |         return v3;
178 |     }
179 | }
180 | 
181 | async function __wbg_load(module, imports) {
182 |     if (typeof Response === 'function' && module instanceof Response) {
183 |         if (typeof WebAssembly.instantiateStreaming === 'function') {
184 |             try {
185 |                 return await WebAssembly.instantiateStreaming(module, imports);
186 | 
187 |             } catch (e) {
188 |                 if (module.headers.get('Content-Type') != 'application/wasm') {
189 |                     console.warn("`WebAssembly.instantiateStreaming` failed because your server does not serve Wasm with `application/wasm` MIME type. Falling back to `WebAssembly.instantiate` which is slower. Original error:\n", e);
190 | 
191 |                 } else {
192 |                     throw e;
193 |                 }
194 |             }
195 |         }
196 | 
197 |         const bytes = await module.arrayBuffer();
198 |         return await WebAssembly.instantiate(bytes, imports);
199 | 
200 |     } else {
201 |         const instance = await WebAssembly.instantiate(module, imports);
202 | 
203 |         if (instance instanceof WebAssembly.Instance) {
204 |             return { instance, module };
205 | 
206 |         } else {
207 |             return instance;
208 |         }
209 |     }
210 | }
211 | 
212 | function __wbg_get_imports() {
213 |     const imports = {};
214 |     imports.wbg = {};
215 |     imports.wbg.__wbg_error_7534b8e9a36f1ab4 = function(arg0, arg1) {
216 |         let deferred0_0;
217 |         let deferred0_1;
218 |         try {
219 |             deferred0_0 = arg0;
220 |             deferred0_1 = arg1;
221 |             console.error(getStringFromWasm0(arg0, arg1));
222 |         } finally {
223 |             wasm.__wbindgen_free(deferred0_0, deferred0_1, 1);
224 |         }
225 |     };
226 |     imports.wbg.__wbg_new_8a6f238a6ece86ea = function() {
227 |         const ret = new Error();
228 |         return ret;
229 |     };
230 |     imports.wbg.__wbg_stack_0ed75d68575b0f3c = function(arg0, arg1) {
231 |         const ret = arg1.stack;
232 |         const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
233 |         const len1 = WASM_VECTOR_LEN;
234 |         getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, true);
235 |         getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true);
236 |     };
237 |     imports.wbg.__wbindgen_init_externref_table = function() {
238 |         const table = wasm.__wbindgen_export_3;
239 |         const offset = table.grow(4);
240 |         table.set(0, undefined);
241 |         table.set(offset + 0, undefined);
242 |         table.set(offset + 1, null);
243 |         table.set(offset + 2, true);
244 |         table.set(offset + 3, false);
245 |         ;
246 |     };
247 |     imports.wbg.__wbindgen_throw = function(arg0, arg1) {
248 |         throw new Error(getStringFromWasm0(arg0, arg1));
249 |     };
250 | 
251 |     return imports;
252 | }
253 | 
254 | function __wbg_init_memory(imports, memory) {
255 | 
256 | }
257 | 
258 | function __wbg_finalize_init(instance, module) {
259 |     wasm = instance.exports;
260 |     __wbg_init.__wbindgen_wasm_module = module;
261 |     cachedDataViewMemory0 = null;
262 |     cachedFloat32ArrayMemory0 = null;
263 |     cachedUint8ArrayMemory0 = null;
264 | 
265 | 
266 |     wasm.__wbindgen_start();
267 |     return wasm;
268 | }
269 | 
270 | function initSync(module) {
271 |     if (wasm !== undefined) return wasm;
272 | 
273 | 
274 |     if (typeof module !== 'undefined') {
275 |         if (Object.getPrototypeOf(module) === Object.prototype) {
276 |             ({module} = module)
277 |         } else {
278 |             console.warn('using deprecated parameters for `initSync()`; pass a single object instead')
279 |         }
280 |     }
281 | 
282 |     const imports = __wbg_get_imports();
283 | 
284 |     __wbg_init_memory(imports);
285 | 
286 |     if (!(module instanceof WebAssembly.Module)) {
287 |         module = new WebAssembly.Module(module);
288 |     }
289 | 
290 |     const instance = new WebAssembly.Instance(module, imports);
291 | 
292 |     return __wbg_finalize_init(instance, module);
293 | }
294 | 
295 | async function __wbg_init(module_or_path) {
296 |     if (wasm !== undefined) return wasm;
297 | 
298 | 
299 |     if (typeof module_or_path !== 'undefined') {
300 |         if (Object.getPrototypeOf(module_or_path) === Object.prototype) {
301 |             ({module_or_path} = module_or_path)
302 |         } else {
303 |             console.warn('using deprecated parameters for the initialization function; pass a single object instead')
304 |         }
305 |     }
306 | 
307 |     if (typeof module_or_path === 'undefined') {
308 |         module_or_path = new URL('simd_math_bg.wasm', import.meta.url);
309 |     }
310 |     const imports = __wbg_get_imports();
311 | 
312 |     if (typeof module_or_path === 'string' || (typeof Request === 'function' && module_or_path instanceof Request) || (typeof URL === 'function' && module_or_path instanceof URL)) {
313 |         module_or_path = fetch(module_or_path);
314 |     }
315 | 
316 |     __wbg_init_memory(imports);
317 | 
318 |     const { instance, module } = await __wbg_load(await module_or_path, imports);
319 | 
320 |     return __wbg_finalize_init(instance, module);
321 | }
322 | 
323 | export { initSync };
324 | export default __wbg_init;
325 | 
```

--------------------------------------------------------------------------------
/app/chrome-extension/entrypoints/background/tools/browser/console.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { createErrorResponse, ToolResult } from '@/common/tool-handler';
  2 | import { BaseBrowserToolExecutor } from '../base-browser';
  3 | import { TOOL_NAMES } from 'chrome-mcp-shared';
  4 | 
  5 | const DEBUGGER_PROTOCOL_VERSION = '1.3';
  6 | const DEFAULT_MAX_MESSAGES = 100;
  7 | 
  8 | interface ConsoleToolParams {
  9 |   url?: string;
 10 |   includeExceptions?: boolean;
 11 |   maxMessages?: number;
 12 | }
 13 | 
 14 | interface ConsoleMessage {
 15 |   timestamp: number;
 16 |   level: string;
 17 |   text: string;
 18 |   args?: any[];
 19 |   source?: string;
 20 |   url?: string;
 21 |   lineNumber?: number;
 22 |   stackTrace?: any;
 23 | }
 24 | 
 25 | interface ConsoleException {
 26 |   timestamp: number;
 27 |   text: string;
 28 |   url?: string;
 29 |   lineNumber?: number;
 30 |   columnNumber?: number;
 31 |   stackTrace?: any;
 32 | }
 33 | 
 34 | interface ConsoleResult {
 35 |   success: boolean;
 36 |   message: string;
 37 |   tabId: number;
 38 |   tabUrl: string;
 39 |   tabTitle: string;
 40 |   captureStartTime: number;
 41 |   captureEndTime: number;
 42 |   totalDurationMs: number;
 43 |   messages: ConsoleMessage[];
 44 |   exceptions: ConsoleException[];
 45 |   messageCount: number;
 46 |   exceptionCount: number;
 47 |   messageLimitReached: boolean;
 48 | }
 49 | 
 50 | /**
 51 |  * Tool for capturing console output from browser tabs
 52 |  */
 53 | class ConsoleTool extends BaseBrowserToolExecutor {
 54 |   name = TOOL_NAMES.BROWSER.CONSOLE;
 55 | 
 56 |   async execute(args: ConsoleToolParams): Promise<ToolResult> {
 57 |     const { url, includeExceptions = true, maxMessages = DEFAULT_MAX_MESSAGES } = args;
 58 | 
 59 |     let targetTab: chrome.tabs.Tab;
 60 | 
 61 |     try {
 62 |       if (url) {
 63 |         // Navigate to the specified URL
 64 |         targetTab = await this.navigateToUrl(url);
 65 |       } else {
 66 |         // Use current active tab
 67 |         const [activeTab] = await chrome.tabs.query({ active: true, currentWindow: true });
 68 |         if (!activeTab?.id) {
 69 |           return createErrorResponse('No active tab found and no URL provided.');
 70 |         }
 71 |         targetTab = activeTab;
 72 |       }
 73 | 
 74 |       if (!targetTab?.id) {
 75 |         return createErrorResponse('Failed to identify target tab.');
 76 |       }
 77 | 
 78 |       const tabId = targetTab.id;
 79 | 
 80 |       // Capture console messages (one-time capture)
 81 |       const result = await this.captureConsoleMessages(tabId, {
 82 |         includeExceptions,
 83 |         maxMessages,
 84 |       });
 85 | 
 86 |       return {
 87 |         content: [
 88 |           {
 89 |             type: 'text',
 90 |             text: JSON.stringify(result),
 91 |           },
 92 |         ],
 93 |         isError: false,
 94 |       };
 95 |     } catch (error: any) {
 96 |       console.error('ConsoleTool: Critical error during execute:', error);
 97 |       return createErrorResponse(`Error in ConsoleTool: ${error.message || String(error)}`);
 98 |     }
 99 |   }
100 | 
101 |   private async navigateToUrl(url: string): Promise<chrome.tabs.Tab> {
102 |     // Check if URL is already open
103 |     const existingTabs = await chrome.tabs.query({ url });
104 | 
105 |     if (existingTabs.length > 0 && existingTabs[0]?.id) {
106 |       const tab = existingTabs[0];
107 |       // Activate the existing tab
108 |       await chrome.tabs.update(tab.id!, { active: true });
109 |       await chrome.windows.update(tab.windowId, { focused: true });
110 |       return tab;
111 |     } else {
112 |       // Create new tab with the URL
113 |       const newTab = await chrome.tabs.create({ url, active: true });
114 |       // Wait for tab to be ready
115 |       await this.waitForTabReady(newTab.id!);
116 |       return newTab;
117 |     }
118 |   }
119 | 
120 |   private async waitForTabReady(tabId: number): Promise<void> {
121 |     return new Promise((resolve) => {
122 |       const checkTab = async () => {
123 |         try {
124 |           const tab = await chrome.tabs.get(tabId);
125 |           if (tab.status === 'complete') {
126 |             resolve();
127 |           } else {
128 |             setTimeout(checkTab, 100);
129 |           }
130 |         } catch (error) {
131 |           // Tab might be closed, resolve anyway
132 |           resolve();
133 |         }
134 |       };
135 |       checkTab();
136 |     });
137 |   }
138 | 
139 |   private formatConsoleArgs(args: any[]): string {
140 |     if (!args || args.length === 0) return '';
141 | 
142 |     return args
143 |       .map((arg) => {
144 |         if (arg.type === 'string') {
145 |           return arg.value || '';
146 |         } else if (arg.type === 'number') {
147 |           return String(arg.value || '');
148 |         } else if (arg.type === 'boolean') {
149 |           return String(arg.value || '');
150 |         } else if (arg.type === 'object') {
151 |           return arg.description || '[Object]';
152 |         } else if (arg.type === 'undefined') {
153 |           return 'undefined';
154 |         } else if (arg.type === 'function') {
155 |           return arg.description || '[Function]';
156 |         } else {
157 |           return arg.description || arg.value || String(arg);
158 |         }
159 |       })
160 |       .join(' ');
161 |   }
162 | 
163 |   private async captureConsoleMessages(
164 |     tabId: number,
165 |     options: {
166 |       includeExceptions: boolean;
167 |       maxMessages: number;
168 |     },
169 |   ): Promise<ConsoleResult> {
170 |     const { includeExceptions, maxMessages } = options;
171 |     const startTime = Date.now();
172 |     const messages: ConsoleMessage[] = [];
173 |     const exceptions: ConsoleException[] = [];
174 |     let limitReached = false;
175 | 
176 |     try {
177 |       // Get tab information
178 |       const tab = await chrome.tabs.get(tabId);
179 | 
180 |       // Check if debugger is already attached
181 |       const targets = await chrome.debugger.getTargets();
182 |       const existingTarget = targets.find(
183 |         (t) => t.tabId === tabId && t.attached && t.type === 'page',
184 |       );
185 |       if (existingTarget && !existingTarget.extensionId) {
186 |         throw new Error(
187 |           `Debugger is already attached to tab ${tabId} by another tool (e.g., DevTools).`,
188 |         );
189 |       }
190 | 
191 |       // Attach debugger
192 |       try {
193 |         await chrome.debugger.attach({ tabId }, DEBUGGER_PROTOCOL_VERSION);
194 |       } catch (error: any) {
195 |         if (error.message?.includes('Cannot attach to the target with an attached client')) {
196 |           throw new Error(
197 |             `Debugger is already attached to tab ${tabId}. This might be DevTools or another extension.`,
198 |           );
199 |         }
200 |         throw error;
201 |       }
202 | 
203 |       // Set up event listener to collect messages
204 |       const collectedMessages: any[] = [];
205 |       const collectedExceptions: any[] = [];
206 | 
207 |       const eventListener = (source: chrome.debugger.Debuggee, method: string, params?: any) => {
208 |         if (source.tabId !== tabId) return;
209 | 
210 |         if (method === 'Log.entryAdded' && params?.entry) {
211 |           collectedMessages.push(params.entry);
212 |         } else if (method === 'Runtime.consoleAPICalled' && params) {
213 |           // Convert Runtime.consoleAPICalled to Log.entryAdded format
214 |           const logEntry = {
215 |             timestamp: params.timestamp,
216 |             level: params.type || 'log',
217 |             text: this.formatConsoleArgs(params.args || []),
218 |             source: 'console-api',
219 |             url: params.stackTrace?.callFrames?.[0]?.url,
220 |             lineNumber: params.stackTrace?.callFrames?.[0]?.lineNumber,
221 |             stackTrace: params.stackTrace,
222 |             args: params.args,
223 |           };
224 |           collectedMessages.push(logEntry);
225 |         } else if (
226 |           method === 'Runtime.exceptionThrown' &&
227 |           includeExceptions &&
228 |           params?.exceptionDetails
229 |         ) {
230 |           collectedExceptions.push(params.exceptionDetails);
231 |         }
232 |       };
233 | 
234 |       chrome.debugger.onEvent.addListener(eventListener);
235 | 
236 |       try {
237 |         // Enable Runtime domain first to capture console API calls and exceptions
238 |         await chrome.debugger.sendCommand({ tabId }, 'Runtime.enable');
239 | 
240 |         // Also enable Log domain to capture other log entries
241 |         await chrome.debugger.sendCommand({ tabId }, 'Log.enable');
242 | 
243 |         // Wait for all messages to be flushed
244 |         await new Promise((resolve) => setTimeout(resolve, 2000));
245 | 
246 |         // Process collected messages
247 |         for (const entry of collectedMessages) {
248 |           if (messages.length >= maxMessages) {
249 |             limitReached = true;
250 |             break;
251 |           }
252 | 
253 |           const message: ConsoleMessage = {
254 |             timestamp: entry.timestamp,
255 |             level: entry.level || 'log',
256 |             text: entry.text || '',
257 |             source: entry.source,
258 |             url: entry.url,
259 |             lineNumber: entry.lineNumber,
260 |           };
261 | 
262 |           if (entry.stackTrace) {
263 |             message.stackTrace = entry.stackTrace;
264 |           }
265 | 
266 |           if (entry.args && Array.isArray(entry.args)) {
267 |             message.args = entry.args;
268 |           }
269 | 
270 |           messages.push(message);
271 |         }
272 | 
273 |         // Process collected exceptions
274 |         for (const exceptionDetails of collectedExceptions) {
275 |           const exception: ConsoleException = {
276 |             timestamp: Date.now(),
277 |             text:
278 |               exceptionDetails.text ||
279 |               exceptionDetails.exception?.description ||
280 |               'Unknown exception',
281 |             url: exceptionDetails.url,
282 |             lineNumber: exceptionDetails.lineNumber,
283 |             columnNumber: exceptionDetails.columnNumber,
284 |           };
285 | 
286 |           if (exceptionDetails.stackTrace) {
287 |             exception.stackTrace = exceptionDetails.stackTrace;
288 |           }
289 | 
290 |           exceptions.push(exception);
291 |         }
292 |       } finally {
293 |         // Clean up
294 |         chrome.debugger.onEvent.removeListener(eventListener);
295 | 
296 |         try {
297 |           await chrome.debugger.sendCommand({ tabId }, 'Runtime.disable');
298 |         } catch (e) {
299 |           console.warn(`ConsoleTool: Error disabling Runtime for tab ${tabId}:`, e);
300 |         }
301 | 
302 |         try {
303 |           await chrome.debugger.sendCommand({ tabId }, 'Log.disable');
304 |         } catch (e) {
305 |           console.warn(`ConsoleTool: Error disabling Log for tab ${tabId}:`, e);
306 |         }
307 | 
308 |         try {
309 |           await chrome.debugger.detach({ tabId });
310 |         } catch (e) {
311 |           console.warn(`ConsoleTool: Error detaching debugger for tab ${tabId}:`, e);
312 |         }
313 |       }
314 | 
315 |       const endTime = Date.now();
316 | 
317 |       // Sort messages by timestamp
318 |       messages.sort((a, b) => a.timestamp - b.timestamp);
319 |       exceptions.sort((a, b) => a.timestamp - b.timestamp);
320 | 
321 |       return {
322 |         success: true,
323 |         message: `Console capture completed for tab ${tabId}. ${messages.length} messages, ${exceptions.length} exceptions captured.`,
324 |         tabId,
325 |         tabUrl: tab.url || '',
326 |         tabTitle: tab.title || '',
327 |         captureStartTime: startTime,
328 |         captureEndTime: endTime,
329 |         totalDurationMs: endTime - startTime,
330 |         messages,
331 |         exceptions,
332 |         messageCount: messages.length,
333 |         exceptionCount: exceptions.length,
334 |         messageLimitReached: limitReached,
335 |       };
336 |     } catch (error: any) {
337 |       console.error(`ConsoleTool: Error capturing console messages for tab ${tabId}:`, error);
338 |       throw error;
339 |     }
340 |   }
341 | }
342 | 
343 | export const consoleTool = new ConsoleTool();
344 | 
```

--------------------------------------------------------------------------------
/app/chrome-extension/utils/model-cache-manager.ts:
--------------------------------------------------------------------------------

```typescript
  1 | /**
  2 |  * Model Cache Manager
  3 |  */
  4 | 
  5 | const CACHE_NAME = 'onnx-model-cache-v1';
  6 | const CACHE_EXPIRY_DAYS = 30;
  7 | const MAX_CACHE_SIZE_MB = 500;
  8 | 
  9 | export interface CacheMetadata {
 10 |   timestamp: number;
 11 |   modelUrl: string;
 12 |   size: number;
 13 |   version: string;
 14 | }
 15 | 
 16 | export interface CacheEntry {
 17 |   url: string;
 18 |   size: number;
 19 |   sizeMB: number;
 20 |   timestamp: number;
 21 |   age: string;
 22 |   expired: boolean;
 23 | }
 24 | 
 25 | export interface CacheStats {
 26 |   totalSize: number;
 27 |   totalSizeMB: number;
 28 |   entryCount: number;
 29 |   entries: CacheEntry[];
 30 | }
 31 | 
 32 | interface CacheEntryDetails {
 33 |   url: string;
 34 |   timestamp: number;
 35 |   size: number;
 36 | }
 37 | 
 38 | export class ModelCacheManager {
 39 |   private static instance: ModelCacheManager | null = null;
 40 | 
 41 |   public static getInstance(): ModelCacheManager {
 42 |     if (!ModelCacheManager.instance) {
 43 |       ModelCacheManager.instance = new ModelCacheManager();
 44 |     }
 45 |     return ModelCacheManager.instance;
 46 |   }
 47 | 
 48 |   private constructor() {}
 49 | 
 50 |   private getCacheMetadataKey(modelUrl: string): string {
 51 |     const encodedUrl = encodeURIComponent(modelUrl);
 52 |     return `https://cache-metadata.local/${encodedUrl}`;
 53 |   }
 54 | 
 55 |   private isCacheExpired(metadata: CacheMetadata): boolean {
 56 |     const now = Date.now();
 57 |     const expiryTime = metadata.timestamp + CACHE_EXPIRY_DAYS * 24 * 60 * 60 * 1000;
 58 |     return now > expiryTime;
 59 |   }
 60 | 
 61 |   private isMetadataUrl(url: string): boolean {
 62 |     return url.startsWith('https://cache-metadata.local/');
 63 |   }
 64 | 
 65 |   private async collectCacheEntries(): Promise<{
 66 |     entries: CacheEntryDetails[];
 67 |     totalSize: number;
 68 |     entryCount: number;
 69 |   }> {
 70 |     const cache = await caches.open(CACHE_NAME);
 71 |     const keys = await cache.keys();
 72 |     const entries: CacheEntryDetails[] = [];
 73 |     let totalSize = 0;
 74 |     let entryCount = 0;
 75 | 
 76 |     for (const request of keys) {
 77 |       if (this.isMetadataUrl(request.url)) continue;
 78 | 
 79 |       const response = await cache.match(request);
 80 |       if (response) {
 81 |         const blob = await response.blob();
 82 |         const size = blob.size;
 83 |         totalSize += size;
 84 |         entryCount++;
 85 | 
 86 |         const metadataResponse = await cache.match(this.getCacheMetadataKey(request.url));
 87 |         let timestamp = 0;
 88 | 
 89 |         if (metadataResponse) {
 90 |           try {
 91 |             const metadata: CacheMetadata = await metadataResponse.json();
 92 |             timestamp = metadata.timestamp;
 93 |           } catch (error) {
 94 |             console.warn('Failed to parse cache metadata:', error);
 95 |           }
 96 |         }
 97 | 
 98 |         entries.push({
 99 |           url: request.url,
100 |           timestamp,
101 |           size,
102 |         });
103 |       }
104 |     }
105 | 
106 |     return { entries, totalSize, entryCount };
107 |   }
108 | 
109 |   public async cleanupCacheOnDemand(newDataSize: number = 0): Promise<void> {
110 |     const cache = await caches.open(CACHE_NAME);
111 |     const { entries, totalSize } = await this.collectCacheEntries();
112 |     const maxSizeBytes = MAX_CACHE_SIZE_MB * 1024 * 1024;
113 |     const projectedSize = totalSize + newDataSize;
114 | 
115 |     if (projectedSize <= maxSizeBytes) {
116 |       return;
117 |     }
118 | 
119 |     console.log(
120 |       `Cache size (${(totalSize / 1024 / 1024).toFixed(2)}MB) + new data (${(newDataSize / 1024 / 1024).toFixed(2)}MB) exceeds limit (${MAX_CACHE_SIZE_MB}MB), cleaning up...`,
121 |     );
122 | 
123 |     const expiredEntries: CacheEntryDetails[] = [];
124 |     const validEntries: CacheEntryDetails[] = [];
125 | 
126 |     for (const entry of entries) {
127 |       const metadataResponse = await cache.match(this.getCacheMetadataKey(entry.url));
128 |       let isExpired = false;
129 | 
130 |       if (metadataResponse) {
131 |         try {
132 |           const metadata: CacheMetadata = await metadataResponse.json();
133 |           isExpired = this.isCacheExpired(metadata);
134 |         } catch (error) {
135 |           isExpired = true;
136 |         }
137 |       } else {
138 |         isExpired = true;
139 |       }
140 | 
141 |       if (isExpired) {
142 |         expiredEntries.push(entry);
143 |       } else {
144 |         validEntries.push(entry);
145 |       }
146 |     }
147 | 
148 |     let currentSize = totalSize;
149 |     for (const entry of expiredEntries) {
150 |       await cache.delete(entry.url);
151 |       await cache.delete(this.getCacheMetadataKey(entry.url));
152 |       currentSize -= entry.size;
153 |       console.log(
154 |         `Cleaned up expired cache entry: ${entry.url} (${(entry.size / 1024 / 1024).toFixed(2)}MB)`,
155 |       );
156 |     }
157 | 
158 |     if (currentSize + newDataSize > maxSizeBytes) {
159 |       validEntries.sort((a, b) => a.timestamp - b.timestamp);
160 | 
161 |       for (const entry of validEntries) {
162 |         if (currentSize + newDataSize <= maxSizeBytes) break;
163 | 
164 |         await cache.delete(entry.url);
165 |         await cache.delete(this.getCacheMetadataKey(entry.url));
166 |         currentSize -= entry.size;
167 |         console.log(
168 |           `Cleaned up old cache entry: ${entry.url} (${(entry.size / 1024 / 1024).toFixed(2)}MB)`,
169 |         );
170 |       }
171 |     }
172 | 
173 |     console.log(`Cache cleanup complete. New size: ${(currentSize / 1024 / 1024).toFixed(2)}MB`);
174 |   }
175 | 
176 |   public async storeCacheMetadata(modelUrl: string, size: number): Promise<void> {
177 |     const cache = await caches.open(CACHE_NAME);
178 |     const metadata: CacheMetadata = {
179 |       timestamp: Date.now(),
180 |       modelUrl,
181 |       size,
182 |       version: CACHE_NAME,
183 |     };
184 | 
185 |     const metadataResponse = new Response(JSON.stringify(metadata), {
186 |       headers: { 'Content-Type': 'application/json' },
187 |     });
188 | 
189 |     await cache.put(this.getCacheMetadataKey(modelUrl), metadataResponse);
190 |   }
191 | 
192 |   public async getCachedModelData(modelUrl: string): Promise<ArrayBuffer | null> {
193 |     const cache = await caches.open(CACHE_NAME);
194 |     const cachedResponse = await cache.match(modelUrl);
195 | 
196 |     if (!cachedResponse) {
197 |       return null;
198 |     }
199 | 
200 |     const metadataResponse = await cache.match(this.getCacheMetadataKey(modelUrl));
201 |     if (metadataResponse) {
202 |       try {
203 |         const metadata: CacheMetadata = await metadataResponse.json();
204 |         if (!this.isCacheExpired(metadata)) {
205 |           console.log('Model found in cache and not expired. Loading from cache.');
206 |           return cachedResponse.arrayBuffer();
207 |         } else {
208 |           console.log('Cached model is expired, removing...');
209 |           await this.deleteCacheEntry(modelUrl);
210 |           return null;
211 |         }
212 |       } catch (error) {
213 |         console.warn('Failed to parse cache metadata, treating as expired:', error);
214 |         await this.deleteCacheEntry(modelUrl);
215 |         return null;
216 |       }
217 |     } else {
218 |       console.log('Cached model has no metadata, treating as expired...');
219 |       await this.deleteCacheEntry(modelUrl);
220 |       return null;
221 |     }
222 |   }
223 | 
224 |   public async storeModelData(modelUrl: string, data: ArrayBuffer): Promise<void> {
225 |     await this.cleanupCacheOnDemand(data.byteLength);
226 | 
227 |     const cache = await caches.open(CACHE_NAME);
228 |     const response = new Response(data);
229 | 
230 |     await cache.put(modelUrl, response);
231 |     await this.storeCacheMetadata(modelUrl, data.byteLength);
232 | 
233 |     console.log(
234 |       `Model cached successfully (${(data.byteLength / 1024 / 1024).toFixed(2)}MB): ${modelUrl}`,
235 |     );
236 |   }
237 | 
238 |   public async deleteCacheEntry(modelUrl: string): Promise<void> {
239 |     const cache = await caches.open(CACHE_NAME);
240 |     await cache.delete(modelUrl);
241 |     await cache.delete(this.getCacheMetadataKey(modelUrl));
242 |   }
243 | 
244 |   public async clearAllCache(): Promise<void> {
245 |     const cache = await caches.open(CACHE_NAME);
246 |     const keys = await cache.keys();
247 | 
248 |     for (const request of keys) {
249 |       await cache.delete(request);
250 |     }
251 | 
252 |     console.log('All model cache entries cleared');
253 |   }
254 | 
255 |   public async getCacheStats(): Promise<CacheStats> {
256 |     const { entries, totalSize, entryCount } = await this.collectCacheEntries();
257 |     const cache = await caches.open(CACHE_NAME);
258 | 
259 |     const cacheEntries: CacheEntry[] = [];
260 | 
261 |     for (const entry of entries) {
262 |       const metadataResponse = await cache.match(this.getCacheMetadataKey(entry.url));
263 |       let expired = false;
264 | 
265 |       if (metadataResponse) {
266 |         try {
267 |           const metadata: CacheMetadata = await metadataResponse.json();
268 |           expired = this.isCacheExpired(metadata);
269 |         } catch (error) {
270 |           expired = true;
271 |         }
272 |       } else {
273 |         expired = true;
274 |       }
275 | 
276 |       const age =
277 |         entry.timestamp > 0
278 |           ? `${Math.round((Date.now() - entry.timestamp) / (1000 * 60 * 60 * 24))} days`
279 |           : 'unknown';
280 | 
281 |       cacheEntries.push({
282 |         url: entry.url,
283 |         size: entry.size,
284 |         sizeMB: Number((entry.size / 1024 / 1024).toFixed(2)),
285 |         timestamp: entry.timestamp,
286 |         age,
287 |         expired,
288 |       });
289 |     }
290 | 
291 |     return {
292 |       totalSize,
293 |       totalSizeMB: Number((totalSize / 1024 / 1024).toFixed(2)),
294 |       entryCount,
295 |       entries: cacheEntries.sort((a, b) => b.timestamp - a.timestamp),
296 |     };
297 |   }
298 | 
299 |   public async manualCleanup(): Promise<void> {
300 |     await this.cleanupCacheOnDemand(0);
301 |     console.log('Manual cache cleanup completed');
302 |   }
303 | 
304 |   /**
305 |    * Check if a specific model is cached and not expired
306 |    * @param modelUrl The model URL to check
307 |    * @returns Promise<boolean> True if model is cached and valid
308 |    */
309 |   public async isModelCached(modelUrl: string): Promise<boolean> {
310 |     try {
311 |       const cache = await caches.open(CACHE_NAME);
312 |       const cachedResponse = await cache.match(modelUrl);
313 | 
314 |       if (!cachedResponse) {
315 |         return false;
316 |       }
317 | 
318 |       const metadataResponse = await cache.match(this.getCacheMetadataKey(modelUrl));
319 |       if (metadataResponse) {
320 |         try {
321 |           const metadata: CacheMetadata = await metadataResponse.json();
322 |           return !this.isCacheExpired(metadata);
323 |         } catch (error) {
324 |           console.warn('Failed to parse cache metadata for cache check:', error);
325 |           return false;
326 |         }
327 |       } else {
328 |         // No metadata means expired
329 |         return false;
330 |       }
331 |     } catch (error) {
332 |       console.error('Error checking model cache:', error);
333 |       return false;
334 |     }
335 |   }
336 | 
337 |   /**
338 |    * Check if any valid (non-expired) model cache exists
339 |    * @returns Promise<boolean> True if at least one valid model cache exists
340 |    */
341 |   public async hasAnyValidCache(): Promise<boolean> {
342 |     try {
343 |       const cache = await caches.open(CACHE_NAME);
344 |       const keys = await cache.keys();
345 | 
346 |       for (const request of keys) {
347 |         if (this.isMetadataUrl(request.url)) continue;
348 | 
349 |         const metadataResponse = await cache.match(this.getCacheMetadataKey(request.url));
350 |         if (metadataResponse) {
351 |           try {
352 |             const metadata: CacheMetadata = await metadataResponse.json();
353 |             if (!this.isCacheExpired(metadata)) {
354 |               return true; // Found at least one valid cache
355 |             }
356 |           } catch (error) {
357 |             // Skip invalid metadata
358 |             continue;
359 |           }
360 |         }
361 |       }
362 | 
363 |       return false;
364 |     } catch (error) {
365 |       console.error('Error checking for valid cache:', error);
366 |       return false;
367 |     }
368 |   }
369 | }
370 | 
```

--------------------------------------------------------------------------------
/app/chrome-extension/inject-scripts/keyboard-helper.js:
--------------------------------------------------------------------------------

```javascript
  1 | /* eslint-disable */
  2 | // keyboard-helper.js
  3 | // This script is injected into the page to handle keyboard event simulation
  4 | 
  5 | if (window.__KEYBOARD_HELPER_INITIALIZED__) {
  6 |   // Already initialized, skip
  7 | } else {
  8 |   window.__KEYBOARD_HELPER_INITIALIZED__ = true;
  9 | 
 10 |   // A map for special keys to their KeyboardEvent properties
 11 |   // Key names should be lowercase for matching
 12 |   const SPECIAL_KEY_MAP = {
 13 |     enter: { key: 'Enter', code: 'Enter', keyCode: 13 },
 14 |     tab: { key: 'Tab', code: 'Tab', keyCode: 9 },
 15 |     esc: { key: 'Escape', code: 'Escape', keyCode: 27 },
 16 |     escape: { key: 'Escape', code: 'Escape', keyCode: 27 },
 17 |     space: { key: ' ', code: 'Space', keyCode: 32 },
 18 |     backspace: { key: 'Backspace', code: 'Backspace', keyCode: 8 },
 19 |     delete: { key: 'Delete', code: 'Delete', keyCode: 46 },
 20 |     del: { key: 'Delete', code: 'Delete', keyCode: 46 },
 21 |     up: { key: 'ArrowUp', code: 'ArrowUp', keyCode: 38 },
 22 |     arrowup: { key: 'ArrowUp', code: 'ArrowUp', keyCode: 38 },
 23 |     down: { key: 'ArrowDown', code: 'ArrowDown', keyCode: 40 },
 24 |     arrowdown: { key: 'ArrowDown', code: 'ArrowDown', keyCode: 40 },
 25 |     left: { key: 'ArrowLeft', code: 'ArrowLeft', keyCode: 37 },
 26 |     arrowleft: { key: 'ArrowLeft', code: 'ArrowLeft', keyCode: 37 },
 27 |     right: { key: 'ArrowRight', code: 'ArrowRight', keyCode: 39 },
 28 |     arrowright: { key: 'ArrowRight', code: 'ArrowRight', keyCode: 39 },
 29 |     home: { key: 'Home', code: 'Home', keyCode: 36 },
 30 |     end: { key: 'End', code: 'End', keyCode: 35 },
 31 |     pageup: { key: 'PageUp', code: 'PageUp', keyCode: 33 },
 32 |     pagedown: { key: 'PageDown', code: 'PageDown', keyCode: 34 },
 33 |     insert: { key: 'Insert', code: 'Insert', keyCode: 45 },
 34 |     // Function keys
 35 |     ...Object.fromEntries(
 36 |       Array.from({ length: 12 }, (_, i) => [
 37 |         `f${i + 1}`,
 38 |         { key: `F${i + 1}`, code: `F${i + 1}`, keyCode: 112 + i },
 39 |       ]),
 40 |     ),
 41 |   };
 42 | 
 43 |   const MODIFIER_KEYS = {
 44 |     ctrl: 'ctrlKey',
 45 |     control: 'ctrlKey',
 46 |     alt: 'altKey',
 47 |     shift: 'shiftKey',
 48 |     meta: 'metaKey',
 49 |     command: 'metaKey',
 50 |     cmd: 'metaKey',
 51 |   };
 52 | 
 53 |   /**
 54 |    * Parses a key string (e.g., "Ctrl+Shift+A", "Enter") into a main key and modifiers.
 55 |    * @param {string} keyString - String representation of a single key press (can include modifiers).
 56 |    * @returns { {key: string, code: string, keyCode: number, charCode?: number, modifiers: {ctrlKey:boolean, altKey:boolean, shiftKey:boolean, metaKey:boolean}} | null }
 57 |    *          Returns null if the keyString is invalid or represents only modifiers.
 58 |    */
 59 |   function parseSingleKeyCombination(keyString) {
 60 |     const parts = keyString.split('+').map((part) => part.trim().toLowerCase());
 61 |     const modifiers = {
 62 |       ctrlKey: false,
 63 |       altKey: false,
 64 |       shiftKey: false,
 65 |       metaKey: false,
 66 |     };
 67 |     let mainKeyPart = null;
 68 | 
 69 |     for (const part of parts) {
 70 |       if (MODIFIER_KEYS[part]) {
 71 |         modifiers[MODIFIER_KEYS[part]] = true;
 72 |       } else if (mainKeyPart === null) {
 73 |         // First non-modifier is the main key
 74 |         mainKeyPart = part;
 75 |       } else {
 76 |         // Invalid format: multiple main keys in a single combination (e.g., "Ctrl+A+B")
 77 |         console.error(`Invalid key combination string: ${keyString}. Multiple main keys found.`);
 78 |         return null;
 79 |       }
 80 |     }
 81 | 
 82 |     if (!mainKeyPart) {
 83 |       // This case could happen if the keyString is something like "Ctrl+" or just "Ctrl"
 84 |       // If the intent was to press JUST 'Control', the input should be 'Control' not 'Control+'
 85 |       // Let's check if mainKeyPart is actually a modifier name used as a main key
 86 |       if (Object.keys(MODIFIER_KEYS).includes(parts[parts.length - 1]) && parts.length === 1) {
 87 |         mainKeyPart = parts[parts.length - 1]; // e.g. user wants to press "Control" key itself
 88 |         // For "Control" key itself, key: "Control", code: "ControlLeft" (or Right)
 89 |         if (mainKeyPart === 'ctrl' || mainKeyPart === 'control')
 90 |           return { key: 'Control', code: 'ControlLeft', keyCode: 17, modifiers };
 91 |         if (mainKeyPart === 'alt') return { key: 'Alt', code: 'AltLeft', keyCode: 18, modifiers };
 92 |         if (mainKeyPart === 'shift')
 93 |           return { key: 'Shift', code: 'ShiftLeft', keyCode: 16, modifiers };
 94 |         if (mainKeyPart === 'meta' || mainKeyPart === 'command' || mainKeyPart === 'cmd')
 95 |           return { key: 'Meta', code: 'MetaLeft', keyCode: 91, modifiers };
 96 |       } else {
 97 |         console.error(`Invalid key combination string: ${keyString}. No main key specified.`);
 98 |         return null;
 99 |       }
100 |     }
101 | 
102 |     const specialKey = SPECIAL_KEY_MAP[mainKeyPart];
103 |     if (specialKey) {
104 |       return { ...specialKey, modifiers };
105 |     }
106 | 
107 |     // For single characters or other unmapped keys
108 |     if (mainKeyPart.length === 1) {
109 |       const charCode = mainKeyPart.charCodeAt(0);
110 |       // If Shift is active and it's a letter, use the uppercase version for 'key'
111 |       // This mimics more closely how keyboards behave.
112 |       let keyChar = mainKeyPart;
113 |       if (modifiers.shiftKey && mainKeyPart.match(/^[a-z]$/i)) {
114 |         keyChar = mainKeyPart.toUpperCase();
115 |       }
116 | 
117 |       return {
118 |         key: keyChar,
119 |         code: `Key${mainKeyPart.toUpperCase()}`, // 'a' -> KeyA, 'A' -> KeyA
120 |         keyCode: charCode,
121 |         charCode: charCode, // charCode is legacy, but some old systems might use it
122 |         modifiers,
123 |       };
124 |     }
125 | 
126 |     console.error(`Unknown key: ${mainKeyPart} in string "${keyString}"`);
127 |     return null; // Or handle as an error
128 |   }
129 | 
130 |   /**
131 |    * Simulates a single key press (keydown, (keypress), keyup) for a parsed key.
132 |    * @param { {key: string, code: string, keyCode: number, charCode?: number, modifiers: object} } parsedKeyInfo
133 |    * @param {Element} element - Target element.
134 |    * @returns {{success: boolean, error?: string}}
135 |    */
136 |   function dispatchKeyEvents(parsedKeyInfo, element) {
137 |     if (!parsedKeyInfo) return { success: false, error: 'Invalid key info provided for dispatch.' };
138 | 
139 |     const { key, code, keyCode, charCode, modifiers } = parsedKeyInfo;
140 | 
141 |     const eventOptions = {
142 |       key: key,
143 |       code: code,
144 |       bubbles: true,
145 |       cancelable: true,
146 |       composed: true, // Important for shadow DOM
147 |       view: window,
148 |       ...modifiers, // ctrlKey, altKey, shiftKey, metaKey
149 |       // keyCode/which are deprecated but often set for compatibility
150 |       keyCode: keyCode || (key.length === 1 ? key.charCodeAt(0) : 0),
151 |       which: keyCode || (key.length === 1 ? key.charCodeAt(0) : 0),
152 |     };
153 | 
154 |     try {
155 |       const kdRes = element.dispatchEvent(new KeyboardEvent('keydown', eventOptions));
156 | 
157 |       // keypress is deprecated, but simulate if it's a character key or Enter
158 |       // Only dispatch if keydown was not cancelled and it's a character producing key
159 |       if (kdRes && (key.length === 1 || key === 'Enter' || key === ' ')) {
160 |         const keypressOptions = { ...eventOptions };
161 |         if (charCode) keypressOptions.charCode = charCode;
162 |         element.dispatchEvent(new KeyboardEvent('keypress', keypressOptions));
163 |       }
164 | 
165 |       element.dispatchEvent(new KeyboardEvent('keyup', eventOptions));
166 |       return { success: true };
167 |     } catch (error) {
168 |       console.error(`Error dispatching key events for "${key}":`, error);
169 |       return {
170 |         success: false,
171 |         error: `Error dispatching key events for "${key}": ${error.message}`,
172 |       };
173 |     }
174 |   }
175 | 
176 |   /**
177 |    * Simulate keyboard events on an element or document
178 |    * @param {string} keysSequenceString - String representation of key(s) (e.g., "Enter", "Ctrl+C, A, B")
179 |    * @param {Element} targetElement - Element to dispatch events on (optional)
180 |    * @param {number} delay - Delay between key sequences in milliseconds (optional)
181 |    * @returns {Promise<Object>} - Result of the keyboard operation
182 |    */
183 |   async function simulateKeyboard(keysSequenceString, targetElement = null, delay = 0) {
184 |     try {
185 |       const element = targetElement || document.activeElement || document.body;
186 | 
187 |       if (element !== document.activeElement && typeof element.focus === 'function') {
188 |         element.focus();
189 |         await new Promise((resolve) => setTimeout(resolve, 50)); // Small delay for focus
190 |       }
191 | 
192 |       const keyCombinations = keysSequenceString
193 |         .split(',')
194 |         .map((k) => k.trim())
195 |         .filter((k) => k.length > 0);
196 |       const operationResults = [];
197 | 
198 |       for (let i = 0; i < keyCombinations.length; i++) {
199 |         const comboString = keyCombinations[i];
200 |         const parsedKeyInfo = parseSingleKeyCombination(comboString);
201 | 
202 |         if (!parsedKeyInfo) {
203 |           operationResults.push({
204 |             keyCombination: comboString,
205 |             success: false,
206 |             error: `Invalid key string or combination: ${comboString}`,
207 |           });
208 |           continue; // Skip to next combination in sequence
209 |         }
210 | 
211 |         const dispatchResult = dispatchKeyEvents(parsedKeyInfo, element);
212 |         operationResults.push({
213 |           keyCombination: comboString,
214 |           ...dispatchResult,
215 |         });
216 | 
217 |         if (dispatchResult.error) {
218 |           // Optionally, decide if sequence should stop on first error
219 |           // For now, we continue but log the error in results
220 |           console.warn(
221 |             `Failed to simulate key combination "${comboString}": ${dispatchResult.error}`,
222 |           );
223 |         }
224 | 
225 |         if (delay > 0 && i < keyCombinations.length - 1) {
226 |           await new Promise((resolve) => setTimeout(resolve, delay));
227 |         }
228 |       }
229 | 
230 |       // Check if all individual operations were successful
231 |       const overallSuccess = operationResults.every((r) => r.success);
232 | 
233 |       return {
234 |         success: overallSuccess,
235 |         message: overallSuccess
236 |           ? `Keyboard events simulated successfully: ${keysSequenceString}`
237 |           : `Some keyboard events failed for: ${keysSequenceString}`,
238 |         results: operationResults, // Detailed results for each key combination
239 |         targetElement: {
240 |           tagName: element.tagName,
241 |           id: element.id,
242 |           className: element.className,
243 |           type: element.type, // if applicable e.g. for input
244 |         },
245 |       };
246 |     } catch (error) {
247 |       console.error('Error in simulateKeyboard:', error);
248 |       return {
249 |         success: false,
250 |         error: `Error simulating keyboard events: ${error.message}`,
251 |         results: [],
252 |       };
253 |     }
254 |   }
255 | 
256 |   // Listener for messages from the extension
257 |   chrome.runtime.onMessage.addListener((request, _sender, sendResponse) => {
258 |     if (request.action === 'simulateKeyboard') {
259 |       let targetEl = null;
260 |       if (request.selector) {
261 |         targetEl = document.querySelector(request.selector);
262 |         if (!targetEl) {
263 |           sendResponse({
264 |             success: false,
265 |             error: `Element with selector "${request.selector}" not found`,
266 |             results: [],
267 |           });
268 |           return true; // Keep channel open for async response
269 |         }
270 |       }
271 | 
272 |       simulateKeyboard(request.keys, targetEl, request.delay)
273 |         .then(sendResponse)
274 |         .catch((error) => {
275 |           // This catch is for unexpected errors in simulateKeyboard promise chain itself
276 |           console.error('Unexpected error in simulateKeyboard promise chain:', error);
277 |           sendResponse({
278 |             success: false,
279 |             error: `Unexpected error during keyboard simulation: ${error.message}`,
280 |             results: [],
281 |           });
282 |         });
283 |       return true; // Indicates async response is expected
284 |     } else if (request.action === 'chrome_keyboard_ping') {
285 |       sendResponse({ status: 'pong', initialized: true }); // Respond that it's initialized
286 |       return false; // Synchronous response
287 |     }
288 |     // Not our message, or no async response needed
289 |     return false;
290 |   });
291 | }
292 | 
```

--------------------------------------------------------------------------------
/app/chrome-extension/entrypoints/background/semantic-similarity.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import type { ModelPreset } from '@/utils/semantic-similarity-engine';
  2 | import { OffscreenManager } from '@/utils/offscreen-manager';
  3 | import { BACKGROUND_MESSAGE_TYPES, OFFSCREEN_MESSAGE_TYPES } from '@/common/message-types';
  4 | import { STORAGE_KEYS, ERROR_MESSAGES } from '@/common/constants';
  5 | import { hasAnyModelCache } from '@/utils/semantic-similarity-engine';
  6 | 
  7 | /**
  8 |  * Model configuration state management interface
  9 |  */
 10 | interface ModelConfig {
 11 |   modelPreset: ModelPreset;
 12 |   modelVersion: 'full' | 'quantized' | 'compressed';
 13 |   modelDimension: number;
 14 | }
 15 | 
 16 | let currentBackgroundModelConfig: ModelConfig | null = null;
 17 | 
 18 | /**
 19 |  * Initialize semantic engine only if model cache exists
 20 |  * This is called during plugin startup to avoid downloading models unnecessarily
 21 |  */
 22 | export async function initializeSemanticEngineIfCached(): Promise<boolean> {
 23 |   try {
 24 |     console.log('Background: Checking if semantic engine should be initialized from cache...');
 25 | 
 26 |     const hasCachedModel = await hasAnyModelCache();
 27 |     if (!hasCachedModel) {
 28 |       console.log('Background: No cached models found, skipping semantic engine initialization');
 29 |       return false;
 30 |     }
 31 | 
 32 |     console.log('Background: Found cached models, initializing semantic engine...');
 33 |     await initializeDefaultSemanticEngine();
 34 |     return true;
 35 |   } catch (error) {
 36 |     console.error('Background: Error during conditional semantic engine initialization:', error);
 37 |     return false;
 38 |   }
 39 | }
 40 | 
 41 | /**
 42 |  * Initialize default semantic engine model
 43 |  */
 44 | export async function initializeDefaultSemanticEngine(): Promise<void> {
 45 |   try {
 46 |     console.log('Background: Initializing default semantic engine...');
 47 | 
 48 |     // Update status to initializing
 49 |     await updateModelStatus('initializing', 0);
 50 | 
 51 |     const result = await chrome.storage.local.get([STORAGE_KEYS.SEMANTIC_MODEL, 'selectedVersion']);
 52 |     const defaultModel =
 53 |       (result[STORAGE_KEYS.SEMANTIC_MODEL] as ModelPreset) || 'multilingual-e5-small';
 54 |     const defaultVersion =
 55 |       (result.selectedVersion as 'full' | 'quantized' | 'compressed') || 'quantized';
 56 | 
 57 |     const { PREDEFINED_MODELS } = await import('@/utils/semantic-similarity-engine');
 58 |     const modelInfo = PREDEFINED_MODELS[defaultModel];
 59 | 
 60 |     await OffscreenManager.getInstance().ensureOffscreenDocument();
 61 | 
 62 |     const response = await chrome.runtime.sendMessage({
 63 |       target: 'offscreen',
 64 |       type: OFFSCREEN_MESSAGE_TYPES.SIMILARITY_ENGINE_INIT,
 65 |       config: {
 66 |         useLocalFiles: false,
 67 |         modelPreset: defaultModel,
 68 |         modelVersion: defaultVersion,
 69 |         modelDimension: modelInfo.dimension,
 70 |         forceOffscreen: true,
 71 |       },
 72 |     });
 73 | 
 74 |     if (response && response.success) {
 75 |       currentBackgroundModelConfig = {
 76 |         modelPreset: defaultModel,
 77 |         modelVersion: defaultVersion,
 78 |         modelDimension: modelInfo.dimension,
 79 |       };
 80 |       console.log('Semantic engine initialized successfully:', currentBackgroundModelConfig);
 81 | 
 82 |       // Update status to ready
 83 |       await updateModelStatus('ready', 100);
 84 | 
 85 |       // Also initialize ContentIndexer now that semantic engine is ready
 86 |       try {
 87 |         const { getGlobalContentIndexer } = await import('@/utils/content-indexer');
 88 |         const contentIndexer = getGlobalContentIndexer();
 89 |         contentIndexer.startSemanticEngineInitialization();
 90 |         console.log('ContentIndexer initialization triggered after semantic engine initialization');
 91 |       } catch (indexerError) {
 92 |         console.warn(
 93 |           'Failed to initialize ContentIndexer after semantic engine initialization:',
 94 |           indexerError,
 95 |         );
 96 |       }
 97 |     } else {
 98 |       const errorMessage = response?.error || ERROR_MESSAGES.TOOL_EXECUTION_FAILED;
 99 |       await updateModelStatus('error', 0, errorMessage, 'unknown');
100 |       throw new Error(errorMessage);
101 |     }
102 |   } catch (error: any) {
103 |     console.error('Background: Failed to initialize default semantic engine:', error);
104 |     const errorMessage = error?.message || 'Unknown error during semantic engine initialization';
105 |     await updateModelStatus('error', 0, errorMessage, 'unknown');
106 |     // Don't throw error, let the extension continue running
107 |   }
108 | }
109 | 
110 | /**
111 |  * Check if model switch is needed
112 |  */
113 | function needsModelSwitch(
114 |   modelPreset: ModelPreset,
115 |   modelVersion: 'full' | 'quantized' | 'compressed',
116 |   modelDimension?: number,
117 | ): boolean {
118 |   if (!currentBackgroundModelConfig) {
119 |     return true;
120 |   }
121 | 
122 |   const keyFields = ['modelPreset', 'modelVersion', 'modelDimension'];
123 |   for (const field of keyFields) {
124 |     const newValue =
125 |       field === 'modelPreset'
126 |         ? modelPreset
127 |         : field === 'modelVersion'
128 |           ? modelVersion
129 |           : modelDimension;
130 |     if (newValue !== currentBackgroundModelConfig[field as keyof ModelConfig]) {
131 |       return true;
132 |     }
133 |   }
134 | 
135 |   return false;
136 | }
137 | 
138 | /**
139 |  * Handle model switching
140 |  */
141 | export async function handleModelSwitch(
142 |   modelPreset: ModelPreset,
143 |   modelVersion: 'full' | 'quantized' | 'compressed' = 'quantized',
144 |   modelDimension?: number,
145 |   previousDimension?: number,
146 | ): Promise<{ success: boolean; error?: string }> {
147 |   try {
148 |     const needsSwitch = needsModelSwitch(modelPreset, modelVersion, modelDimension);
149 |     if (!needsSwitch) {
150 |       await updateModelStatus('ready', 100);
151 |       return { success: true };
152 |     }
153 | 
154 |     await updateModelStatus('downloading', 0);
155 | 
156 |     try {
157 |       await OffscreenManager.getInstance().ensureOffscreenDocument();
158 |     } catch (offscreenError) {
159 |       console.error('Background: Failed to create offscreen document:', offscreenError);
160 |       const errorMessage = `Failed to create offscreen document: ${offscreenError}`;
161 |       await updateModelStatus('error', 0, errorMessage, 'unknown');
162 |       return { success: false, error: errorMessage };
163 |     }
164 | 
165 |     const response = await chrome.runtime.sendMessage({
166 |       target: 'offscreen',
167 |       type: OFFSCREEN_MESSAGE_TYPES.SIMILARITY_ENGINE_INIT,
168 |       config: {
169 |         useLocalFiles: false,
170 |         modelPreset: modelPreset,
171 |         modelVersion: modelVersion,
172 |         modelDimension: modelDimension,
173 |         forceOffscreen: true,
174 |       },
175 |     });
176 | 
177 |     if (response && response.success) {
178 |       currentBackgroundModelConfig = {
179 |         modelPreset: modelPreset,
180 |         modelVersion: modelVersion,
181 |         modelDimension: modelDimension!,
182 |       };
183 | 
184 |       // Only reinitialize ContentIndexer when dimension changes
185 |       try {
186 |         if (modelDimension && previousDimension && modelDimension !== previousDimension) {
187 |           const { getGlobalContentIndexer } = await import('@/utils/content-indexer');
188 |           const contentIndexer = getGlobalContentIndexer();
189 |           await contentIndexer.reinitialize();
190 |         }
191 |       } catch (indexerError) {
192 |         console.warn('Background: Failed to reinitialize ContentIndexer:', indexerError);
193 |       }
194 | 
195 |       await updateModelStatus('ready', 100);
196 |       return { success: true };
197 |     } else {
198 |       const errorMessage = response?.error || 'Failed to switch model';
199 |       const errorType = analyzeErrorType(errorMessage);
200 |       await updateModelStatus('error', 0, errorMessage, errorType);
201 |       throw new Error(errorMessage);
202 |     }
203 |   } catch (error: any) {
204 |     console.error('Model switch failed:', error);
205 |     const errorMessage = error.message || 'Unknown error';
206 |     const errorType = analyzeErrorType(errorMessage);
207 |     await updateModelStatus('error', 0, errorMessage, errorType);
208 |     return { success: false, error: errorMessage };
209 |   }
210 | }
211 | 
212 | /**
213 |  * Get model status
214 |  */
215 | export async function handleGetModelStatus(): Promise<{
216 |   success: boolean;
217 |   status?: any;
218 |   error?: string;
219 | }> {
220 |   try {
221 |     if (typeof chrome === 'undefined' || !chrome.storage || !chrome.storage.local) {
222 |       console.error('Background: chrome.storage.local is not available for status query');
223 |       return {
224 |         success: true,
225 |         status: {
226 |           initializationStatus: 'idle',
227 |           downloadProgress: 0,
228 |           isDownloading: false,
229 |           lastUpdated: Date.now(),
230 |         },
231 |       };
232 |     }
233 | 
234 |     const result = await chrome.storage.local.get(['modelState']);
235 |     const modelState = result.modelState || {
236 |       status: 'idle',
237 |       downloadProgress: 0,
238 |       isDownloading: false,
239 |       lastUpdated: Date.now(),
240 |     };
241 | 
242 |     return {
243 |       success: true,
244 |       status: {
245 |         initializationStatus: modelState.status,
246 |         downloadProgress: modelState.downloadProgress,
247 |         isDownloading: modelState.isDownloading,
248 |         lastUpdated: modelState.lastUpdated,
249 |         errorMessage: modelState.errorMessage,
250 |         errorType: modelState.errorType,
251 |       },
252 |     };
253 |   } catch (error: any) {
254 |     console.error('Failed to get model status:', error);
255 |     return { success: false, error: error.message };
256 |   }
257 | }
258 | 
259 | /**
260 |  * Update model status
261 |  */
262 | export async function updateModelStatus(
263 |   status: string,
264 |   progress: number,
265 |   errorMessage?: string,
266 |   errorType?: string,
267 | ): Promise<void> {
268 |   try {
269 |     // Check if chrome.storage is available
270 |     if (typeof chrome === 'undefined' || !chrome.storage || !chrome.storage.local) {
271 |       console.error('Background: chrome.storage.local is not available for status update');
272 |       return;
273 |     }
274 | 
275 |     const modelState = {
276 |       status,
277 |       downloadProgress: progress,
278 |       isDownloading: status === 'downloading' || status === 'initializing',
279 |       lastUpdated: Date.now(),
280 |       errorMessage: errorMessage || '',
281 |       errorType: errorType || '',
282 |     };
283 |     await chrome.storage.local.set({ modelState });
284 |   } catch (error) {
285 |     console.error('Failed to update model status:', error);
286 |   }
287 | }
288 | 
289 | /**
290 |  * Handle model status updates from offscreen document
291 |  */
292 | export async function handleUpdateModelStatus(
293 |   modelState: any,
294 | ): Promise<{ success: boolean; error?: string }> {
295 |   try {
296 |     // Check if chrome.storage is available
297 |     if (typeof chrome === 'undefined' || !chrome.storage || !chrome.storage.local) {
298 |       console.error('Background: chrome.storage.local is not available');
299 |       return { success: false, error: 'chrome.storage.local is not available' };
300 |     }
301 | 
302 |     await chrome.storage.local.set({ modelState });
303 |     return { success: true };
304 |   } catch (error: any) {
305 |     console.error('Background: Failed to update model status:', error);
306 |     return { success: false, error: error.message };
307 |   }
308 | }
309 | 
310 | /**
311 |  * Analyze error type based on error message
312 |  */
313 | function analyzeErrorType(errorMessage: string): 'network' | 'file' | 'unknown' {
314 |   const message = errorMessage.toLowerCase();
315 | 
316 |   if (
317 |     message.includes('network') ||
318 |     message.includes('fetch') ||
319 |     message.includes('timeout') ||
320 |     message.includes('connection') ||
321 |     message.includes('cors') ||
322 |     message.includes('failed to fetch')
323 |   ) {
324 |     return 'network';
325 |   }
326 | 
327 |   if (
328 |     message.includes('corrupt') ||
329 |     message.includes('invalid') ||
330 |     message.includes('format') ||
331 |     message.includes('parse') ||
332 |     message.includes('decode') ||
333 |     message.includes('onnx')
334 |   ) {
335 |     return 'file';
336 |   }
337 | 
338 |   return 'unknown';
339 | }
340 | 
341 | /**
342 |  * Initialize semantic similarity module message listeners
343 |  */
344 | export const initSemanticSimilarityListener = () => {
345 |   chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
346 |     if (message.type === BACKGROUND_MESSAGE_TYPES.SWITCH_SEMANTIC_MODEL) {
347 |       handleModelSwitch(
348 |         message.modelPreset,
349 |         message.modelVersion,
350 |         message.modelDimension,
351 |         message.previousDimension,
352 |       )
353 |         .then((result: { success: boolean; error?: string }) => sendResponse(result))
354 |         .catch((error: any) => sendResponse({ success: false, error: error.message }));
355 |       return true;
356 |     } else if (message.type === BACKGROUND_MESSAGE_TYPES.GET_MODEL_STATUS) {
357 |       handleGetModelStatus()
358 |         .then((result: { success: boolean; status?: any; error?: string }) => sendResponse(result))
359 |         .catch((error: any) => sendResponse({ success: false, error: error.message }));
360 |       return true;
361 |     } else if (message.type === BACKGROUND_MESSAGE_TYPES.UPDATE_MODEL_STATUS) {
362 |       handleUpdateModelStatus(message.modelState)
363 |         .then((result: { success: boolean; error?: string }) => sendResponse(result))
364 |         .catch((error: any) => sendResponse({ success: false, error: error.message }));
365 |       return true;
366 |     } else if (message.type === BACKGROUND_MESSAGE_TYPES.INITIALIZE_SEMANTIC_ENGINE) {
367 |       initializeDefaultSemanticEngine()
368 |         .then(() => sendResponse({ success: true }))
369 |         .catch((error: any) => sendResponse({ success: false, error: error.message }));
370 |       return true;
371 |     }
372 |   });
373 | };
374 | 
```

--------------------------------------------------------------------------------
/app/chrome-extension/inject-scripts/interactive-elements-helper.js:
--------------------------------------------------------------------------------

```javascript
  1 | /* eslint-disable */
  2 | // interactive-elements-helper.js
  3 | // This script is injected into the page to find interactive elements.
  4 | // Final version by Calvin, featuring a multi-layered fallback strategy
  5 | // and comprehensive element support, built on a performant and reliable core.
  6 | 
  7 | (function () {
  8 |   // Prevent re-initialization
  9 |   if (window.__INTERACTIVE_ELEMENTS_HELPER_INITIALIZED__) {
 10 |     return;
 11 |   }
 12 |   window.__INTERACTIVE_ELEMENTS_HELPER_INITIALIZED__ = true;
 13 | 
 14 |   /**
 15 |    * @typedef {Object} ElementInfo
 16 |    * @property {string} type - The type of the element (e.g., 'button', 'link').
 17 |    * @property {string} selector - A CSS selector to uniquely identify the element.
 18 |    * @property {string} text - The visible text or accessible name of the element.
 19 |    * @property {boolean} isInteractive - Whether the element is currently interactive.
 20 |    * @property {Object} [coordinates] - The coordinates of the element if requested.
 21 |    * @property {boolean} [disabled] - For elements that can be disabled.
 22 |    * @property {string} [href] - For links.
 23 |    * @property {boolean} [checked] - for checkboxes and radio buttons.
 24 |    */
 25 | 
 26 |   /**
 27 |    * Configuration for element types and their corresponding selectors.
 28 |    * Now more comprehensive with common ARIA roles.
 29 |    */
 30 |   const ELEMENT_CONFIG = {
 31 |     button: 'button, input[type="button"], input[type="submit"], [role="button"]',
 32 |     link: 'a[href], [role="link"]',
 33 |     input:
 34 |       'input:not([type="button"]):not([type="submit"]):not([type="checkbox"]):not([type="radio"])',
 35 |     checkbox: 'input[type="checkbox"], [role="checkbox"]',
 36 |     radio: 'input[type="radio"], [role="radio"]',
 37 |     textarea: 'textarea',
 38 |     select: 'select',
 39 |     tab: '[role="tab"]',
 40 |     // Generic interactive elements: combines tabindex, common roles, and explicit handlers.
 41 |     // This is the key to finding custom-built interactive components.
 42 |     interactive: `[onclick], [tabindex]:not([tabindex^="-"]), [role="menuitem"], [role="slider"], [role="option"], [role="treeitem"]`,
 43 |   };
 44 | 
 45 |   // A combined selector for ANY interactive element, used in the fallback logic.
 46 |   const ANY_INTERACTIVE_SELECTOR = Object.values(ELEMENT_CONFIG).join(', ');
 47 | 
 48 |   // --- Core Helper Functions ---
 49 | 
 50 |   /**
 51 |    * Checks if an element is genuinely visible on the page.
 52 |    * "Visible" means it's not styled with display:none, visibility:hidden, etc.
 53 |    * This check intentionally IGNORES whether the element is within the current viewport.
 54 |    * @param {Element} el The element to check.
 55 |    * @returns {boolean} True if the element is visible.
 56 |    */
 57 |   function isElementVisible(el) {
 58 |     if (!el || !el.isConnected) return false;
 59 | 
 60 |     const style = window.getComputedStyle(el);
 61 |     if (
 62 |       style.display === 'none' ||
 63 |       style.visibility === 'hidden' ||
 64 |       parseFloat(style.opacity) === 0
 65 |     ) {
 66 |       return false;
 67 |     }
 68 | 
 69 |     const rect = el.getBoundingClientRect();
 70 |     return rect.width > 0 || rect.height > 0 || el.tagName === 'A'; // Allow zero-size anchors as they can still be navigated
 71 |   }
 72 | 
 73 |   /**
 74 |    * Checks if an element is considered interactive (not disabled or hidden from accessibility).
 75 |    * @param {Element} el The element to check.
 76 |    * @returns {boolean} True if the element is interactive.
 77 |    */
 78 |   function isElementInteractive(el) {
 79 |     if (el.hasAttribute('disabled') || el.getAttribute('aria-disabled') === 'true') {
 80 |       return false;
 81 |     }
 82 |     if (el.closest('[aria-hidden="true"]')) {
 83 |       return false;
 84 |     }
 85 |     return true;
 86 |   }
 87 | 
 88 |   /**
 89 |    * Generates a reasonably stable CSS selector for a given element.
 90 |    * @param {Element} el The element.
 91 |    * @returns {string} A CSS selector.
 92 |    */
 93 |   function generateSelector(el) {
 94 |     if (!(el instanceof Element)) return '';
 95 | 
 96 |     if (el.id) {
 97 |       const idSelector = `#${CSS.escape(el.id)}`;
 98 |       if (document.querySelectorAll(idSelector).length === 1) return idSelector;
 99 |     }
100 | 
101 |     for (const attr of ['data-testid', 'data-cy', 'name']) {
102 |       const attrValue = el.getAttribute(attr);
103 |       if (attrValue) {
104 |         const attrSelector = `[${attr}="${CSS.escape(attrValue)}"]`;
105 |         if (document.querySelectorAll(attrSelector).length === 1) return attrSelector;
106 |       }
107 |     }
108 | 
109 |     let path = '';
110 |     let current = el;
111 |     while (current && current.nodeType === Node.ELEMENT_NODE && current.tagName !== 'BODY') {
112 |       let selector = current.tagName.toLowerCase();
113 |       const parent = current.parentElement;
114 |       if (parent) {
115 |         const siblings = Array.from(parent.children).filter(
116 |           (child) => child.tagName === current.tagName,
117 |         );
118 |         if (siblings.length > 1) {
119 |           const index = siblings.indexOf(current) + 1;
120 |           selector += `:nth-of-type(${index})`;
121 |         }
122 |       }
123 |       path = path ? `${selector} > ${path}` : selector;
124 |       current = parent;
125 |     }
126 |     return path ? `body > ${path}` : 'body';
127 |   }
128 | 
129 |   /**
130 |    * Finds the accessible name for an element (label, aria-label, etc.).
131 |    * @param {Element} el The element.
132 |    * @returns {string} The accessible name.
133 |    */
134 |   function getAccessibleName(el) {
135 |     const labelledby = el.getAttribute('aria-labelledby');
136 |     if (labelledby) {
137 |       const labelElement = document.getElementById(labelledby);
138 |       if (labelElement) return labelElement.textContent?.trim() || '';
139 |     }
140 |     const ariaLabel = el.getAttribute('aria-label');
141 |     if (ariaLabel) return ariaLabel.trim();
142 |     if (el.id) {
143 |       const label = document.querySelector(`label[for="${el.id}"]`);
144 |       if (label) return label.textContent?.trim() || '';
145 |     }
146 |     const parentLabel = el.closest('label');
147 |     if (parentLabel) return parentLabel.textContent?.trim() || '';
148 |     return (
149 |       el.getAttribute('placeholder') ||
150 |       el.getAttribute('value') ||
151 |       el.textContent?.trim() ||
152 |       el.getAttribute('title') ||
153 |       ''
154 |     );
155 |   }
156 | 
157 |   /**
158 |    * Simple subsequence matching for fuzzy search.
159 |    * @param {string} text The text to search within.
160 |    * @param {string} query The query subsequence.
161 |    * @returns {boolean}
162 |    */
163 |   function fuzzyMatch(text, query) {
164 |     if (!text || !query) return false;
165 |     const lowerText = text.toLowerCase();
166 |     const lowerQuery = query.toLowerCase();
167 |     let textIndex = 0;
168 |     let queryIndex = 0;
169 |     while (textIndex < lowerText.length && queryIndex < lowerQuery.length) {
170 |       if (lowerText[textIndex] === lowerQuery[queryIndex]) {
171 |         queryIndex++;
172 |       }
173 |       textIndex++;
174 |     }
175 |     return queryIndex === lowerQuery.length;
176 |   }
177 | 
178 |   /**
179 |    * Creates the standardized info object for an element.
180 |    * Modified to handle the new 'text' type from the final fallback.
181 |    */
182 |   function createElementInfo(el, type, includeCoordinates, isInteractiveOverride = null) {
183 |     const isActuallyInteractive = isElementInteractive(el);
184 |     const info = {
185 |       type,
186 |       selector: generateSelector(el),
187 |       text: getAccessibleName(el) || el.textContent?.trim(),
188 |       isInteractive: isInteractiveOverride !== null ? isInteractiveOverride : isActuallyInteractive,
189 |       disabled: el.hasAttribute('disabled') || el.getAttribute('aria-disabled') === 'true',
190 |     };
191 |     if (includeCoordinates) {
192 |       const rect = el.getBoundingClientRect();
193 |       info.coordinates = {
194 |         x: rect.left + rect.width / 2,
195 |         y: rect.top + rect.height / 2,
196 |         rect: {
197 |           x: rect.x,
198 |           y: rect.y,
199 |           width: rect.width,
200 |           height: rect.height,
201 |           top: rect.top,
202 |           right: rect.right,
203 |           bottom: rect.bottom,
204 |           left: rect.left,
205 |         },
206 |       };
207 |     }
208 |     return info;
209 |   }
210 | 
211 |   /**
212 |    * [CORE UTILITY] Finds interactive elements based on a set of types.
213 |    * This is our high-performance Layer 1 search function.
214 |    */
215 |   function findInteractiveElements(options = {}) {
216 |     const { textQuery, includeCoordinates = true, types = Object.keys(ELEMENT_CONFIG) } = options;
217 | 
218 |     const selectorsToFind = types
219 |       .map((type) => ELEMENT_CONFIG[type])
220 |       .filter(Boolean)
221 |       .join(', ');
222 |     if (!selectorsToFind) return [];
223 | 
224 |     const targetElements = Array.from(document.querySelectorAll(selectorsToFind));
225 |     const uniqueElements = new Set(targetElements);
226 |     const results = [];
227 | 
228 |     for (const el of uniqueElements) {
229 |       if (!isElementVisible(el) || !isElementInteractive(el)) continue;
230 | 
231 |       const accessibleName = getAccessibleName(el);
232 |       if (textQuery && !fuzzyMatch(accessibleName, textQuery)) continue;
233 | 
234 |       let elementType = 'unknown';
235 |       for (const [type, typeSelector] of Object.entries(ELEMENT_CONFIG)) {
236 |         if (el.matches(typeSelector)) {
237 |           elementType = type;
238 |           break;
239 |         }
240 |       }
241 |       results.push(createElementInfo(el, elementType, includeCoordinates));
242 |     }
243 |     return results;
244 |   }
245 | 
246 |   /**
247 |    * [ORCHESTRATOR] The main entry point that implements the 3-layer fallback logic.
248 |    * @param {object} options - The main search options.
249 |    * @returns {ElementInfo[]}
250 |    */
251 |   function findElementsByTextWithFallback(options = {}) {
252 |     const { textQuery, includeCoordinates = true } = options;
253 | 
254 |     if (!textQuery) {
255 |       return findInteractiveElements({ ...options, types: Object.keys(ELEMENT_CONFIG) });
256 |     }
257 | 
258 |     // --- Layer 1: High-reliability search for interactive elements matching text ---
259 |     let results = findInteractiveElements({ ...options, types: Object.keys(ELEMENT_CONFIG) });
260 |     if (results.length > 0) {
261 |       return results;
262 |     }
263 | 
264 |     // --- Layer 2: Find text, then find its interactive ancestor ---
265 |     const lowerCaseText = textQuery.toLowerCase();
266 |     const xPath = `//text()[contains(translate(., 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'), '${lowerCaseText}')]`;
267 |     const textNodes = document.evaluate(
268 |       xPath,
269 |       document,
270 |       null,
271 |       XPathResult.ORDERED_NODE_SNAPSHOT_TYPE,
272 |       null,
273 |     );
274 | 
275 |     const interactiveElements = new Set();
276 |     if (textNodes.snapshotLength > 0) {
277 |       for (let i = 0; i < textNodes.snapshotLength; i++) {
278 |         const parentElement = textNodes.snapshotItem(i).parentElement;
279 |         if (parentElement) {
280 |           const interactiveAncestor = parentElement.closest(ANY_INTERACTIVE_SELECTOR);
281 |           if (
282 |             interactiveAncestor &&
283 |             isElementVisible(interactiveAncestor) &&
284 |             isElementInteractive(interactiveAncestor)
285 |           ) {
286 |             interactiveElements.add(interactiveAncestor);
287 |           }
288 |         }
289 |       }
290 | 
291 |       if (interactiveElements.size > 0) {
292 |         return Array.from(interactiveElements).map((el) => {
293 |           let elementType = 'interactive';
294 |           for (const [type, typeSelector] of Object.entries(ELEMENT_CONFIG)) {
295 |             if (el.matches(typeSelector)) {
296 |               elementType = type;
297 |               break;
298 |             }
299 |           }
300 |           return createElementInfo(el, elementType, includeCoordinates);
301 |         });
302 |       }
303 |     }
304 | 
305 |     // --- Layer 3: Final fallback, return any element containing the text ---
306 |     const leafElements = new Set();
307 |     for (let i = 0; i < textNodes.snapshotLength; i++) {
308 |       const parentElement = textNodes.snapshotItem(i).parentElement;
309 |       if (parentElement && isElementVisible(parentElement)) {
310 |         leafElements.add(parentElement);
311 |       }
312 |     }
313 | 
314 |     const finalElements = Array.from(leafElements).filter((el) => {
315 |       return ![...leafElements].some((otherEl) => el !== otherEl && el.contains(otherEl));
316 |     });
317 | 
318 |     return finalElements.map((el) => createElementInfo(el, 'text', includeCoordinates, true));
319 |   }
320 | 
321 |   // --- Chrome Message Listener ---
322 |   chrome.runtime.onMessage.addListener((request, _sender, sendResponse) => {
323 |     if (request.action === 'getInteractiveElements') {
324 |       try {
325 |         let elements;
326 |         if (request.selector) {
327 |           // If a selector is provided, bypass the text-based logic and use a direct query.
328 |           const foundEls = Array.from(document.querySelectorAll(request.selector));
329 |           elements = foundEls.map((el) =>
330 |             createElementInfo(
331 |               el,
332 |               'selected',
333 |               request.includeCoordinates !== false,
334 |               isElementInteractive(el),
335 |             ),
336 |           );
337 |         } else {
338 |           // Otherwise, use our powerful multi-layered text search
339 |           elements = findElementsByTextWithFallback(request);
340 |         }
341 |         sendResponse({ success: true, elements });
342 |       } catch (error) {
343 |         console.error('Error in getInteractiveElements:', error);
344 |         sendResponse({ success: false, error: error.message });
345 |       }
346 |       return true; // Async response
347 |     } else if (request.action === 'chrome_get_interactive_elements_ping') {
348 |       sendResponse({ status: 'pong' });
349 |       return false;
350 |     }
351 |   });
352 | 
353 |   console.log('Interactive elements helper script loaded');
354 | })();
355 | 
```

--------------------------------------------------------------------------------
/app/chrome-extension/_locales/en/messages.json:
--------------------------------------------------------------------------------

```json
  1 | {
  2 |   "extensionName": {
  3 |     "message": "chrome-mcp-server",
  4 |     "description": "Extension name"
  5 |   },
  6 |   "extensionDescription": {
  7 |     "message": "Exposes browser capabilities with your own chrome",
  8 |     "description": "Extension description"
  9 |   },
 10 |   "nativeServerConfigLabel": {
 11 |     "message": "Native Server Configuration",
 12 |     "description": "Main section header for native server settings"
 13 |   },
 14 |   "semanticEngineLabel": {
 15 |     "message": "Semantic Engine",
 16 |     "description": "Main section header for semantic engine"
 17 |   },
 18 |   "embeddingModelLabel": {
 19 |     "message": "Embedding Model",
 20 |     "description": "Main section header for model selection"
 21 |   },
 22 |   "indexDataManagementLabel": {
 23 |     "message": "Index Data Management",
 24 |     "description": "Main section header for data management"
 25 |   },
 26 |   "modelCacheManagementLabel": {
 27 |     "message": "Model Cache Management",
 28 |     "description": "Main section header for cache management"
 29 |   },
 30 |   "statusLabel": {
 31 |     "message": "Status",
 32 |     "description": "Generic status label"
 33 |   },
 34 |   "runningStatusLabel": {
 35 |     "message": "Running Status",
 36 |     "description": "Server running status label"
 37 |   },
 38 |   "connectionStatusLabel": {
 39 |     "message": "Connection Status",
 40 |     "description": "Connection status label"
 41 |   },
 42 |   "lastUpdatedLabel": {
 43 |     "message": "Last Updated:",
 44 |     "description": "Last updated timestamp label"
 45 |   },
 46 |   "connectButton": {
 47 |     "message": "Connect",
 48 |     "description": "Connect button text"
 49 |   },
 50 |   "disconnectButton": {
 51 |     "message": "Disconnect",
 52 |     "description": "Disconnect button text"
 53 |   },
 54 |   "connectingStatus": {
 55 |     "message": "Connecting...",
 56 |     "description": "Connecting status message"
 57 |   },
 58 |   "connectedStatus": {
 59 |     "message": "Connected",
 60 |     "description": "Connected status message"
 61 |   },
 62 |   "disconnectedStatus": {
 63 |     "message": "Disconnected",
 64 |     "description": "Disconnected status message"
 65 |   },
 66 |   "detectingStatus": {
 67 |     "message": "Detecting...",
 68 |     "description": "Detecting status message"
 69 |   },
 70 |   "serviceRunningStatus": {
 71 |     "message": "Service Running (Port: $PORT$)",
 72 |     "description": "Service running with port number",
 73 |     "placeholders": {
 74 |       "port": {
 75 |         "content": "$1",
 76 |         "example": "12306"
 77 |       }
 78 |     }
 79 |   },
 80 |   "serviceNotConnectedStatus": {
 81 |     "message": "Service Not Connected",
 82 |     "description": "Service not connected status"
 83 |   },
 84 |   "connectedServiceNotStartedStatus": {
 85 |     "message": "Connected, Service Not Started",
 86 |     "description": "Connected but service not started status"
 87 |   },
 88 |   "mcpServerConfigLabel": {
 89 |     "message": "MCP Server Configuration",
 90 |     "description": "MCP server configuration section label"
 91 |   },
 92 |   "connectionPortLabel": {
 93 |     "message": "Connection Port",
 94 |     "description": "Connection port input label"
 95 |   },
 96 |   "refreshStatusButton": {
 97 |     "message": "Refresh Status",
 98 |     "description": "Refresh status button tooltip"
 99 |   },
100 |   "copyConfigButton": {
101 |     "message": "Copy Configuration",
102 |     "description": "Copy configuration button text"
103 |   },
104 |   "retryButton": {
105 |     "message": "Retry",
106 |     "description": "Retry button text"
107 |   },
108 |   "cancelButton": {
109 |     "message": "Cancel",
110 |     "description": "Cancel button text"
111 |   },
112 |   "confirmButton": {
113 |     "message": "Confirm",
114 |     "description": "Confirm button text"
115 |   },
116 |   "saveButton": {
117 |     "message": "Save",
118 |     "description": "Save button text"
119 |   },
120 |   "closeButton": {
121 |     "message": "Close",
122 |     "description": "Close button text"
123 |   },
124 |   "resetButton": {
125 |     "message": "Reset",
126 |     "description": "Reset button text"
127 |   },
128 |   "initializingStatus": {
129 |     "message": "Initializing...",
130 |     "description": "Initializing progress message"
131 |   },
132 |   "processingStatus": {
133 |     "message": "Processing...",
134 |     "description": "Processing progress message"
135 |   },
136 |   "loadingStatus": {
137 |     "message": "Loading...",
138 |     "description": "Loading progress message"
139 |   },
140 |   "clearingStatus": {
141 |     "message": "Clearing...",
142 |     "description": "Clearing progress message"
143 |   },
144 |   "cleaningStatus": {
145 |     "message": "Cleaning...",
146 |     "description": "Cleaning progress message"
147 |   },
148 |   "downloadingStatus": {
149 |     "message": "Downloading...",
150 |     "description": "Downloading progress message"
151 |   },
152 |   "semanticEngineReadyStatus": {
153 |     "message": "Semantic Engine Ready",
154 |     "description": "Semantic engine ready status"
155 |   },
156 |   "semanticEngineInitializingStatus": {
157 |     "message": "Semantic Engine Initializing...",
158 |     "description": "Semantic engine initializing status"
159 |   },
160 |   "semanticEngineInitFailedStatus": {
161 |     "message": "Semantic Engine Initialization Failed",
162 |     "description": "Semantic engine initialization failed status"
163 |   },
164 |   "semanticEngineNotInitStatus": {
165 |     "message": "Semantic Engine Not Initialized",
166 |     "description": "Semantic engine not initialized status"
167 |   },
168 |   "initSemanticEngineButton": {
169 |     "message": "Initialize Semantic Engine",
170 |     "description": "Initialize semantic engine button text"
171 |   },
172 |   "reinitializeButton": {
173 |     "message": "Reinitialize",
174 |     "description": "Reinitialize button text"
175 |   },
176 |   "downloadingModelStatus": {
177 |     "message": "Downloading Model... $PROGRESS$%",
178 |     "description": "Model download progress with percentage",
179 |     "placeholders": {
180 |       "progress": {
181 |         "content": "$1",
182 |         "example": "50"
183 |       }
184 |     }
185 |   },
186 |   "switchingModelStatus": {
187 |     "message": "Switching Model...",
188 |     "description": "Model switching progress message"
189 |   },
190 |   "modelLoadedStatus": {
191 |     "message": "Model Loaded",
192 |     "description": "Model successfully loaded status"
193 |   },
194 |   "modelFailedStatus": {
195 |     "message": "Model Failed to Load",
196 |     "description": "Model failed to load status"
197 |   },
198 |   "lightweightModelDescription": {
199 |     "message": "Lightweight Multilingual Model",
200 |     "description": "Description for lightweight model option"
201 |   },
202 |   "betterThanSmallDescription": {
203 |     "message": "Slightly larger than e5-small, but better performance",
204 |     "description": "Description for medium model option"
205 |   },
206 |   "multilingualModelDescription": {
207 |     "message": "Multilingual Semantic Model",
208 |     "description": "Description for multilingual model option"
209 |   },
210 |   "fastPerformance": {
211 |     "message": "Fast",
212 |     "description": "Fast performance indicator"
213 |   },
214 |   "balancedPerformance": {
215 |     "message": "Balanced",
216 |     "description": "Balanced performance indicator"
217 |   },
218 |   "accuratePerformance": {
219 |     "message": "Accurate",
220 |     "description": "Accurate performance indicator"
221 |   },
222 |   "networkErrorMessage": {
223 |     "message": "Network connection error, please check network and retry",
224 |     "description": "Network connection error message"
225 |   },
226 |   "modelCorruptedErrorMessage": {
227 |     "message": "Model file corrupted or incomplete, please retry download",
228 |     "description": "Model corruption error message"
229 |   },
230 |   "unknownErrorMessage": {
231 |     "message": "Unknown error, please check if your network can access HuggingFace",
232 |     "description": "Unknown error fallback message"
233 |   },
234 |   "permissionDeniedErrorMessage": {
235 |     "message": "Permission denied",
236 |     "description": "Permission denied error message"
237 |   },
238 |   "timeoutErrorMessage": {
239 |     "message": "Operation timed out",
240 |     "description": "Timeout error message"
241 |   },
242 |   "indexedPagesLabel": {
243 |     "message": "Indexed Pages",
244 |     "description": "Number of indexed pages label"
245 |   },
246 |   "indexSizeLabel": {
247 |     "message": "Index Size",
248 |     "description": "Index size label"
249 |   },
250 |   "activeTabsLabel": {
251 |     "message": "Active Tabs",
252 |     "description": "Number of active tabs label"
253 |   },
254 |   "vectorDocumentsLabel": {
255 |     "message": "Vector Documents",
256 |     "description": "Number of vector documents label"
257 |   },
258 |   "cacheSizeLabel": {
259 |     "message": "Cache Size",
260 |     "description": "Cache size label"
261 |   },
262 |   "cacheEntriesLabel": {
263 |     "message": "Cache Entries",
264 |     "description": "Number of cache entries label"
265 |   },
266 |   "clearAllDataButton": {
267 |     "message": "Clear All Data",
268 |     "description": "Clear all data button text"
269 |   },
270 |   "clearAllCacheButton": {
271 |     "message": "Clear All Cache",
272 |     "description": "Clear all cache button text"
273 |   },
274 |   "cleanExpiredCacheButton": {
275 |     "message": "Clean Expired Cache",
276 |     "description": "Clean expired cache button text"
277 |   },
278 |   "exportDataButton": {
279 |     "message": "Export Data",
280 |     "description": "Export data button text"
281 |   },
282 |   "importDataButton": {
283 |     "message": "Import Data",
284 |     "description": "Import data button text"
285 |   },
286 |   "confirmClearDataTitle": {
287 |     "message": "Confirm Clear Data",
288 |     "description": "Clear data confirmation dialog title"
289 |   },
290 |   "settingsTitle": {
291 |     "message": "Settings",
292 |     "description": "Settings dialog title"
293 |   },
294 |   "aboutTitle": {
295 |     "message": "About",
296 |     "description": "About dialog title"
297 |   },
298 |   "helpTitle": {
299 |     "message": "Help",
300 |     "description": "Help dialog title"
301 |   },
302 |   "clearDataWarningMessage": {
303 |     "message": "This operation will clear all indexed webpage content and vector data, including:",
304 |     "description": "Clear data warning message"
305 |   },
306 |   "clearDataList1": {
307 |     "message": "All webpage text content index",
308 |     "description": "First item in clear data list"
309 |   },
310 |   "clearDataList2": {
311 |     "message": "Vector embedding data",
312 |     "description": "Second item in clear data list"
313 |   },
314 |   "clearDataList3": {
315 |     "message": "Search history and cache",
316 |     "description": "Third item in clear data list"
317 |   },
318 |   "clearDataIrreversibleWarning": {
319 |     "message": "This operation is irreversible! After clearing, you need to browse webpages again to rebuild the index.",
320 |     "description": "Irreversible operation warning"
321 |   },
322 |   "confirmClearButton": {
323 |     "message": "Confirm Clear",
324 |     "description": "Confirm clear action button"
325 |   },
326 |   "cacheDetailsLabel": {
327 |     "message": "Cache Details",
328 |     "description": "Cache details section label"
329 |   },
330 |   "noCacheDataMessage": {
331 |     "message": "No cache data",
332 |     "description": "No cache data available message"
333 |   },
334 |   "loadingCacheInfoStatus": {
335 |     "message": "Loading cache information...",
336 |     "description": "Loading cache information status"
337 |   },
338 |   "processingCacheStatus": {
339 |     "message": "Processing cache...",
340 |     "description": "Processing cache status"
341 |   },
342 |   "expiredLabel": {
343 |     "message": "Expired",
344 |     "description": "Expired item label"
345 |   },
346 |   "bookmarksBarLabel": {
347 |     "message": "Bookmarks Bar",
348 |     "description": "Bookmarks bar folder name"
349 |   },
350 |   "newTabLabel": {
351 |     "message": "New Tab",
352 |     "description": "New tab label"
353 |   },
354 |   "currentPageLabel": {
355 |     "message": "Current Page",
356 |     "description": "Current page label"
357 |   },
358 |   "menuLabel": {
359 |     "message": "Menu",
360 |     "description": "Menu accessibility label"
361 |   },
362 |   "navigationLabel": {
363 |     "message": "Navigation",
364 |     "description": "Navigation accessibility label"
365 |   },
366 |   "mainContentLabel": {
367 |     "message": "Main Content",
368 |     "description": "Main content accessibility label"
369 |   },
370 |   "languageSelectorLabel": {
371 |     "message": "Language",
372 |     "description": "Language selector label"
373 |   },
374 |   "themeLabel": {
375 |     "message": "Theme",
376 |     "description": "Theme selector label"
377 |   },
378 |   "lightTheme": {
379 |     "message": "Light",
380 |     "description": "Light theme option"
381 |   },
382 |   "darkTheme": {
383 |     "message": "Dark",
384 |     "description": "Dark theme option"
385 |   },
386 |   "autoTheme": {
387 |     "message": "Auto",
388 |     "description": "Auto theme option"
389 |   },
390 |   "advancedSettingsLabel": {
391 |     "message": "Advanced Settings",
392 |     "description": "Advanced settings section label"
393 |   },
394 |   "debugModeLabel": {
395 |     "message": "Debug Mode",
396 |     "description": "Debug mode toggle label"
397 |   },
398 |   "verboseLoggingLabel": {
399 |     "message": "Verbose Logging",
400 |     "description": "Verbose logging toggle label"
401 |   },
402 |   "successNotification": {
403 |     "message": "Operation completed successfully",
404 |     "description": "Generic success notification"
405 |   },
406 |   "warningNotification": {
407 |     "message": "Warning: Please review before proceeding",
408 |     "description": "Generic warning notification"
409 |   },
410 |   "infoNotification": {
411 |     "message": "Information",
412 |     "description": "Generic info notification"
413 |   },
414 |   "configCopiedNotification": {
415 |     "message": "Configuration copied to clipboard",
416 |     "description": "Configuration copied success message"
417 |   },
418 |   "dataClearedNotification": {
419 |     "message": "Data cleared successfully",
420 |     "description": "Data cleared success message"
421 |   },
422 |   "bytesUnit": {
423 |     "message": "bytes",
424 |     "description": "Bytes unit"
425 |   },
426 |   "kilobytesUnit": {
427 |     "message": "KB",
428 |     "description": "Kilobytes unit"
429 |   },
430 |   "megabytesUnit": {
431 |     "message": "MB",
432 |     "description": "Megabytes unit"
433 |   },
434 |   "gigabytesUnit": {
435 |     "message": "GB",
436 |     "description": "Gigabytes unit"
437 |   },
438 |   "itemsUnit": {
439 |     "message": "items",
440 |     "description": "Items count unit"
441 |   },
442 |   "pagesUnit": {
443 |     "message": "pages",
444 |     "description": "Pages count unit"
445 |   }
446 | }
447 | 
```

--------------------------------------------------------------------------------
/app/chrome-extension/_locales/de/messages.json:
--------------------------------------------------------------------------------

```json
  1 | {
  2 |   "extensionName": {
  3 |     "message": "chrome-mcp-server",
  4 |     "description": "Erweiterungsname"
  5 |   },
  6 |   "extensionDescription": {
  7 |     "message": "Stellt Browser-Funktionen mit Ihrem eigenen Chrome zur Verfügung",
  8 |     "description": "Erweiterungsbeschreibung"
  9 |   },
 10 |   "nativeServerConfigLabel": {
 11 |     "message": "Native Server-Konfiguration",
 12 |     "description": "Hauptabschnittstitel für Native Server-Einstellungen"
 13 |   },
 14 |   "semanticEngineLabel": {
 15 |     "message": "Semantische Engine",
 16 |     "description": "Hauptabschnittstitel für semantische Engine"
 17 |   },
 18 |   "embeddingModelLabel": {
 19 |     "message": "Embedding-Modell",
 20 |     "description": "Hauptabschnittstitel für Modellauswahl"
 21 |   },
 22 |   "indexDataManagementLabel": {
 23 |     "message": "Index-Datenverwaltung",
 24 |     "description": "Hauptabschnittstitel für Datenverwaltung"
 25 |   },
 26 |   "modelCacheManagementLabel": {
 27 |     "message": "Modell-Cache-Verwaltung",
 28 |     "description": "Hauptabschnittstitel für Cache-Verwaltung"
 29 |   },
 30 |   "statusLabel": {
 31 |     "message": "Status",
 32 |     "description": "Allgemeines Statuslabel"
 33 |   },
 34 |   "runningStatusLabel": {
 35 |     "message": "Betriebsstatus",
 36 |     "description": "Server-Betriebsstatuslabel"
 37 |   },
 38 |   "connectionStatusLabel": {
 39 |     "message": "Verbindungsstatus",
 40 |     "description": "Verbindungsstatuslabel"
 41 |   },
 42 |   "lastUpdatedLabel": {
 43 |     "message": "Zuletzt aktualisiert:",
 44 |     "description": "Zeitstempel der letzten Aktualisierung"
 45 |   },
 46 |   "connectButton": {
 47 |     "message": "Verbinden",
 48 |     "description": "Verbinden-Schaltflächentext"
 49 |   },
 50 |   "disconnectButton": {
 51 |     "message": "Trennen",
 52 |     "description": "Trennen-Schaltflächentext"
 53 |   },
 54 |   "connectingStatus": {
 55 |     "message": "Verbindung wird hergestellt...",
 56 |     "description": "Verbindungsstatusmeldung"
 57 |   },
 58 |   "connectedStatus": {
 59 |     "message": "Verbunden",
 60 |     "description": "Verbunden-Statusmeldung"
 61 |   },
 62 |   "disconnectedStatus": {
 63 |     "message": "Getrennt",
 64 |     "description": "Getrennt-Statusmeldung"
 65 |   },
 66 |   "detectingStatus": {
 67 |     "message": "Erkennung läuft...",
 68 |     "description": "Erkennungsstatusmeldung"
 69 |   },
 70 |   "serviceRunningStatus": {
 71 |     "message": "Service läuft (Port: $PORT$)",
 72 |     "description": "Service läuft mit Portnummer",
 73 |     "placeholders": {
 74 |       "port": {
 75 |         "content": "$1",
 76 |         "example": "12306"
 77 |       }
 78 |     }
 79 |   },
 80 |   "serviceNotConnectedStatus": {
 81 |     "message": "Service nicht verbunden",
 82 |     "description": "Service nicht verbunden Status"
 83 |   },
 84 |   "connectedServiceNotStartedStatus": {
 85 |     "message": "Verbunden, Service nicht gestartet",
 86 |     "description": "Verbunden aber Service nicht gestartet Status"
 87 |   },
 88 |   "mcpServerConfigLabel": {
 89 |     "message": "MCP Server-Konfiguration",
 90 |     "description": "MCP Server-Konfigurationsabschnittslabel"
 91 |   },
 92 |   "connectionPortLabel": {
 93 |     "message": "Verbindungsport",
 94 |     "description": "Verbindungsport-Eingabelabel"
 95 |   },
 96 |   "refreshStatusButton": {
 97 |     "message": "Status aktualisieren",
 98 |     "description": "Status aktualisieren Schaltflächen-Tooltip"
 99 |   },
100 |   "copyConfigButton": {
101 |     "message": "Konfiguration kopieren",
102 |     "description": "Konfiguration kopieren Schaltflächentext"
103 |   },
104 |   "retryButton": {
105 |     "message": "Wiederholen",
106 |     "description": "Wiederholen-Schaltflächentext"
107 |   },
108 |   "cancelButton": {
109 |     "message": "Abbrechen",
110 |     "description": "Abbrechen-Schaltflächentext"
111 |   },
112 |   "confirmButton": {
113 |     "message": "Bestätigen",
114 |     "description": "Bestätigen-Schaltflächentext"
115 |   },
116 |   "saveButton": {
117 |     "message": "Speichern",
118 |     "description": "Speichern-Schaltflächentext"
119 |   },
120 |   "closeButton": {
121 |     "message": "Schließen",
122 |     "description": "Schließen-Schaltflächentext"
123 |   },
124 |   "resetButton": {
125 |     "message": "Zurücksetzen",
126 |     "description": "Zurücksetzen-Schaltflächentext"
127 |   },
128 |   "initializingStatus": {
129 |     "message": "Initialisierung...",
130 |     "description": "Initialisierung-Fortschrittsmeldung"
131 |   },
132 |   "processingStatus": {
133 |     "message": "Verarbeitung...",
134 |     "description": "Verarbeitung-Fortschrittsmeldung"
135 |   },
136 |   "loadingStatus": {
137 |     "message": "Wird geladen...",
138 |     "description": "Ladefortschrittsmeldung"
139 |   },
140 |   "clearingStatus": {
141 |     "message": "Wird geleert...",
142 |     "description": "Leerungsfortschrittsmeldung"
143 |   },
144 |   "cleaningStatus": {
145 |     "message": "Wird bereinigt...",
146 |     "description": "Bereinigungsfortschrittsmeldung"
147 |   },
148 |   "downloadingStatus": {
149 |     "message": "Wird heruntergeladen...",
150 |     "description": "Download-Fortschrittsmeldung"
151 |   },
152 |   "semanticEngineReadyStatus": {
153 |     "message": "Semantische Engine bereit",
154 |     "description": "Semantische Engine bereit Status"
155 |   },
156 |   "semanticEngineInitializingStatus": {
157 |     "message": "Semantische Engine wird initialisiert...",
158 |     "description": "Semantische Engine Initialisierungsstatus"
159 |   },
160 |   "semanticEngineInitFailedStatus": {
161 |     "message": "Initialisierung der semantischen Engine fehlgeschlagen",
162 |     "description": "Semantische Engine Initialisierung fehlgeschlagen Status"
163 |   },
164 |   "semanticEngineNotInitStatus": {
165 |     "message": "Semantische Engine nicht initialisiert",
166 |     "description": "Semantische Engine nicht initialisiert Status"
167 |   },
168 |   "initSemanticEngineButton": {
169 |     "message": "Semantische Engine initialisieren",
170 |     "description": "Semantische Engine initialisieren Schaltflächentext"
171 |   },
172 |   "reinitializeButton": {
173 |     "message": "Neu initialisieren",
174 |     "description": "Neu initialisieren Schaltflächentext"
175 |   },
176 |   "downloadingModelStatus": {
177 |     "message": "Modell wird heruntergeladen... $PROGRESS$%",
178 |     "description": "Modell-Download-Fortschritt mit Prozentsatz",
179 |     "placeholders": {
180 |       "progress": {
181 |         "content": "$1",
182 |         "example": "50"
183 |       }
184 |     }
185 |   },
186 |   "switchingModelStatus": {
187 |     "message": "Modell wird gewechselt...",
188 |     "description": "Modellwechsel-Fortschrittsmeldung"
189 |   },
190 |   "modelLoadedStatus": {
191 |     "message": "Modell geladen",
192 |     "description": "Modell erfolgreich geladen Status"
193 |   },
194 |   "modelFailedStatus": {
195 |     "message": "Modell konnte nicht geladen werden",
196 |     "description": "Modell-Ladefehler Status"
197 |   },
198 |   "lightweightModelDescription": {
199 |     "message": "Leichtgewichtiges mehrsprachiges Modell",
200 |     "description": "Beschreibung für leichtgewichtige Modelloption"
201 |   },
202 |   "betterThanSmallDescription": {
203 |     "message": "Etwas größer als e5-small, aber bessere Leistung",
204 |     "description": "Beschreibung für mittlere Modelloption"
205 |   },
206 |   "multilingualModelDescription": {
207 |     "message": "Mehrsprachiges semantisches Modell",
208 |     "description": "Beschreibung für mehrsprachige Modelloption"
209 |   },
210 |   "fastPerformance": {
211 |     "message": "Schnell",
212 |     "description": "Schnelle Leistungsanzeige"
213 |   },
214 |   "balancedPerformance": {
215 |     "message": "Ausgewogen",
216 |     "description": "Ausgewogene Leistungsanzeige"
217 |   },
218 |   "accuratePerformance": {
219 |     "message": "Genau",
220 |     "description": "Genaue Leistungsanzeige"
221 |   },
222 |   "networkErrorMessage": {
223 |     "message": "Netzwerkverbindungsfehler, bitte Netzwerk prüfen und erneut versuchen",
224 |     "description": "Netzwerkverbindungsfehlermeldung"
225 |   },
226 |   "modelCorruptedErrorMessage": {
227 |     "message": "Modelldatei beschädigt oder unvollständig, bitte Download wiederholen",
228 |     "description": "Modell-Beschädigungsfehlermeldung"
229 |   },
230 |   "unknownErrorMessage": {
231 |     "message": "Unbekannter Fehler, bitte prüfen Sie, ob Ihr Netzwerk auf HuggingFace zugreifen kann",
232 |     "description": "Unbekannte Fehler-Rückfallmeldung"
233 |   },
234 |   "permissionDeniedErrorMessage": {
235 |     "message": "Zugriff verweigert",
236 |     "description": "Zugriff verweigert Fehlermeldung"
237 |   },
238 |   "timeoutErrorMessage": {
239 |     "message": "Zeitüberschreitung",
240 |     "description": "Zeitüberschreitungsfehlermeldung"
241 |   },
242 |   "indexedPagesLabel": {
243 |     "message": "Indizierte Seiten",
244 |     "description": "Anzahl indizierter Seiten Label"
245 |   },
246 |   "indexSizeLabel": {
247 |     "message": "Indexgröße",
248 |     "description": "Indexgröße Label"
249 |   },
250 |   "activeTabsLabel": {
251 |     "message": "Aktive Tabs",
252 |     "description": "Anzahl aktiver Tabs Label"
253 |   },
254 |   "vectorDocumentsLabel": {
255 |     "message": "Vektordokumente",
256 |     "description": "Anzahl Vektordokumente Label"
257 |   },
258 |   "cacheSizeLabel": {
259 |     "message": "Cache-Größe",
260 |     "description": "Cache-Größe Label"
261 |   },
262 |   "cacheEntriesLabel": {
263 |     "message": "Cache-Einträge",
264 |     "description": "Anzahl Cache-Einträge Label"
265 |   },
266 |   "clearAllDataButton": {
267 |     "message": "Alle Daten löschen",
268 |     "description": "Alle Daten löschen Schaltflächentext"
269 |   },
270 |   "clearAllCacheButton": {
271 |     "message": "Gesamten Cache löschen",
272 |     "description": "Gesamten Cache löschen Schaltflächentext"
273 |   },
274 |   "cleanExpiredCacheButton": {
275 |     "message": "Abgelaufenen Cache bereinigen",
276 |     "description": "Abgelaufenen Cache bereinigen Schaltflächentext"
277 |   },
278 |   "exportDataButton": {
279 |     "message": "Daten exportieren",
280 |     "description": "Daten exportieren Schaltflächentext"
281 |   },
282 |   "importDataButton": {
283 |     "message": "Daten importieren",
284 |     "description": "Daten importieren Schaltflächentext"
285 |   },
286 |   "confirmClearDataTitle": {
287 |     "message": "Datenlöschung bestätigen",
288 |     "description": "Datenlöschung bestätigen Dialogtitel"
289 |   },
290 |   "settingsTitle": {
291 |     "message": "Einstellungen",
292 |     "description": "Einstellungen Dialogtitel"
293 |   },
294 |   "aboutTitle": {
295 |     "message": "Über",
296 |     "description": "Über Dialogtitel"
297 |   },
298 |   "helpTitle": {
299 |     "message": "Hilfe",
300 |     "description": "Hilfe Dialogtitel"
301 |   },
302 |   "clearDataWarningMessage": {
303 |     "message": "Diese Aktion löscht alle indizierten Webseiteninhalte und Vektordaten, einschließlich:",
304 |     "description": "Datenlöschung Warnmeldung"
305 |   },
306 |   "clearDataList1": {
307 |     "message": "Alle Webseitentextinhaltsindizes",
308 |     "description": "Erster Punkt in Datenlöschungsliste"
309 |   },
310 |   "clearDataList2": {
311 |     "message": "Vektor-Embedding-Daten",
312 |     "description": "Zweiter Punkt in Datenlöschungsliste"
313 |   },
314 |   "clearDataList3": {
315 |     "message": "Suchverlauf und Cache",
316 |     "description": "Dritter Punkt in Datenlöschungsliste"
317 |   },
318 |   "clearDataIrreversibleWarning": {
319 |     "message": "Diese Aktion ist unwiderruflich! Nach dem Löschen müssen Sie Webseiten erneut durchsuchen, um den Index neu aufzubauen.",
320 |     "description": "Unwiderrufliche Aktion Warnung"
321 |   },
322 |   "confirmClearButton": {
323 |     "message": "Löschung bestätigen",
324 |     "description": "Löschung bestätigen Aktionsschaltfläche"
325 |   },
326 |   "cacheDetailsLabel": {
327 |     "message": "Cache-Details",
328 |     "description": "Cache-Details Abschnittslabel"
329 |   },
330 |   "noCacheDataMessage": {
331 |     "message": "Keine Cache-Daten vorhanden",
332 |     "description": "Keine Cache-Daten verfügbar Meldung"
333 |   },
334 |   "loadingCacheInfoStatus": {
335 |     "message": "Cache-Informationen werden geladen...",
336 |     "description": "Cache-Informationen laden Status"
337 |   },
338 |   "processingCacheStatus": {
339 |     "message": "Cache wird verarbeitet...",
340 |     "description": "Cache verarbeiten Status"
341 |   },
342 |   "expiredLabel": {
343 |     "message": "Abgelaufen",
344 |     "description": "Abgelaufenes Element Label"
345 |   },
346 |   "bookmarksBarLabel": {
347 |     "message": "Lesezeichenleiste",
348 |     "description": "Lesezeichenleiste Ordnername"
349 |   },
350 |   "newTabLabel": {
351 |     "message": "Neuer Tab",
352 |     "description": "Neuer Tab Label"
353 |   },
354 |   "currentPageLabel": {
355 |     "message": "Aktuelle Seite",
356 |     "description": "Aktuelle Seite Label"
357 |   },
358 |   "menuLabel": {
359 |     "message": "Menü",
360 |     "description": "Menü Barrierefreiheitslabel"
361 |   },
362 |   "navigationLabel": {
363 |     "message": "Navigation",
364 |     "description": "Navigation Barrierefreiheitslabel"
365 |   },
366 |   "mainContentLabel": {
367 |     "message": "Hauptinhalt",
368 |     "description": "Hauptinhalt Barrierefreiheitslabel"
369 |   },
370 |   "languageSelectorLabel": {
371 |     "message": "Sprache",
372 |     "description": "Sprachauswahl Label"
373 |   },
374 |   "themeLabel": {
375 |     "message": "Design",
376 |     "description": "Design-Auswahl Label"
377 |   },
378 |   "lightTheme": {
379 |     "message": "Hell",
380 |     "description": "Helles Design Option"
381 |   },
382 |   "darkTheme": {
383 |     "message": "Dunkel",
384 |     "description": "Dunkles Design Option"
385 |   },
386 |   "autoTheme": {
387 |     "message": "Automatisch",
388 |     "description": "Automatisches Design Option"
389 |   },
390 |   "advancedSettingsLabel": {
391 |     "message": "Erweiterte Einstellungen",
392 |     "description": "Erweiterte Einstellungen Abschnittslabel"
393 |   },
394 |   "debugModeLabel": {
395 |     "message": "Debug-Modus",
396 |     "description": "Debug-Modus Umschalter Label"
397 |   },
398 |   "verboseLoggingLabel": {
399 |     "message": "Ausführliche Protokollierung",
400 |     "description": "Ausführliche Protokollierung Umschalter Label"
401 |   },
402 |   "successNotification": {
403 |     "message": "Vorgang erfolgreich abgeschlossen",
404 |     "description": "Allgemeine Erfolgsmeldung"
405 |   },
406 |   "warningNotification": {
407 |     "message": "Warnung: Bitte prüfen Sie vor dem Fortfahren",
408 |     "description": "Allgemeine Warnmeldung"
409 |   },
410 |   "infoNotification": {
411 |     "message": "Information",
412 |     "description": "Allgemeine Informationsmeldung"
413 |   },
414 |   "configCopiedNotification": {
415 |     "message": "Konfiguration in Zwischenablage kopiert",
416 |     "description": "Konfiguration kopiert Erfolgsmeldung"
417 |   },
418 |   "dataClearedNotification": {
419 |     "message": "Daten erfolgreich gelöscht",
420 |     "description": "Daten gelöscht Erfolgsmeldung"
421 |   },
422 |   "bytesUnit": {
423 |     "message": "Bytes",
424 |     "description": "Bytes Einheit"
425 |   },
426 |   "kilobytesUnit": {
427 |     "message": "KB",
428 |     "description": "Kilobytes Einheit"
429 |   },
430 |   "megabytesUnit": {
431 |     "message": "MB",
432 |     "description": "Megabytes Einheit"
433 |   },
434 |   "gigabytesUnit": {
435 |     "message": "GB",
436 |     "description": "Gigabytes Einheit"
437 |   },
438 |   "itemsUnit": {
439 |     "message": "Elemente",
440 |     "description": "Elemente Zähleinheit"
441 |   },
442 |   "pagesUnit": {
443 |     "message": "Seiten",
444 |     "description": "Seiten Zähleinheit"
445 |   }
446 | }
```

--------------------------------------------------------------------------------
/app/chrome-extension/utils/simd-math-engine.ts:
--------------------------------------------------------------------------------

```typescript
  1 | /**
  2 |  * SIMD-optimized mathematical computation engine
  3 |  * Uses WebAssembly + SIMD instructions to accelerate vector calculations
  4 |  */
  5 | 
  6 | interface SIMDMathWasm {
  7 |   free(): void;
  8 |   cosine_similarity(vec_a: Float32Array, vec_b: Float32Array): number;
  9 |   batch_similarity(vectors: Float32Array, query: Float32Array, vector_dim: number): Float32Array;
 10 |   similarity_matrix(
 11 |     vectors_a: Float32Array,
 12 |     vectors_b: Float32Array,
 13 |     vector_dim: number,
 14 |   ): Float32Array;
 15 | }
 16 | 
 17 | interface WasmModule {
 18 |   SIMDMath: new () => SIMDMathWasm;
 19 |   memory: WebAssembly.Memory;
 20 |   default: (module_or_path?: any) => Promise<any>;
 21 | }
 22 | 
 23 | export class SIMDMathEngine {
 24 |   private wasmModule: WasmModule | null = null;
 25 |   private simdMath: SIMDMathWasm | null = null;
 26 |   private isInitialized = false;
 27 |   private isInitializing = false;
 28 |   private initPromise: Promise<void> | null = null;
 29 | 
 30 |   private alignedBufferPool: Map<number, Float32Array[]> = new Map();
 31 |   private maxPoolSize = 5;
 32 | 
 33 |   async initialize(): Promise<void> {
 34 |     if (this.isInitialized) return;
 35 |     if (this.isInitializing && this.initPromise) return this.initPromise;
 36 | 
 37 |     this.isInitializing = true;
 38 |     this.initPromise = this._doInitialize().finally(() => {
 39 |       this.isInitializing = false;
 40 |     });
 41 | 
 42 |     return this.initPromise;
 43 |   }
 44 | 
 45 |   private async _doInitialize(): Promise<void> {
 46 |     try {
 47 |       console.log('SIMDMathEngine: Initializing WebAssembly module...');
 48 | 
 49 |       const wasmUrl = chrome.runtime.getURL('workers/simd_math.js');
 50 |       const wasmModule = await import(wasmUrl);
 51 | 
 52 |       const wasmInstance = await wasmModule.default();
 53 | 
 54 |       this.wasmModule = {
 55 |         SIMDMath: wasmModule.SIMDMath,
 56 |         memory: wasmInstance.memory,
 57 |         default: wasmModule.default,
 58 |       };
 59 | 
 60 |       this.simdMath = new this.wasmModule.SIMDMath();
 61 | 
 62 |       this.isInitialized = true;
 63 |       console.log('SIMDMathEngine: WebAssembly module initialized successfully');
 64 |     } catch (error) {
 65 |       console.error('SIMDMathEngine: Failed to initialize WebAssembly module:', error);
 66 |       this.isInitialized = false;
 67 |       throw error;
 68 |     }
 69 |   }
 70 | 
 71 |   /**
 72 |    * Get aligned buffer (16-byte aligned, suitable for SIMD)
 73 |    */
 74 |   private getAlignedBuffer(size: number): Float32Array {
 75 |     if (!this.alignedBufferPool.has(size)) {
 76 |       this.alignedBufferPool.set(size, []);
 77 |     }
 78 | 
 79 |     const pool = this.alignedBufferPool.get(size)!;
 80 |     if (pool.length > 0) {
 81 |       return pool.pop()!;
 82 |     }
 83 | 
 84 |     // Create 16-byte aligned buffer
 85 |     const buffer = new ArrayBuffer(size * 4 + 15);
 86 |     const alignedOffset = (16 - (buffer.byteLength % 16)) % 16;
 87 |     return new Float32Array(buffer, alignedOffset, size);
 88 |   }
 89 | 
 90 |   /**
 91 |    * Release aligned buffer back to pool
 92 |    */
 93 |   private releaseAlignedBuffer(buffer: Float32Array): void {
 94 |     const size = buffer.length;
 95 |     const pool = this.alignedBufferPool.get(size);
 96 |     if (pool && pool.length < this.maxPoolSize) {
 97 |       buffer.fill(0); // Clear to zero
 98 |       pool.push(buffer);
 99 |     }
100 |   }
101 | 
102 |   /**
103 |    * Check if vector is already aligned
104 |    */
105 |   private isAligned(array: Float32Array): boolean {
106 |     return array.byteOffset % 16 === 0;
107 |   }
108 | 
109 |   /**
110 |    * Ensure vector alignment, create aligned copy if not aligned
111 |    */
112 |   private ensureAligned(array: Float32Array): { aligned: Float32Array; needsRelease: boolean } {
113 |     if (this.isAligned(array)) {
114 |       return { aligned: array, needsRelease: false };
115 |     }
116 | 
117 |     const aligned = this.getAlignedBuffer(array.length);
118 |     aligned.set(array);
119 |     return { aligned, needsRelease: true };
120 |   }
121 | 
122 |   /**
123 |    * SIMD-optimized cosine similarity calculation
124 |    */
125 |   async cosineSimilarity(vecA: Float32Array, vecB: Float32Array): Promise<number> {
126 |     if (!this.isInitialized) {
127 |       await this.initialize();
128 |     }
129 | 
130 |     if (!this.simdMath) {
131 |       throw new Error('SIMD math engine not initialized');
132 |     }
133 | 
134 |     // Ensure vector alignment
135 |     const { aligned: alignedA, needsRelease: releaseA } = this.ensureAligned(vecA);
136 |     const { aligned: alignedB, needsRelease: releaseB } = this.ensureAligned(vecB);
137 | 
138 |     try {
139 |       const result = this.simdMath.cosine_similarity(alignedA, alignedB);
140 |       return result;
141 |     } finally {
142 |       // Release temporary buffers
143 |       if (releaseA) this.releaseAlignedBuffer(alignedA);
144 |       if (releaseB) this.releaseAlignedBuffer(alignedB);
145 |     }
146 |   }
147 | 
148 |   /**
149 |    * Batch similarity calculation
150 |    */
151 |   async batchSimilarity(vectors: Float32Array[], query: Float32Array): Promise<number[]> {
152 |     if (!this.isInitialized) {
153 |       await this.initialize();
154 |     }
155 | 
156 |     if (!this.simdMath) {
157 |       throw new Error('SIMD math engine not initialized');
158 |     }
159 | 
160 |     const vectorDim = query.length;
161 |     const numVectors = vectors.length;
162 | 
163 |     // Pack all vectors into contiguous memory layout
164 |     const packedVectors = this.getAlignedBuffer(numVectors * vectorDim);
165 |     const { aligned: alignedQuery, needsRelease: releaseQuery } = this.ensureAligned(query);
166 | 
167 |     try {
168 |       // Copy vector data
169 |       let offset = 0;
170 |       for (const vector of vectors) {
171 |         packedVectors.set(vector, offset);
172 |         offset += vectorDim;
173 |       }
174 | 
175 |       // Batch calculation
176 |       const results = this.simdMath.batch_similarity(packedVectors, alignedQuery, vectorDim);
177 |       return Array.from(results);
178 |     } finally {
179 |       this.releaseAlignedBuffer(packedVectors);
180 |       if (releaseQuery) this.releaseAlignedBuffer(alignedQuery);
181 |     }
182 |   }
183 | 
184 |   /**
185 |    * Similarity matrix calculation
186 |    */
187 |   async similarityMatrix(vectorsA: Float32Array[], vectorsB: Float32Array[]): Promise<number[][]> {
188 |     if (!this.isInitialized) {
189 |       await this.initialize();
190 |     }
191 | 
192 |     if (!this.simdMath || vectorsA.length === 0 || vectorsB.length === 0) {
193 |       return [];
194 |     }
195 | 
196 |     const vectorDim = vectorsA[0].length;
197 |     const numA = vectorsA.length;
198 |     const numB = vectorsB.length;
199 | 
200 |     // Pack vectors
201 |     const packedA = this.getAlignedBuffer(numA * vectorDim);
202 |     const packedB = this.getAlignedBuffer(numB * vectorDim);
203 | 
204 |     try {
205 |       // Copy data
206 |       let offsetA = 0;
207 |       for (const vector of vectorsA) {
208 |         packedA.set(vector, offsetA);
209 |         offsetA += vectorDim;
210 |       }
211 | 
212 |       let offsetB = 0;
213 |       for (const vector of vectorsB) {
214 |         packedB.set(vector, offsetB);
215 |         offsetB += vectorDim;
216 |       }
217 | 
218 |       // Calculate matrix
219 |       const flatResults = this.simdMath.similarity_matrix(packedA, packedB, vectorDim);
220 | 
221 |       // Convert to 2D array
222 |       const matrix: number[][] = [];
223 |       for (let i = 0; i < numA; i++) {
224 |         const row: number[] = [];
225 |         for (let j = 0; j < numB; j++) {
226 |           row.push(flatResults[i * numB + j]);
227 |         }
228 |         matrix.push(row);
229 |       }
230 | 
231 |       return matrix;
232 |     } finally {
233 |       this.releaseAlignedBuffer(packedA);
234 |       this.releaseAlignedBuffer(packedB);
235 |     }
236 |   }
237 | 
238 |   /**
239 |    * Check SIMD support
240 |    */
241 |   static async checkSIMDSupport(): Promise<boolean> {
242 |     try {
243 |       console.log('SIMDMathEngine: Checking SIMD support...');
244 | 
245 |       // Get browser information
246 |       const userAgent = navigator.userAgent;
247 |       const browserInfo = SIMDMathEngine.getBrowserInfo();
248 |       console.log('Browser info:', browserInfo);
249 |       console.log('User Agent:', userAgent);
250 | 
251 |       // Check WebAssembly basic support
252 |       if (typeof WebAssembly !== 'object') {
253 |         console.log('WebAssembly not supported');
254 |         return false;
255 |       }
256 |       console.log('✅ WebAssembly basic support: OK');
257 | 
258 |       // Check WebAssembly.validate method
259 |       if (typeof WebAssembly.validate !== 'function') {
260 |         console.log('❌ WebAssembly.validate not available');
261 |         return false;
262 |       }
263 |       console.log('✅ WebAssembly.validate: OK');
264 | 
265 |       // Test basic WebAssembly module validation
266 |       const basicWasm = new Uint8Array([0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00]);
267 |       const basicValid = WebAssembly.validate(basicWasm);
268 |       console.log('✅ Basic WASM validation:', basicValid);
269 | 
270 |       // Check WebAssembly SIMD support - using correct SIMD test module
271 |       console.log('Testing SIMD WASM module...');
272 | 
273 |       // Method 1: Use standard SIMD detection bytecode
274 |       let wasmSIMDSupported = false;
275 |       try {
276 |         // This is a minimal SIMD module containing v128.const instruction
277 |         const simdWasm = new Uint8Array([
278 |           0x00,
279 |           0x61,
280 |           0x73,
281 |           0x6d, // WASM magic
282 |           0x01,
283 |           0x00,
284 |           0x00,
285 |           0x00, // version
286 |           0x01,
287 |           0x05,
288 |           0x01, // type section
289 |           0x60,
290 |           0x00,
291 |           0x01,
292 |           0x7b, // function type: () -> v128
293 |           0x03,
294 |           0x02,
295 |           0x01,
296 |           0x00, // function section
297 |           0x0a,
298 |           0x0a,
299 |           0x01, // code section
300 |           0x08,
301 |           0x00, // function body
302 |           0xfd,
303 |           0x0c, // v128.const
304 |           0x00,
305 |           0x00,
306 |           0x00,
307 |           0x00,
308 |           0x00,
309 |           0x00,
310 |           0x00,
311 |           0x00,
312 |           0x00,
313 |           0x00,
314 |           0x00,
315 |           0x00,
316 |           0x00,
317 |           0x00,
318 |           0x00,
319 |           0x00,
320 |           0x0b, // end
321 |         ]);
322 |         wasmSIMDSupported = WebAssembly.validate(simdWasm);
323 |         console.log('Method 1 - Standard SIMD test result:', wasmSIMDSupported);
324 |       } catch (error) {
325 |         console.log('Method 1 failed:', error);
326 |       }
327 | 
328 |       // Method 2: If method 1 fails, try simpler SIMD instruction
329 |       if (!wasmSIMDSupported) {
330 |         try {
331 |           // Test using i32x4.splat instruction
332 |           const simpleSimdWasm = new Uint8Array([
333 |             0x00,
334 |             0x61,
335 |             0x73,
336 |             0x6d, // WASM magic
337 |             0x01,
338 |             0x00,
339 |             0x00,
340 |             0x00, // version
341 |             0x01,
342 |             0x06,
343 |             0x01, // type section
344 |             0x60,
345 |             0x01,
346 |             0x7f,
347 |             0x01,
348 |             0x7b, // function type: (i32) -> v128
349 |             0x03,
350 |             0x02,
351 |             0x01,
352 |             0x00, // function section
353 |             0x0a,
354 |             0x07,
355 |             0x01, // code section
356 |             0x05,
357 |             0x00, // function body
358 |             0x20,
359 |             0x00, // local.get 0
360 |             0xfd,
361 |             0x0d, // i32x4.splat
362 |             0x0b, // end
363 |           ]);
364 |           wasmSIMDSupported = WebAssembly.validate(simpleSimdWasm);
365 |           console.log('Method 2 - Simple SIMD test result:', wasmSIMDSupported);
366 |         } catch (error) {
367 |           console.log('Method 2 failed:', error);
368 |         }
369 |       }
370 | 
371 |       // Method 3: If previous methods fail, try detecting specific SIMD features
372 |       if (!wasmSIMDSupported) {
373 |         try {
374 |           // Check if SIMD feature flags are supported
375 |           const featureTest = WebAssembly.validate(
376 |             new Uint8Array([0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00]),
377 |           );
378 | 
379 |           if (featureTest) {
380 |             // In Chrome, if basic WebAssembly works and version >= 91, SIMD is usually available
381 |             const chromeMatch = userAgent.match(/Chrome\/(\d+)/);
382 |             if (chromeMatch && parseInt(chromeMatch[1]) >= 91) {
383 |               console.log('Method 3 - Chrome version check: SIMD should be available');
384 |               wasmSIMDSupported = true;
385 |             }
386 |           }
387 |         } catch (error) {
388 |           console.log('Method 3 failed:', error);
389 |         }
390 |       }
391 | 
392 |       // Output final result
393 |       if (!wasmSIMDSupported) {
394 |         console.log('❌ SIMD not supported. Browser requirements:');
395 |         console.log('- Chrome 91+, Firefox 89+, Safari 16.4+, Edge 91+');
396 |         console.log('Your browser should support SIMD. Possible issues:');
397 |         console.log('1. Extension context limitations');
398 |         console.log('2. Security policies');
399 |         console.log('3. Feature flags disabled');
400 |       } else {
401 |         console.log('✅ SIMD supported!');
402 |       }
403 | 
404 |       return wasmSIMDSupported;
405 |     } catch (error: any) {
406 |       console.error('SIMD support check failed:', error);
407 |       if (error instanceof Error) {
408 |         console.error('Error details:', {
409 |           name: error.name,
410 |           message: error.message,
411 |           stack: error.stack,
412 |         });
413 |       }
414 |       return false;
415 |     }
416 |   }
417 | 
418 |   /**
419 |    * Get browser information
420 |    */
421 |   static getBrowserInfo(): { name: string; version: string; supported: boolean } {
422 |     const userAgent = navigator.userAgent;
423 |     let browserName = 'Unknown';
424 |     let version = 'Unknown';
425 |     let supported = false;
426 | 
427 |     // Chrome
428 |     if (userAgent.includes('Chrome/')) {
429 |       browserName = 'Chrome';
430 |       const match = userAgent.match(/Chrome\/(\d+)/);
431 |       if (match) {
432 |         version = match[1];
433 |         supported = parseInt(version) >= 91;
434 |       }
435 |     }
436 |     // Firefox
437 |     else if (userAgent.includes('Firefox/')) {
438 |       browserName = 'Firefox';
439 |       const match = userAgent.match(/Firefox\/(\d+)/);
440 |       if (match) {
441 |         version = match[1];
442 |         supported = parseInt(version) >= 89;
443 |       }
444 |     }
445 |     // Safari
446 |     else if (userAgent.includes('Safari/') && !userAgent.includes('Chrome/')) {
447 |       browserName = 'Safari';
448 |       const match = userAgent.match(/Version\/(\d+\.\d+)/);
449 |       if (match) {
450 |         version = match[1];
451 |         const versionNum = parseFloat(version);
452 |         supported = versionNum >= 16.4;
453 |       }
454 |     }
455 |     // Edge
456 |     else if (userAgent.includes('Edg/')) {
457 |       browserName = 'Edge';
458 |       const match = userAgent.match(/Edg\/(\d+)/);
459 |       if (match) {
460 |         version = match[1];
461 |         supported = parseInt(version) >= 91;
462 |       }
463 |     }
464 | 
465 |     return { name: browserName, version, supported };
466 |   }
467 | 
468 |   getStats() {
469 |     return {
470 |       isInitialized: this.isInitialized,
471 |       isInitializing: this.isInitializing,
472 |       bufferPoolStats: Array.from(this.alignedBufferPool.entries()).map(([size, buffers]) => ({
473 |         size,
474 |         pooled: buffers.length,
475 |         maxPoolSize: this.maxPoolSize,
476 |       })),
477 |     };
478 |   }
479 | 
480 |   dispose(): void {
481 |     if (this.simdMath) {
482 |       try {
483 |         this.simdMath.free();
484 |       } catch (error) {
485 |         console.warn('Failed to free SIMD math instance:', error);
486 |       }
487 |       this.simdMath = null;
488 |     }
489 | 
490 |     this.alignedBufferPool.clear();
491 |     this.wasmModule = null;
492 |     this.isInitialized = false;
493 |     this.isInitializing = false;
494 |     this.initPromise = null;
495 |   }
496 | }
497 | 
```

--------------------------------------------------------------------------------
/app/chrome-extension/entrypoints/background/tools/browser/screenshot.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { createErrorResponse, ToolResult } from '@/common/tool-handler';
  2 | import { BaseBrowserToolExecutor } from '../base-browser';
  3 | import { TOOL_NAMES } from 'chrome-mcp-shared';
  4 | import { TOOL_MESSAGE_TYPES } from '@/common/message-types';
  5 | import { TIMEOUTS, ERROR_MESSAGES } from '@/common/constants';
  6 | import {
  7 |   canvasToDataURL,
  8 |   createImageBitmapFromUrl,
  9 |   cropAndResizeImage,
 10 |   stitchImages,
 11 |   compressImage,
 12 | } from '../../../../utils/image-utils';
 13 | 
 14 | // Screenshot-specific constants
 15 | const SCREENSHOT_CONSTANTS = {
 16 |   SCROLL_DELAY_MS: 350, // Time to wait after scroll for rendering and lazy loading
 17 |   CAPTURE_STITCH_DELAY_MS: 50, // Small delay between captures in a scroll sequence
 18 |   MAX_CAPTURE_PARTS: 50, // Maximum number of parts to capture (for infinite scroll pages)
 19 |   MAX_CAPTURE_HEIGHT_PX: 50000, // Maximum height in pixels to capture
 20 |   PIXEL_TOLERANCE: 1,
 21 |   SCRIPT_INIT_DELAY: 100, // Delay for script initialization
 22 | } as {
 23 |   readonly SCROLL_DELAY_MS: number;
 24 |   CAPTURE_STITCH_DELAY_MS: number; // This one is mutable
 25 |   readonly MAX_CAPTURE_PARTS: number;
 26 |   readonly MAX_CAPTURE_HEIGHT_PX: number;
 27 |   readonly PIXEL_TOLERANCE: number;
 28 |   readonly SCRIPT_INIT_DELAY: number;
 29 | };
 30 | 
 31 | SCREENSHOT_CONSTANTS["CAPTURE_STITCH_DELAY_MS"] = Math.max(1000 / chrome.tabs.MAX_CAPTURE_VISIBLE_TAB_CALLS_PER_SECOND - SCREENSHOT_CONSTANTS.SCROLL_DELAY_MS, SCREENSHOT_CONSTANTS.CAPTURE_STITCH_DELAY_MS)
 32 | 
 33 | interface ScreenshotToolParams {
 34 |   name: string;
 35 |   selector?: string;
 36 |   width?: number;
 37 |   height?: number;
 38 |   storeBase64?: boolean;
 39 |   fullPage?: boolean;
 40 |   savePng?: boolean;
 41 |   maxHeight?: number; // Maximum height to capture in pixels (for infinite scroll pages)
 42 | }
 43 | 
 44 | /**
 45 |  * Tool for capturing screenshots of web pages
 46 |  */
 47 | class ScreenshotTool extends BaseBrowserToolExecutor {
 48 |   name = TOOL_NAMES.BROWSER.SCREENSHOT;
 49 | 
 50 |   /**
 51 |    * Execute screenshot operation
 52 |    */
 53 |   async execute(args: ScreenshotToolParams): Promise<ToolResult> {
 54 |     const {
 55 |       name = 'screenshot',
 56 |       selector,
 57 |       storeBase64 = false,
 58 |       fullPage = false,
 59 |       savePng = true,
 60 |     } = args;
 61 | 
 62 |     console.log(`Starting screenshot with options:`, args);
 63 | 
 64 |     // Get current tab
 65 |     const tabs = await chrome.tabs.query({ active: true, currentWindow: true });
 66 |     if (!tabs[0]) {
 67 |       return createErrorResponse(ERROR_MESSAGES.TAB_NOT_FOUND);
 68 |     }
 69 |     const tab = tabs[0];
 70 | 
 71 |     // Check URL restrictions
 72 |     if (
 73 |       tab.url?.startsWith('chrome://') ||
 74 |       tab.url?.startsWith('edge://') ||
 75 |       tab.url?.startsWith('https://chrome.google.com/webstore') ||
 76 |       tab.url?.startsWith('https://microsoftedge.microsoft.com/')
 77 |     ) {
 78 |       return createErrorResponse(
 79 |         'Cannot capture special browser pages or web store pages due to security restrictions.',
 80 |       );
 81 |     }
 82 | 
 83 |     let finalImageDataUrl: string | undefined;
 84 |     const results: any = { base64: null, fileSaved: false };
 85 |     let originalScroll = { x: 0, y: 0 };
 86 | 
 87 |     try {
 88 |       await this.injectContentScript(tab.id!, ['inject-scripts/screenshot-helper.js']);
 89 |       // Wait for script initialization
 90 |       await new Promise((resolve) => setTimeout(resolve, SCREENSHOT_CONSTANTS.SCRIPT_INIT_DELAY));
 91 |       // 1. Prepare page (hide scrollbars, potentially fixed elements)
 92 |       await this.sendMessageToTab(tab.id!, {
 93 |         action: TOOL_MESSAGE_TYPES.SCREENSHOT_PREPARE_PAGE_FOR_CAPTURE,
 94 |         options: { fullPage },
 95 |       });
 96 | 
 97 |       // Get initial page details, including original scroll position
 98 |       const pageDetails = await this.sendMessageToTab(tab.id!, {
 99 |         action: TOOL_MESSAGE_TYPES.SCREENSHOT_GET_PAGE_DETAILS,
100 |       });
101 |       originalScroll = { x: pageDetails.currentScrollX, y: pageDetails.currentScrollY };
102 | 
103 |       if (fullPage) {
104 |         this.logInfo('Capturing full page...');
105 |         finalImageDataUrl = await this._captureFullPage(tab.id!, args, pageDetails);
106 |       } else if (selector) {
107 |         this.logInfo(`Capturing element: ${selector}`);
108 |         finalImageDataUrl = await this._captureElement(tab.id!, args, pageDetails.devicePixelRatio);
109 |       } else {
110 |         // Visible area only
111 |         this.logInfo('Capturing visible area...');
112 |         finalImageDataUrl = await chrome.tabs.captureVisibleTab(tab.windowId, { format: 'png' });
113 |       }
114 | 
115 |       if (!finalImageDataUrl) {
116 |         throw new Error('Failed to capture image data');
117 |       }
118 | 
119 |       // 2. Process output
120 |       if (storeBase64 === true) {
121 |         // Compress image for base64 output to reduce size
122 |         const compressed = await compressImage(finalImageDataUrl, {
123 |           scale: 0.7, // Reduce dimensions by 30%
124 |           quality: 0.8, // 80% quality for good balance
125 |           format: 'image/jpeg', // JPEG for better compression
126 |         });
127 | 
128 |         // Include base64 data in response (without prefix)
129 |         const base64Data = compressed.dataUrl.replace(/^data:image\/[^;]+;base64,/, '');
130 |         results.base64 = base64Data;
131 |         return {
132 |           content: [
133 |             {
134 |               type: 'text',
135 |               text: JSON.stringify({ base64Data, mimeType: compressed.mimeType }),
136 |             },
137 |           ],
138 |           isError: false,
139 |         };
140 |       }
141 | 
142 |       if (savePng === true) {
143 |         // Save PNG file to downloads
144 |         this.logInfo('Saving PNG...');
145 |         try {
146 |           // Generate filename
147 |           const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
148 |           const filename = `${name.replace(/[^a-z0-9_-]/gi, '_') || 'screenshot'}_${timestamp}.png`;
149 | 
150 |           // Use Chrome's download API to save the file
151 |           const downloadId = await chrome.downloads.download({
152 |             url: finalImageDataUrl,
153 |             filename: filename,
154 |             saveAs: false,
155 |           });
156 | 
157 |           results.downloadId = downloadId;
158 |           results.filename = filename;
159 |           results.fileSaved = true;
160 | 
161 |           // Try to get the full file path
162 |           try {
163 |             // Wait a moment to ensure download info is updated
164 |             await new Promise((resolve) => setTimeout(resolve, 100));
165 | 
166 |             // Search for download item to get full path
167 |             const [downloadItem] = await chrome.downloads.search({ id: downloadId });
168 |             if (downloadItem && downloadItem.filename) {
169 |               // Add full path to response
170 |               results.fullPath = downloadItem.filename;
171 |             }
172 |           } catch (pathError) {
173 |             console.warn('Could not get full file path:', pathError);
174 |           }
175 |         } catch (error) {
176 |           console.error('Error saving PNG file:', error);
177 |           results.saveError = String(error instanceof Error ? error.message : error);
178 |         }
179 |       }
180 |     } catch (error) {
181 |       console.error('Error during screenshot execution:', error);
182 |       return createErrorResponse(
183 |         `Screenshot error: ${error instanceof Error ? error.message : JSON.stringify(error)}`,
184 |       );
185 |     } finally {
186 |       // 3. Reset page
187 |       try {
188 |         await this.sendMessageToTab(tab.id!, {
189 |           action: TOOL_MESSAGE_TYPES.SCREENSHOT_RESET_PAGE_AFTER_CAPTURE,
190 |           scrollX: originalScroll.x,
191 |           scrollY: originalScroll.y,
192 |         });
193 |       } catch (err) {
194 |         console.warn('Failed to reset page, tab might have closed:', err);
195 |       }
196 |     }
197 | 
198 |     this.logInfo('Screenshot completed!');
199 | 
200 |     return {
201 |       content: [
202 |         {
203 |           type: 'text',
204 |           text: JSON.stringify({
205 |             success: true,
206 |             message: `Screenshot [${name}] captured successfully`,
207 |             tabId: tab.id,
208 |             url: tab.url,
209 |             name: name,
210 |             ...results,
211 |           }),
212 |         },
213 |       ],
214 |       isError: false,
215 |     };
216 |   }
217 | 
218 |   /**
219 |    * Log information
220 |    */
221 |   private logInfo(message: string) {
222 |     console.log(`[Screenshot Tool] ${message}`);
223 |   }
224 | 
225 |   /**
226 |    * Capture specific element
227 |    */
228 |   async _captureElement(
229 |     tabId: number,
230 |     options: ScreenshotToolParams,
231 |     pageDpr: number,
232 |   ): Promise<string> {
233 |     const elementDetails = await this.sendMessageToTab(tabId, {
234 |       action: TOOL_MESSAGE_TYPES.SCREENSHOT_GET_ELEMENT_DETAILS,
235 |       selector: options.selector,
236 |     });
237 | 
238 |     const dpr = elementDetails.devicePixelRatio || pageDpr || 1;
239 | 
240 |     // Element rect is viewport-relative, in CSS pixels
241 |     // captureVisibleTab captures in physical pixels
242 |     const cropRectPx = {
243 |       x: elementDetails.rect.x * dpr,
244 |       y: elementDetails.rect.y * dpr,
245 |       width: elementDetails.rect.width * dpr,
246 |       height: elementDetails.rect.height * dpr,
247 |     };
248 | 
249 |     // Small delay to ensure element is fully rendered after scrollIntoView
250 |     await new Promise((resolve) => setTimeout(resolve, SCREENSHOT_CONSTANTS.SCRIPT_INIT_DELAY));
251 | 
252 |     const visibleCaptureDataUrl = await chrome.tabs.captureVisibleTab({ format: 'png' });
253 |     if (!visibleCaptureDataUrl) {
254 |       throw new Error('Failed to capture visible tab for element cropping');
255 |     }
256 | 
257 |     const croppedCanvas = await cropAndResizeImage(
258 |       visibleCaptureDataUrl,
259 |       cropRectPx,
260 |       dpr,
261 |       options.width, // Target output width in CSS pixels
262 |       options.height, // Target output height in CSS pixels
263 |     );
264 |     return canvasToDataURL(croppedCanvas);
265 |   }
266 | 
267 |   /**
268 |    * Capture full page
269 |    */
270 |   async _captureFullPage(
271 |     tabId: number,
272 |     options: ScreenshotToolParams,
273 |     initialPageDetails: any,
274 |   ): Promise<string> {
275 |     const dpr = initialPageDetails.devicePixelRatio;
276 |     const totalWidthCss = options.width || initialPageDetails.totalWidth; // Use option width if provided
277 |     const totalHeightCss = initialPageDetails.totalHeight; // Full page always uses actual height
278 | 
279 |     // Apply maximum height limit for infinite scroll pages
280 |     const maxHeightPx = options.maxHeight || SCREENSHOT_CONSTANTS.MAX_CAPTURE_HEIGHT_PX;
281 |     const limitedHeightCss = Math.min(totalHeightCss, maxHeightPx / dpr);
282 | 
283 |     const totalWidthPx = totalWidthCss * dpr;
284 |     const totalHeightPx = limitedHeightCss * dpr;
285 | 
286 |     // Viewport dimensions (CSS pixels) - logged for debugging
287 |     this.logInfo(
288 |       `Viewport size: ${initialPageDetails.viewportWidth}x${initialPageDetails.viewportHeight} CSS pixels`,
289 |     );
290 |     this.logInfo(
291 |       `Page dimensions: ${totalWidthCss}x${totalHeightCss} CSS pixels (limited to ${limitedHeightCss} height)`,
292 |     );
293 | 
294 |     const viewportHeightCss = initialPageDetails.viewportHeight;
295 | 
296 |     const capturedParts = [];
297 |     let currentScrollYCss = 0;
298 |     let capturedHeightPx = 0;
299 |     let partIndex = 0;
300 | 
301 |     while (capturedHeightPx < totalHeightPx && partIndex < SCREENSHOT_CONSTANTS.MAX_CAPTURE_PARTS) {
302 |       this.logInfo(
303 |         `Capturing part ${partIndex + 1}... (${Math.round((capturedHeightPx / totalHeightPx) * 100)}%)`,
304 |       );
305 | 
306 |       if (currentScrollYCss > 0) {
307 |         // Don't scroll for the first part if already at top
308 |         const scrollResp = await this.sendMessageToTab(tabId, {
309 |           action: TOOL_MESSAGE_TYPES.SCREENSHOT_SCROLL_PAGE,
310 |           x: 0,
311 |           y: currentScrollYCss,
312 |           scrollDelay: SCREENSHOT_CONSTANTS.SCROLL_DELAY_MS,
313 |         });
314 |         // Update currentScrollYCss based on actual scroll achieved
315 |         currentScrollYCss = scrollResp.newScrollY;
316 |       }
317 | 
318 |       // Ensure rendering after scroll
319 |       await new Promise((resolve) =>
320 |         setTimeout(resolve, SCREENSHOT_CONSTANTS.CAPTURE_STITCH_DELAY_MS),
321 |       );
322 | 
323 |       const dataUrl = await chrome.tabs.captureVisibleTab({ format: 'png' });
324 |       if (!dataUrl) throw new Error('captureVisibleTab returned empty during full page capture');
325 | 
326 |       const yOffsetPx = currentScrollYCss * dpr;
327 |       capturedParts.push({ dataUrl, y: yOffsetPx });
328 | 
329 |       const imgForHeight = await createImageBitmapFromUrl(dataUrl); // To get actual captured height
330 |       const lastPartEffectiveHeightPx = Math.min(imgForHeight.height, totalHeightPx - yOffsetPx);
331 | 
332 |       capturedHeightPx = yOffsetPx + lastPartEffectiveHeightPx;
333 | 
334 |       if (capturedHeightPx >= totalHeightPx - SCREENSHOT_CONSTANTS.PIXEL_TOLERANCE) break;
335 | 
336 |       currentScrollYCss += viewportHeightCss;
337 |       // Prevent overscrolling past the document height for the next scroll command
338 |       if (
339 |         currentScrollYCss > totalHeightCss - viewportHeightCss &&
340 |         currentScrollYCss < totalHeightCss
341 |       ) {
342 |         currentScrollYCss = totalHeightCss - viewportHeightCss;
343 |       }
344 |       partIndex++;
345 |     }
346 | 
347 |     // Check if we hit any limits
348 |     if (partIndex >= SCREENSHOT_CONSTANTS.MAX_CAPTURE_PARTS) {
349 |       this.logInfo(
350 |         `Reached maximum number of capture parts (${SCREENSHOT_CONSTANTS.MAX_CAPTURE_PARTS}). This may be an infinite scroll page.`,
351 |       );
352 |     }
353 |     if (totalHeightCss > limitedHeightCss) {
354 |       this.logInfo(
355 |         `Page height (${totalHeightCss}px) exceeds maximum capture height (${maxHeightPx / dpr}px). Capturing limited portion.`,
356 |       );
357 |     }
358 | 
359 |     this.logInfo('Stitching image...');
360 |     const finalCanvas = await stitchImages(capturedParts, totalWidthPx, totalHeightPx);
361 | 
362 |     // If user specified width but not height (or vice versa for full page), resize maintaining aspect ratio
363 |     let outputCanvas = finalCanvas;
364 |     if (options.width && !options.height) {
365 |       const targetWidthPx = options.width * dpr;
366 |       const aspectRatio = finalCanvas.height / finalCanvas.width;
367 |       const targetHeightPx = targetWidthPx * aspectRatio;
368 |       outputCanvas = new OffscreenCanvas(targetWidthPx, targetHeightPx);
369 |       const ctx = outputCanvas.getContext('2d');
370 |       if (ctx) {
371 |         ctx.drawImage(finalCanvas, 0, 0, targetWidthPx, targetHeightPx);
372 |       }
373 |     } else if (options.height && !options.width) {
374 |       const targetHeightPx = options.height * dpr;
375 |       const aspectRatio = finalCanvas.width / finalCanvas.height;
376 |       const targetWidthPx = targetHeightPx * aspectRatio;
377 |       outputCanvas = new OffscreenCanvas(targetWidthPx, targetHeightPx);
378 |       const ctx = outputCanvas.getContext('2d');
379 |       if (ctx) {
380 |         ctx.drawImage(finalCanvas, 0, 0, targetWidthPx, targetHeightPx);
381 |       }
382 |     } else if (options.width && options.height) {
383 |       // Both specified, direct resize
384 |       const targetWidthPx = options.width * dpr;
385 |       const targetHeightPx = options.height * dpr;
386 |       outputCanvas = new OffscreenCanvas(targetWidthPx, targetHeightPx);
387 |       const ctx = outputCanvas.getContext('2d');
388 |       if (ctx) {
389 |         ctx.drawImage(finalCanvas, 0, 0, targetWidthPx, targetHeightPx);
390 |       }
391 |     }
392 | 
393 |     return canvasToDataURL(outputCanvas);
394 |   }
395 | }
396 | 
397 | export const screenshotTool = new ScreenshotTool();
398 | 
```
Page 3/10FirstPrevNextLast