From a6c2a7542a160b0e32e6950e3fd960e30308033a Mon Sep 17 00:00:00 2001 From: Dinesh Yadav <13635627+HumbleBee14@users.noreply.github.com> Date: Fri, 30 Jan 2026 15:47:58 -0500 Subject: [PATCH 1/8] Remove HTTP/SSE MCP transport and tools --- .../roopik/browser/mcpServerServiceClient.ts | 18 +- .../roopik/browser/roopik.contribution.ts | 14 - .../roopik/common/mcp/mcpServerService.ts | 22 +- .../electron-main/channel/mcpServerChannel.ts | 10 +- .../mcp/executor/browserExecutor.ts | 18 + .../electron-main/mcp/mcpServerService.ts | 213 +-- .../electron-main/mcp/tools/browserTools.ts | 1573 ----------------- .../electron-main/mcp/tools/canvasTools.ts | 595 ------- .../electron-main/mcp/tools/contextPrompts.ts | 424 ----- .../electron-main/mcp/tools/projectTools.ts | 239 --- .../projectMode/browserViewService.ts | 2 +- 11 files changed, 31 insertions(+), 3097 deletions(-) delete mode 100644 src/vs/workbench/contrib/roopik/electron-main/mcp/tools/browserTools.ts delete mode 100644 src/vs/workbench/contrib/roopik/electron-main/mcp/tools/canvasTools.ts delete mode 100644 src/vs/workbench/contrib/roopik/electron-main/mcp/tools/contextPrompts.ts delete mode 100644 src/vs/workbench/contrib/roopik/electron-main/mcp/tools/projectTools.ts diff --git a/src/vs/workbench/contrib/roopik/browser/mcpServerServiceClient.ts b/src/vs/workbench/contrib/roopik/browser/mcpServerServiceClient.ts index 701ee3cee..906523b43 100644 --- a/src/vs/workbench/contrib/roopik/browser/mcpServerServiceClient.ts +++ b/src/vs/workbench/contrib/roopik/browser/mcpServerServiceClient.ts @@ -59,16 +59,12 @@ export class McpServerServiceClient implements IMcpServerService { return this.channel.call('getStatus'); } - async getPort(): Promise { - return this.channel.call('getPort'); - } - async getWsPort(): Promise { return this.channel.call('getWsPort'); } // ======================================== - // STDIO/WebSocket Control + // STDIO/Websocket Server Control // ======================================== async isEnabled(): Promise { @@ -79,18 +75,6 @@ export class McpServerServiceClient implements IMcpServerService { return this.channel.call('setEnabled', [enabled]); } - // ======================================== - // HTTP Control - // ======================================== - - async isHttpEnabled(): Promise { - return this.channel.call('isHttpEnabled'); - } - - async setHttpEnabled(enabled: boolean): Promise { - return this.channel.call('setHttpEnabled', [enabled]); - } - // ======================================== // Agent Control // ======================================== diff --git a/src/vs/workbench/contrib/roopik/browser/roopik.contribution.ts b/src/vs/workbench/contrib/roopik/browser/roopik.contribution.ts index eae18fbd4..fcc181527 100644 --- a/src/vs/workbench/contrib/roopik/browser/roopik.contribution.ts +++ b/src/vs/workbench/contrib/roopik/browser/roopik.contribution.ts @@ -155,20 +155,6 @@ Registry.as(ConfigurationExtensions.Configuration).regis order: 13, description: 'Unregister from all AI agents when Roopik exits. If false, registrations persist for quick reconnection.' }, - 'roopik.mcp.httpMCP': { - type: 'boolean', - default: false, - order: 14, - markdownDescription: '**Enable HTTP/SSE MCP** (Experimental) - Enable HTTP-based MCP transport with Server-Sent Events.' - }, - 'roopik.mcp.httpMCPPort': { - type: 'number', - default: 3333, - minimum: 1024, - maximum: 65535, - order: 15, - description: 'Only used when HTTP/SSE MCP is enabled.' - }, // ======================================== // MCP Agent Integrations (individual toggles) diff --git a/src/vs/workbench/contrib/roopik/common/mcp/mcpServerService.ts b/src/vs/workbench/contrib/roopik/common/mcp/mcpServerService.ts index 577e5b59a..a04aa5983 100644 --- a/src/vs/workbench/contrib/roopik/common/mcp/mcpServerService.ts +++ b/src/vs/workbench/contrib/roopik/common/mcp/mcpServerService.ts @@ -29,14 +29,8 @@ export const IMcpServerService = createDecorator('mcpServerSe export interface McpServerStatus { /** Whether the STDIO/WebSocket MCP server is running */ running: boolean; - /** Whether the HTTP MCP server is running */ - httpRunning: boolean; - /** HTTP port for StreamableHTTP transport */ - port: number; /** WebSocket port for STDIO binary connections */ wsPort: number; - /** MCP HTTP endpoint URL */ - url: string; /** Error message if server failed to start */ error?: string; } @@ -108,9 +102,6 @@ export interface IMcpServerService { /** Get current server status */ getStatus(): Promise; - /** Get the HTTP port the server is running on */ - getPort(): Promise; - /** Get the WebSocket port for STDIO connections */ getWsPort(): Promise; @@ -118,21 +109,12 @@ export interface IMcpServerService { // STDIO/WebSocket Control (Primary) // ======================================== - /** Check if STDIO/WebSocket MCP is enabled */ + /** Check if STDIO/WebSocket MCP Server is enabled */ isEnabled(): Promise; - /** Enable or disable STDIO/WebSocket MCP server */ + /** Enable or disable MCP server */ setEnabled(enabled: boolean): Promise; - // ======================================== - // HTTP Control (Advanced) - // ======================================== - - /** Check if HTTP MCP is enabled */ - isHttpEnabled(): Promise; - - /** Enable or disable HTTP MCP server */ - setHttpEnabled(enabled: boolean): Promise; // ======================================== // Agent Control diff --git a/src/vs/workbench/contrib/roopik/electron-main/channel/mcpServerChannel.ts b/src/vs/workbench/contrib/roopik/electron-main/channel/mcpServerChannel.ts index 80599abdf..fb1aeec11 100644 --- a/src/vs/workbench/contrib/roopik/electron-main/channel/mcpServerChannel.ts +++ b/src/vs/workbench/contrib/roopik/electron-main/channel/mcpServerChannel.ts @@ -52,23 +52,15 @@ export class McpServerChannel implements IServerChannel { return this.service.restart(); case 'getStatus': return this.service.getStatus(); - case 'getPort': - return this.service.getPort(); case 'getWsPort': return this.service.getWsPort(); - // STDIO/WebSocket control + // Server control case 'isEnabled': return this.service.isEnabled(); case 'setEnabled': return this.service.setEnabled(args?.[0] ?? true); - // HTTP control - case 'isHttpEnabled': - return this.service.isHttpEnabled(); - case 'setHttpEnabled': - return this.service.setHttpEnabled(args?.[0] ?? false); - // Agent control case 'getAgentStatus': return this.service.getAgentStatus(); diff --git a/src/vs/workbench/contrib/roopik/electron-main/mcp/executor/browserExecutor.ts b/src/vs/workbench/contrib/roopik/electron-main/mcp/executor/browserExecutor.ts index 3de52c8d6..35c37a8cc 100644 --- a/src/vs/workbench/contrib/roopik/electron-main/mcp/executor/browserExecutor.ts +++ b/src/vs/workbench/contrib/roopik/electron-main/mcp/executor/browserExecutor.ts @@ -75,6 +75,24 @@ interface CDPMonitor { const cdpMonitors = new Map(); +/** + * Cleanup CDP monitoring for a specific browser view. + * Exported for use by BrowserViewService when closing views. + */ +export function cleanupCDPMonitoring(browserViewId: number): void { + const monitor = cdpMonitors.get(browserViewId); + if (monitor) { + for (const cleanup of monitor.cleanupFunctions) { + try { + cleanup(); + } catch { + // Ignore cleanup errors + } + } + cdpMonitors.delete(browserViewId); + } +} + // ============================================================================ // Browser Executor Class // ============================================================================ diff --git a/src/vs/workbench/contrib/roopik/electron-main/mcp/mcpServerService.ts b/src/vs/workbench/contrib/roopik/electron-main/mcp/mcpServerService.ts index 5f7b78ffd..49f2dfa76 100644 --- a/src/vs/workbench/contrib/roopik/electron-main/mcp/mcpServerService.ts +++ b/src/vs/workbench/contrib/roopik/electron-main/mcp/mcpServerService.ts @@ -6,13 +6,12 @@ /** * MCP Server Service * - * Main process service that hosts MCP (Model Context Protocol) servers. + * Main process service that hosts the MCP (Model Context Protocol) server. * * Architecture: - * - HTTP Server (port 3333): StreamableHTTP for direct agent connections * - WebSocket Server (port 9876): For STDIO binary connections from external agents * - Installer: Auto-registers with Claude Code, Codex, Gemini, Windsurf, Cursor - * - Unified ToolExecutor: Both transports use same tool execution layer + * - Unified ToolExecutor: All transports use same tool execution layer * * External Agent Flow: * 1. On startup, Installer registers Roopik STDIO binary with external agents @@ -21,7 +20,6 @@ * 4. Tool calls flow: Agent -> STDIO -> WebSocket -> ToolExecutor -> IDE Services */ -import * as http from 'http'; import * as crypto from 'crypto'; import { Emitter, Event } from '../../../../../base/common/event.js'; import { Disposable } from '../../../../../base/common/lifecycle.js'; @@ -34,12 +32,8 @@ import type { BrowserViewService } from '../projectMode/browserViewService.js'; import type { ComponentService } from '../component/componentService.js'; import type { ICanvasService } from '../../common/canvas/canvasService.js'; import type { IRoopikStorageService } from '../../common/storage/storageService.js'; -import { registerProjectTools } from './tools/projectTools.js'; -import { registerBrowserTools } from './tools/browserTools.js'; -import { registerCanvasTools } from './tools/canvasTools.js'; -import { registerContextPrompts } from './tools/contextPrompts.js'; -// Phase 5: Integration imports +// MCP Server Components import { ToolExecutor } from './executor/index.js'; import { McpWebSocketServer } from './websocket/index.js'; import { McpInstaller, type McpPlatformAdapter, type McpIntegrationSettings } from './installer/index.js'; @@ -52,9 +46,7 @@ export class McpServerService extends Disposable implements IMcpServerService { readonly _serviceBrand: undefined; // Port configuration - private static readonly DEFAULT_HTTP_PORT = 3333; private static readonly DEFAULT_WS_PORT = 9876; - private static readonly MAX_PORT_ATTEMPTS = 10; private readonly logger; @@ -65,11 +57,6 @@ export class McpServerService extends Disposable implements IMcpServerService { private readonly _onAgentStatusChanged = this._register(new Emitter()); readonly onAgentStatusChanged: Event = this._onAgentStatusChanged.event; - // HTTP Server (StreamableHTTP transport) - private httpServer: http.Server | null = null; - private httpPort: number = McpServerService.DEFAULT_HTTP_PORT; - private mcpServer: any = null; - // WebSocket Server (for STDIO binaries) private wsServer: McpWebSocketServer | null = null; private wsPort: number = McpServerService.DEFAULT_WS_PORT; @@ -117,7 +104,7 @@ export class McpServerService extends Disposable implements IMcpServerService { } async start(): Promise { - // Initialize Tool Executor (unified execution layer) - shared by both transports + // Initialize Tool Executor (unified execution layer) if (!this.toolExecutor) { this.toolExecutor = new ToolExecutor( this.browserViewService, @@ -128,7 +115,7 @@ export class McpServerService extends Disposable implements IMcpServerService { ); } - // Check STDIO/WebSocket setting (primary transport) + // Check STDIO/WebSocket setting const wsEnabled = this.configurationService.getValue('roopik.mcp.stdioMCP') ?? true; if (wsEnabled && !this.wsServer) { await this.startWebSocketServer(); @@ -136,12 +123,6 @@ export class McpServerService extends Disposable implements IMcpServerService { await this.initializeInstaller(); } - // Check HTTP setting (advanced transport) - const httpEnabled = this.configurationService.getValue('roopik.mcp.httpMCP') ?? false; - if (httpEnabled && !this.httpServer) { - await this.startHttpServer(); - } - // Listen for settings changes if (!this.settingsDisposable) { this.setupSettingsListener(); @@ -152,10 +133,9 @@ export class McpServerService extends Disposable implements IMcpServerService { async stop(): Promise { await this.stopWsServer(); - await this.stopHttpServer(); - // Dispose settings listener only if both servers are stopped - if (!this.wsServer && !this.httpServer && this.settingsDisposable) { + // Dispose settings listener + if (!this.wsServer && this.settingsDisposable) { this.settingsDisposable.dispose(); this.settingsDisposable = null; } @@ -180,126 +160,6 @@ export class McpServerService extends Disposable implements IMcpServerService { } } - private async stopHttpServer(): Promise { - // Stop HTTP server - if (this.httpServer) { - await new Promise((resolve) => { - this.httpServer!.close(() => { - this.httpServer = null; - resolve(); - }); - }); - this.logger.info('HTTP MCP server stopped'); - } - } - - // ============================================================================ - // HTTP Server (StreamableHTTP Transport) - // ============================================================================ - - private async startHttpServer(): Promise { - const configuredPort = this.configurationService.getValue('roopik.mcp.httpMCPPort') || McpServerService.DEFAULT_HTTP_PORT; - - // Dynamic imports (bypass VSCode layering restrictions) - const { McpServer } = await import('@modelcontextprotocol/sdk/server/mcp.js'); - const { StreamableHTTPServerTransport } = await import('@modelcontextprotocol/sdk/server/streamableHttp.js'); - const { z } = await import('zod'); - - // Initialize MCP Server - this.mcpServer = new McpServer({ - name: 'roopik-ide', - version: '1.0.0', - }); - - // Register tools - registerProjectTools(this.mcpServer, z, this.devServerService, this.browserViewService, this.storageService); - registerBrowserTools(this.mcpServer, z, this.browserViewService, this.storageService); - registerCanvasTools(this.mcpServer, z, this.canvasService, this.componentService); - registerContextPrompts(this.mcpServer); - - // Start with retry logic - this.httpPort = await this.listenWithRetry(configuredPort, StreamableHTTPServerTransport); - this.logger.info('HTTP MCP server started', { port: this.httpPort }); - } - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - private async listenWithRetry(startPort: number, StreamableHTTPServerTransport: any, attempt: number = 0): Promise { - const currentPort = startPort + attempt; - - if (attempt >= McpServerService.MAX_PORT_ATTEMPTS) { - throw new Error(`Could not find open port after ${McpServerService.MAX_PORT_ATTEMPTS} attempts`); - } - - return new Promise((resolve, reject) => { - this.httpServer = http.createServer(async (req, res) => { - const sessionId = req.headers['mcp-session-id'] || 'new'; - - // CORS headers - res.setHeader('Access-Control-Allow-Origin', '*'); - res.setHeader('Access-Control-Allow-Methods', 'GET, POST, DELETE, OPTIONS'); - res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, Mcp-Session-Id'); - res.setHeader('Access-Control-Expose-Headers', 'Mcp-Session-Id'); - - if (req.method === 'OPTIONS') { - res.writeHead(204); - res.end(); - return; - } - - const url = new URL(req.url || '/', `http://localhost:${currentPort}`); - - if (url.pathname === '/mcp') { - try { - const transport = new StreamableHTTPServerTransport({}); - await this.mcpServer.connect(transport); - await transport.handleRequest(req, res); - if (!res.headersSent) { - res.writeHead(500, { 'Content-Type': 'application/json' }); - res.end(JSON.stringify({ error: 'Internal Server Error' })); - } - } catch (error) { - this.logger.error('Error handling MCP request', { sessionId, error }); - if (!res.headersSent) { - res.writeHead(500, { 'Content-Type': 'application/json' }); - res.end(JSON.stringify({ error: 'Internal Server Error' })); - } - } - return; - } - - if (url.pathname === '/health' && req.method === 'GET') { - res.writeHead(200, { 'Content-Type': 'application/json' }); - res.end(JSON.stringify({ - status: 'ok', - server: 'roopik-mcp', - version: '1.0.0', - httpPort: this.httpPort, - wsPort: this.wsPort - })); - return; - } - - res.writeHead(404, { 'Content-Type': 'application/json' }); - res.end(JSON.stringify({ error: 'Not found' })); - }); - - this.httpServer.on('error', (error: NodeJS.ErrnoException) => { - if (error.code === 'EADDRINUSE') { - this.logger.warn('HTTP port busy, trying next', { port: currentPort }); - this.httpServer?.close(); - this.httpServer = null; - this.listenWithRetry(startPort, StreamableHTTPServerTransport, attempt + 1) - .then(resolve) - .catch(reject); - } else { - reject(error); - } - }); - - this.httpServer.listen(currentPort, '127.0.0.1', () => resolve(currentPort)); - }); - } - // ============================================================================ // WebSocket Server (for STDIO Binary connections) // ============================================================================ @@ -397,18 +257,6 @@ export class McpServerService extends Disposable implements IMcpServerService { } } - // Handle HTTP switch changes - if (e.affectsConfiguration('roopik.mcp.httpMCP')) { - const enabled = this.configurationService.getValue('roopik.mcp.httpMCP') ?? false; - if (!enabled && this.httpServer) { - this.logger.info('HTTP MCP disabled via settings'); - await this.setHttpEnabled(false); - } else if (enabled && !this.httpServer) { - this.logger.info('HTTP MCP enabled via settings'); - await this.setHttpEnabled(true); - } - } - // Handle agent settings changes if (e.affectsConfiguration('roopik.mcp.agents')) { this.logger.info('MCP agent settings changed, syncing registrations'); @@ -428,17 +276,10 @@ export class McpServerService extends Disposable implements IMcpServerService { async getStatus(): Promise { return { running: this.wsServer !== null, - httpRunning: this.httpServer !== null, - port: this.httpPort, - wsPort: this.wsPort, - url: `http://127.0.0.1:${this.httpPort}/mcp` + wsPort: this.wsPort }; } - async getPort(): Promise { - return this.httpPort; - } - async getWsPort(): Promise { return this.wsPort; } @@ -495,44 +336,6 @@ export class McpServerService extends Disposable implements IMcpServerService { this._onStatusChanged.fire(await this.getStatus()); } - // ============================================================================ - // HTTP Control (Advanced) - // ============================================================================ - - async isHttpEnabled(): Promise { - return this.configurationService.getValue('roopik.mcp.httpMCP') ?? false; - } - - async setHttpEnabled(enabled: boolean): Promise { - // Update the setting - await this.configurationService.updateValue('roopik.mcp.httpMCP', enabled); - - if (enabled) { - // Start HTTP if not running - if (!this.httpServer) { - // Initialize Tool Executor if needed - if (!this.toolExecutor) { - this.toolExecutor = new ToolExecutor( - this.browserViewService, - this.storageService, - this.canvasService, - this.componentService, - this.devServerService - ); - } - await this.startHttpServer(); - if (!this.settingsDisposable) { - this.setupSettingsListener(); - } - } - } else { - // Stop HTTP server - await this.stopHttpServer(); - } - - this._onStatusChanged.fire(await this.getStatus()); - } - // ============================================================================ // Agent Control // ============================================================================ diff --git a/src/vs/workbench/contrib/roopik/electron-main/mcp/tools/browserTools.ts b/src/vs/workbench/contrib/roopik/electron-main/mcp/tools/browserTools.ts deleted file mode 100644 index fdeab9a22..000000000 --- a/src/vs/workbench/contrib/roopik/electron-main/mcp/tools/browserTools.ts +++ /dev/null @@ -1,1573 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Roopik. All rights reserved. - * Licensed under the MIT License. - *--------------------------------------------------------------------------------------------*/ - -/** - * Browser Tools (Project Mode) - * - * MCP tools for browser interaction in Project Mode. - * These give AI agents visual verification, DOM inspection, and debugging capabilities. - * - * Includes: - * - Core browser tools: screenshot, navigate, reload, execute_script, inspect_element - * - CDP tools: get_errors, get_console_logs (auto-attaches debugger when first used) - */ - -import type { BrowserViewService } from '../../projectMode/browserViewService.js'; -import type { IRoopikStorageService } from '../../../common/storage/storageService.js'; - -// ============================================================================ -// CDP Types -// ============================================================================ - -interface ConsoleLog { - id: string; - timestamp: number; - type: 'log' | 'debug' | 'info' | 'warn' | 'error' | 'dir' | 'dirxml' | 'table' | 'trace' | 'clear' | 'assert'; - message: string; - args?: unknown[]; - location?: string; - stackTrace?: { callFrames: Array<{ url: string; lineNumber: number; columnNumber: number; functionName: string }> }; -} - -interface NetworkRequest { - id: string; - requestId: string; - timestamp: number; - method: string; - url: string; - headers: Record; - postData?: string; -} - -interface NetworkResponse { - id: string; - requestId: string; - timestamp: number; - status: number; - statusText: string; - url: string; - headers: Record; - mimeType?: string; - duration?: number; -} - -interface CDPMonitor { - browserViewId: number; - consoleLogs: ConsoleLog[]; - networkRequests: NetworkRequest[]; - networkResponses: NetworkResponse[]; - requestStartTimes: Map; - cleanupFunctions: Array<() => void>; -} - -// ============================================================================ -// CDP Monitoring State -// ============================================================================ - -const cdpMonitors = new Map(); - -async function ensureCDPMonitoring( - browserViewId: number, - browserViewService: BrowserViewService -): Promise { - // Clean up stale monitors - for (const [monitoredId] of cdpMonitors) { - if (monitoredId !== browserViewId) { - cleanupCDPMonitoring(monitoredId); - } - } - - if (cdpMonitors.has(browserViewId)) { - return; - } - - const monitor: CDPMonitor = { - browserViewId, - consoleLogs: [], - networkRequests: [], - networkResponses: [], - requestStartTimes: new Map(), - cleanupFunctions: [] - }; - - await browserViewService.attachDebugger(browserViewId); - await browserViewService.sendCDPCommand(browserViewId, 'Runtime.enable'); - await browserViewService.sendCDPCommand(browserViewId, 'Network.enable'); - await browserViewService.sendCDPCommand(browserViewId, 'Page.enable'); - - // Console logging - const consoleCleanup = browserViewService.onCDPEvent(browserViewId, (method, params) => { - if (method === 'Runtime.consoleAPICalled') { - handleConsoleMessage(monitor, params as any); - } else if (method === 'Runtime.exceptionThrown') { - handleException(monitor, params as any); - } - }); - monitor.cleanupFunctions.push(consoleCleanup); - - // Network monitoring - const networkCleanup = browserViewService.onCDPEvent(browserViewId, (method, params) => { - if (method === 'Network.requestWillBeSent') { - handleNetworkRequest(monitor, params as any); - } else if (method === 'Network.responseReceived') { - handleNetworkResponse(monitor, params as any); - } else if (method === 'Network.loadingFailed') { - handleNetworkFailure(monitor, params as any); - } - }); - monitor.cleanupFunctions.push(networkCleanup); - - cdpMonitors.set(browserViewId, monitor); -} - -function handleConsoleMessage(monitor: CDPMonitor, params: { - type: string; - args: Array<{ type: string; value?: any; unserializableValue?: string; description?: string }>; - timestamp: number; - stackTrace?: { callFrames: Array<{ url: string; lineNumber: number; columnNumber: number; functionName: string }> }; -}): void { - const { type, args, timestamp, stackTrace } = params; - - const messages = args.map(arg => { - if (arg.value !== undefined) { - return typeof arg.value === 'object' ? JSON.stringify(arg.value) : String(arg.value); - } - if (arg.unserializableValue) return arg.unserializableValue; - if (arg.description) return arg.description; - return '[Object]'; - }); - - const location = stackTrace?.callFrames?.[0] - ? `${stackTrace.callFrames[0].url}:${stackTrace.callFrames[0].lineNumber}:${stackTrace.callFrames[0].columnNumber}` - : undefined; - - const log: ConsoleLog = { - id: `console-${Date.now()}-${Math.random()}`, - timestamp: timestamp * 1000, - type: type as ConsoleLog['type'], - message: messages.join(' '), - args: args.map(arg => arg.value), - location, - stackTrace - }; - - monitor.consoleLogs.push(log); - if (monitor.consoleLogs.length > 1000) { - monitor.consoleLogs.shift(); - } -} - -function handleException(monitor: CDPMonitor, params: { - timestamp: number; - exceptionDetails: { - text: string; - lineNumber?: number; - columnNumber?: number; - url?: string; - stackTrace?: { callFrames: Array<{ url: string; lineNumber: number; columnNumber: number; functionName: string }> }; - exception?: { - type?: string; - value?: unknown; - description?: string; - className?: string; - }; - }; -}): void { - const { timestamp, exceptionDetails } = params; - - const location = exceptionDetails.url && exceptionDetails.lineNumber !== undefined - ? `${exceptionDetails.url}:${exceptionDetails.lineNumber}:${exceptionDetails.columnNumber}` - : undefined; - - // Build full error message: - // 1. Start with exception.description (full error like "TypeError: Cannot read property 'foo' of null") - // 2. Fall back to exception.className + exception.value - // 3. Fall back to exceptionDetails.text (usually just "Uncaught") - let message = exceptionDetails.text || 'Unknown error'; - - if (exceptionDetails.exception) { - const exc = exceptionDetails.exception; - if (exc.description) { - // description contains full error message with stack trace - message = exc.description; - } else if (exc.className && exc.value !== undefined) { - // Construct message from class name and value - message = `${exc.className}: ${exc.value}`; - } else if (exc.value !== undefined) { - message = String(exc.value); - } - } - - const log: ConsoleLog = { - id: `exception-${Date.now()}-${Math.random()}`, - timestamp: timestamp * 1000, - type: 'error', - message, - location, - stackTrace: exceptionDetails.stackTrace - }; - - monitor.consoleLogs.push(log); - if (monitor.consoleLogs.length > 1000) { - monitor.consoleLogs.shift(); - } -} - -function handleNetworkRequest(monitor: CDPMonitor, params: { - requestId: string; - request: { url: string; method: string; headers: Record; postData?: string }; - timestamp: number; -}): void { - const { requestId, request, timestamp } = params; - - const networkRequest: NetworkRequest = { - id: `req-${Date.now()}-${Math.random()}`, - requestId, - timestamp: timestamp * 1000, - method: request.method, - url: request.url, - headers: request.headers || {}, - postData: request.postData - }; - - monitor.networkRequests.push(networkRequest); - monitor.requestStartTimes.set(requestId, timestamp); - - if (monitor.networkRequests.length > 500) { - monitor.networkRequests.shift(); - } -} - -function handleNetworkResponse(monitor: CDPMonitor, params: { - requestId: string; - response: { url: string; status: number; statusText: string; headers: Record; mimeType?: string }; - timestamp: number; -}): void { - const { requestId, response, timestamp } = params; - - const startTime = monitor.requestStartTimes.get(requestId); - const duration = startTime ? (timestamp - startTime) * 1000 : undefined; - - const networkResponse: NetworkResponse = { - id: `res-${Date.now()}-${Math.random()}`, - requestId, - timestamp: timestamp * 1000, - status: response.status, - statusText: response.statusText, - url: response.url, - headers: response.headers || {}, - mimeType: response.mimeType, - duration - }; - - monitor.networkResponses.push(networkResponse); - if (monitor.networkResponses.length > 500) { - monitor.networkResponses.shift(); - } -} - -function handleNetworkFailure(monitor: CDPMonitor, params: { - requestId: string; - timestamp: number; - errorText: string; -}): void { - const { requestId, timestamp, errorText } = params; - - const networkResponse: NetworkResponse = { - id: `fail-${Date.now()}-${Math.random()}`, - requestId, - timestamp: timestamp * 1000, - status: 0, - statusText: errorText, - url: '', - headers: {} - }; - - monitor.networkResponses.push(networkResponse); - if (monitor.networkResponses.length > 500) { - monitor.networkResponses.shift(); - } -} - -function getMonitorOrThrow(browserViewId: number | undefined): CDPMonitor { - if (browserViewId === undefined) { - throw new Error('No browser is open. Start a project first with project_start.'); - } - - const monitor = cdpMonitors.get(browserViewId); - if (!monitor) { - throw new Error('CDP monitoring not initialized. Try running the command again.'); - } - - return monitor; -} - -// ============================================================================ -// CDP Cleanup (exported for external use) -// ============================================================================ - -export function cleanupCDPMonitoring(browserViewId: number): void { - const monitor = cdpMonitors.get(browserViewId); - if (monitor) { - for (const cleanup of monitor.cleanupFunctions) { - try { - cleanup(); - } catch (e) { - console.error('[Browser Tools] Error during CDP cleanup:', e); - } - } - cdpMonitors.delete(browserViewId); - } -} - -// ============================================================================ -// Tool Registration -// ============================================================================ - -/** - * Register all browser-related MCP tools - * - * @param server - McpServer instance (dynamically imported) - * @param z - Zod validation library (dynamically imported) - * @param browserViewService - BrowserView service instance - * @param storageService - Storage service instance (for workspace path) - */ -export function registerBrowserTools( - // eslint-disable-next-line @typescript-eslint/no-explicit-any - server: any, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - z: any, - browserViewService: BrowserViewService, - storageService: IRoopikStorageService -): void { - - // ============================================================================ - // CORE BROWSER TOOLS (12) - // ============================================================================ - - // -------------------------------------------------------------- - // TOOL: Open Browser - // -------------------------------------------------------------- - server.tool( - 'browser_open', - '[Roopik IDE] Open the browser or navigate to a URL. If browser is not open, opens it with proper editor UI. If URL is provided, navigates to that URL after opening.', - { - url: z.string().optional().describe('URL to navigate to after browser opens (optional)') - }, - async ({ url }: { url?: string }) => { - try { - // Check if browser is already open - const browserViewId = browserViewService.getActiveBrowserViewId(); - - if (browserViewId === undefined) { - // Browser not open - fire event for renderer to open it with proper UI - // The renderer's projectModeContribution listens and opens browser editor - // URL normalization is done in browserViewService.navigate() - browserViewService.requestBrowserOpen(url); - - return { - content: [{ - type: 'text' as const, - text: JSON.stringify({ - success: true, - message: 'Browser open request sent. The browser will open shortly.', - url: url || undefined - }) - }] - }; - } - - // Browser is already open - navigate if URL provided - // URL normalization is done in browserViewService.navigate() - if (url) { - await browserViewService.navigate(browserViewId, url); - // Enable CDP monitoring proactively to capture errors from page load - await ensureCDPMonitoring(browserViewId, browserViewService); - return { - content: [{ - type: 'text' as const, - text: JSON.stringify({ - success: true, - browserViewId, - url, - message: `Navigated to ${url}` - }) - }] - }; - } - - // Enable CDP monitoring even if not navigating (browser already open) - await ensureCDPMonitoring(browserViewId, browserViewService); - - return { - content: [{ - type: 'text' as const, - text: JSON.stringify({ - success: true, - browserViewId, - message: 'Browser is already open' - }) - }] - }; - } catch (error) { - const errorMessage = error instanceof Error ? error.message : 'Unknown error'; - return { - content: [{ - type: 'text' as const, - text: JSON.stringify({ - success: false, - isError: true, - error: errorMessage - }) - }], - isError: true - }; - } - } - ); - - // -------------------------------------------------------------- - // TOOL: Take Screenshot (with viewport metadata) - // -------------------------------------------------------------- - server.tool( - 'browser_screenshot', - '[Roopik IDE] Capture a screenshot of the browser. Returns base64-encoded image with viewport metadata (width, height, devicePixelRatio) for pixel-perfect clicking with browser_action_input.', - {}, - async () => { - try { - const browserViewId = browserViewService.getActiveBrowserViewId(); - if (browserViewId === undefined) { - return { - content: [{ - type: 'text' as const, - text: JSON.stringify({ - success: false, - isError: true, - error: 'No browser is open. Start a project first with project_start.' - }) - }], - isError: true - }; - } - - const result = await browserViewService.takeScreenshotWithMetadata(browserViewId); - - return { - content: [{ - type: 'text' as const, - text: JSON.stringify({ - success: true, - image: result.image, - format: 'data-url', - viewport: { - width: result.width, - height: result.height, - devicePixelRatio: result.devicePixelRatio - } - }) - }] - }; - } catch (error) { - const errorMessage = error instanceof Error ? error.message : 'Unknown error'; - return { - content: [{ - type: 'text' as const, - text: JSON.stringify({ - success: false, - isError: true, - error: errorMessage - }) - }], - isError: true - }; - } - } - ); - - // -------------------------------------------------------------- - // TOOL: Close Browser - // -------------------------------------------------------------- - server.tool( - 'browser_close', - '[Roopik IDE] Close the browser view. Use this when done with browser testing or to free resources.', - {}, - async () => { - try { - const browserViewId = browserViewService.getActiveBrowserViewId(); - if (browserViewId === undefined) { - return { - content: [{ - type: 'text' as const, - text: JSON.stringify({ - success: true, - message: 'No browser is open' - }) - }] - }; - } - - // Fire event for renderer to close the editor tab properly - // This triggers the full cleanup chain: EditorTabInput.dispose -> destroyBrowserNow - // which stops dev server, destroys browser view, and cleans up all state - // DO NOT call destroyBrowserView directly - it leaves zombie editor tabs! - browserViewService.requestBrowserClose(); - - return { - content: [{ - type: 'text' as const, - text: JSON.stringify({ - success: true, - message: 'Browser close request sent. The browser will close shortly.' - }) - }] - }; - } catch (error) { - const errorMessage = error instanceof Error ? error.message : 'Unknown error'; - return { - content: [{ - type: 'text' as const, - text: JSON.stringify({ - success: false, - isError: true, - error: errorMessage - }) - }], - isError: true - }; - } - } - ); - - // -------------------------------------------------------------- - // TOOL: Browser Action (click, type, press, scroll, hover, drag) - // -------------------------------------------------------------- - server.tool( - 'browser_action_input', - `[Roopik IDE] Perform native input events in the browser. Supports click, right_click, double_click, hover, drag, type, press, scroll. - -TIP: To get element coordinates for clicking, use browser_execute_script first: -\`(function(){ var r = document.querySelector('button.submit').getBoundingClientRect(); return JSON.stringify({x:Math.round(r.x+r.width/2), y:Math.round(r.y+r.height/2)}); })()\` - -Coordinate format: 'x,y@WIDTHxHEIGHT' where WIDTH/HEIGHT are from browser_screenshot viewport. -Example: '450,203@900x600' means click at (450,203) on a 900x600 viewport. - -Actions: -- click/right_click/double_click/hover: requires 'coordinate' -- drag: requires 'coordinate' (start) + 'deltaX'/'deltaY' (offset to end) -- type: requires 'text' (types into focused element) -- press: requires 'key' (e.g., 'Enter', 'Escape', 'Tab'), optional 'modifiers' (['ctrl', 'shift']) -- scroll: requires 'deltaX' and/or 'deltaY' (negative = up/left)`, - { - action: z.enum(['click', 'right_click', 'double_click', 'hover', 'drag', 'type', 'press', 'scroll']) - .describe('The action to perform'), - coordinate: z.string().optional() - .describe("Coordinate string: 'x,y' or 'x,y@WIDTHxHEIGHT' for scaled coordinates"), - text: z.string().optional() - .describe("Text to type (for 'type' action)"), - key: z.string().optional() - .describe("Key to press (for 'press' action): Enter, Escape, Tab, ArrowDown, etc."), - modifiers: z.array(z.string()).optional() - .describe("Modifier keys (for 'press' action): ['ctrl', 'shift', 'alt', 'meta']"), - deltaX: z.number().optional() - .describe("Horizontal offset for drag/scroll (negative = left)"), - deltaY: z.number().optional() - .describe("Vertical offset for drag/scroll (negative = up)") - }, - async (args: { - action: string; - coordinate?: string; - text?: string; - key?: string; - modifiers?: string[]; - deltaX?: number; - deltaY?: number; - }) => { - try { - const browserViewId = browserViewService.getActiveBrowserViewId(); - if (browserViewId === undefined) { - return { - content: [{ - type: 'text' as const, - text: JSON.stringify({ - success: false, - isError: true, - error: 'No browser is open. Start a project first with project_start.' - }) - }], - isError: true - }; - } - - const { action, coordinate, text, key, modifiers, deltaX, deltaY } = args; - - // Parse coordinate string: 'x,y@WIDTHxHEIGHT' - let x = 0, y = 0, refWidth: number | undefined, refHeight: number | undefined; - - if (coordinate) { - const match = coordinate.match(/^(\d+),(\d+)(?:@(\d+)x(\d+))?$/); - if (!match) { - return { - content: [{ - type: 'text' as const, - text: JSON.stringify({ - success: false, - isError: true, - error: `Invalid coordinate format: '${coordinate}'. Expected 'x,y' or 'x,y@WIDTHxHEIGHT'` - }) - }], - isError: true - }; - } - x = parseInt(match[1], 10); - y = parseInt(match[2], 10); - if (match[3] && match[4]) { - refWidth = parseInt(match[3], 10); - refHeight = parseInt(match[4], 10); - } - } - - let resultData: Record = { action }; - - switch (action) { - case 'click': - case 'right_click': - case 'double_click': - case 'hover': - if (!coordinate) { - return { - content: [{ - type: 'text' as const, - text: JSON.stringify({ - success: false, - isError: true, - error: `'${action}' requires 'coordinate' parameter` - }) - }], - isError: true - }; - } - await browserViewService.sendMouseEvent( - browserViewId, - action as 'click' | 'right_click' | 'double_click' | 'hover', - x, y, refWidth, refHeight - ); - resultData = { action, coordinate, x, y, message: `${action} at (${x}, ${y})` }; - break; - - case 'drag': - if (!coordinate || deltaX === undefined || deltaY === undefined) { - return { - content: [{ - type: 'text' as const, - text: JSON.stringify({ - success: false, - isError: true, - error: "'drag' requires 'coordinate' (start) and 'deltaX'/'deltaY' (offset)" - }) - }], - isError: true - }; - } - await browserViewService.sendDragEvent( - browserViewId, - x, y, x + deltaX, y + deltaY, - refWidth, refHeight - ); - resultData = { action, from: { x, y }, to: { x: x + deltaX, y: y + deltaY }, message: 'Drag completed' }; - break; - - case 'type': - if (!text) { - return { - content: [{ - type: 'text' as const, - text: JSON.stringify({ - success: false, - isError: true, - error: "'type' requires 'text' parameter" - }) - }], - isError: true - }; - } - await browserViewService.sendTypeEvent(browserViewId, text); - resultData = { action, textLength: text.length, message: `Typed ${text.length} characters` }; - break; - - case 'press': - if (!key) { - return { - content: [{ - type: 'text' as const, - text: JSON.stringify({ - success: false, - isError: true, - error: "'press' requires 'key' parameter" - }) - }], - isError: true - }; - } - await browserViewService.sendKeyEvent(browserViewId, key, modifiers); - resultData = { action, key, modifiers, message: `Pressed ${key}` }; - break; - - case 'scroll': - if (deltaX === undefined && deltaY === undefined) { - return { - content: [{ - type: 'text' as const, - text: JSON.stringify({ - success: false, - isError: true, - error: "'scroll' requires 'deltaX' and/or 'deltaY' parameters" - }) - }], - isError: true - }; - } - await browserViewService.sendScrollEvent( - browserViewId, - deltaX ?? 0, - deltaY ?? 0, - coordinate ? x : undefined, - coordinate ? y : undefined - ); - resultData = { action, deltaX: deltaX ?? 0, deltaY: deltaY ?? 0, message: 'Scrolled' }; - break; - - default: - return { - content: [{ - type: 'text' as const, - text: JSON.stringify({ - success: false, - isError: true, - error: `Unknown action: '${action}'` - }) - }], - isError: true - }; - } - - return { - content: [{ - type: 'text' as const, - text: JSON.stringify({ - success: true, - ...resultData - }) - }] - }; - } catch (error) { - const errorMessage = error instanceof Error ? error.message : 'Unknown error'; - return { - content: [{ - type: 'text' as const, - text: JSON.stringify({ - success: false, - isError: true, - error: errorMessage - }) - }], - isError: true - }; - } - } - ); - - // -------------------------------------------------------------- - // TOOL: Navigate - // -------------------------------------------------------------- - server.tool( - 'browser_navigate', - '[Roopik IDE] Navigate the browser to a URL. Use this to load specific pages in the project (e.g., /login, /dashboard) or external URLs.', - { - url: z.string().describe('The URL to navigate to (e.g., http://localhost:3000/login)') - }, - async ({ url }: { url: string }) => { - try { - const browserViewId = browserViewService.getActiveBrowserViewId(); - if (browserViewId === undefined) { - return { - content: [{ - type: 'text' as const, - text: JSON.stringify({ - success: false, - isError: true, - error: 'No browser is open. Start a project first with project_start.' - }) - }], - isError: true - }; - } - - // URL normalization is done in browserViewService.navigate() - await browserViewService.navigate(browserViewId, url); - - // Enable CDP monitoring proactively to capture errors from page load - await ensureCDPMonitoring(browserViewId, browserViewService); - - return { - content: [{ - type: 'text' as const, - text: JSON.stringify({ - success: true, - url, - message: `Navigated to ${url}` - }) - }] - }; - } catch (error) { - const errorMessage = error instanceof Error ? error.message : 'Unknown error'; - return { - content: [{ - type: 'text' as const, - text: JSON.stringify({ - success: false, - isError: true, - error: errorMessage, - url - }) - }], - isError: true - }; - } - } - ); - - // -------------------------------------------------------------- - // TOOL: Reload Page - // -------------------------------------------------------------- - server.tool( - 'browser_reload', - '[Roopik IDE] Reload the current page in the browser. Use ignoreCache=true for hard reload after changing assets.', - { - ignoreCache: z.boolean().optional().describe('If true, clears cache before reloading (hard reload)') - }, - async ({ ignoreCache }: { ignoreCache?: boolean }) => { - try { - const browserViewId = browserViewService.getActiveBrowserViewId(); - if (browserViewId === undefined) { - return { - content: [{ - type: 'text' as const, - text: JSON.stringify({ - success: false, - isError: true, - error: 'No browser is open. Start a project first with project_start.' - }) - }], - isError: true - }; - } - - await browserViewService.reload(browserViewId, ignoreCache); - - // Enable CDP monitoring proactively to capture errors from reload - await ensureCDPMonitoring(browserViewId, browserViewService); - - return { - content: [{ - type: 'text' as const, - text: JSON.stringify({ - success: true, - hardReload: ignoreCache || false, - message: ignoreCache ? 'Page hard-reloaded (cache cleared)' : 'Page reloaded' - }) - }] - }; - } catch (error) { - const errorMessage = error instanceof Error ? error.message : 'Unknown error'; - return { - content: [{ - type: 'text' as const, - text: JSON.stringify({ - success: false, - isError: true, - error: errorMessage - }) - }], - isError: true - }; - } - } - ); - - // -------------------------------------------------------------- - // TOOL: Execute JavaScript - // -------------------------------------------------------------- - server.tool( - 'browser_execute_script', - `[Roopik IDE] Execute JavaScript in the browser context. Returns the result of the last expression. - -IMPORTANT: For multi-statement scripts, use IIFE pattern with explicit return: -\`\`\`javascript -(function() { - var el = document.querySelector('.my-element'); - if (!el) return JSON.stringify({ error: 'Not found' }); - var rect = el.getBoundingClientRect(); - return JSON.stringify({ x: rect.x, y: rect.y, width: rect.width, height: rect.height }); -})() -\`\`\` - -Common patterns: -- Get element bounds: \`(function(){ var r = document.querySelector('SEL').getBoundingClientRect(); return JSON.stringify({x:r.x,y:r.y,w:r.width,h:r.height}); })()\` -- Click element: \`document.querySelector('SEL').click()\` -- Type in input: \`(function(){ var i=document.querySelector('SEL'); i.value='text'; i.dispatchEvent(new Event('input',{bubbles:true})); return true; })()\` -- Get page info: \`JSON.stringify({url:location.href,title:document.title})\` - -Single expressions work directly: \`document.title\`, \`document.querySelectorAll('a').length\``, - { - script: z.string().describe('JavaScript code to execute. Use IIFE pattern for multi-statement scripts.') - }, - async ({ script }: { script: string }) => { - try { - const browserViewId = browserViewService.getActiveBrowserViewId(); - if (browserViewId === undefined) { - return { - content: [{ - type: 'text' as const, - text: JSON.stringify({ - success: false, - isError: true, - error: 'No browser is open. Start a project first with project_start.' - }) - }], - isError: true - }; - } - - const result = await browserViewService.executeScript(browserViewId, script); - - return { - content: [{ - type: 'text' as const, - text: JSON.stringify({ - success: true, - result - }) - }] - }; - } catch (error) { - const errorMessage = error instanceof Error ? error.message : 'Unknown error'; - return { - content: [{ - type: 'text' as const, - text: JSON.stringify({ - success: false, - isError: true, - error: errorMessage - }) - }], - isError: true - }; - } - } - ); - - // -------------------------------------------------------------- - // TOOL: Inspect Element Styles - // -------------------------------------------------------------- - server.tool( - 'browser_inspect_element', - `[Roopik IDE] Deep CSS inspection for an element. Returns matched CSS rules with source file locations (file:line:column), computed styles, and specificity. - -Best for LOCAL projects with source maps - returns exact source file locations for CSS edits. -For external sites without source maps, source locations will be empty but you can still get: -- Computed styles (what the browser actually applies) -- Matched selectors (what CSS rules match) -- Use browser_execute_script for computed styles: \`getComputedStyle(document.querySelector('SEL')).color\``, - { - selector: z.string().describe('CSS selector to find element (e.g., ".btn-primary", "#header")'), - includeInherited: z.boolean().optional().describe('Include inherited styles from parents (default: true)') - }, - async ({ selector, includeInherited }: { selector: string; includeInherited?: boolean }) => { - try { - const browserViewId = browserViewService.getActiveBrowserViewId(); - if (browserViewId === undefined) { - return { - content: [{ - type: 'text' as const, - text: JSON.stringify({ - success: false, - isError: true, - error: 'No browser is open. Start a project first with project_start.' - }) - }], - isError: true - }; - } - - const workspacePath = storageService.getWorkspacePath(); - - const result = await browserViewService.getElementStyles({ - browserViewId, - target: selector, - projectRoot: workspacePath, - includeUserAgent: false, - includeInherited: includeInherited ?? true - }); - - if (!result.success || !result.data) { - return { - content: [{ - type: 'text' as const, - text: JSON.stringify({ - success: false, - isError: true, - error: result.error || 'Element not found', - selector - }) - }], - isError: true - }; - } - - const data = result.data; - - return { - content: [{ - type: 'text' as const, - text: JSON.stringify({ - success: true, - selector, - element: { - tag: data.tagName, - classes: data.classes, - componentName: data.componentName, - componentSource: data.htmlSource - }, - matchedRules: data.matchedRules?.map((rule: import('../../../common/cssResolvers/types.js').MatchedCSSRule) => ({ - selector: rule.selector, - file: rule.file, - location: rule.location, - properties: rule.properties, - specificity: rule.specificity, - origin: rule.origin - })), - inlineStyles: data.inlineStyles, - inheritedStyles: data.inheritedStyles, - properties: data.properties, - cssInJs: data.cssInJs - }) - }] - }; - } catch (error) { - const errorMessage = error instanceof Error ? error.message : 'Unknown error'; - return { - content: [{ - type: 'text' as const, - text: JSON.stringify({ - success: false, - isError: true, - error: errorMessage, - selector - }) - }], - isError: true - }; - } - } - ); - - // ============================================================================ - // CDP TOOLS (2) - Auto-attaches debugger when first used - // ============================================================================ - - // -------------------------------------------------------------- - // TOOL: Get Errors (Unified - Console + Network) - // -------------------------------------------------------------- - server.tool( - 'browser_get_errors', - '[Roopik IDE] Get all errors from the browser: console errors (JavaScript exceptions, console.error) AND failed network requests (4xx, 5xx, network failures). This is the primary debugging tool - shows what is broken.', - { - limit: z.number().optional().describe('Maximum errors to return (default: 50)') - }, - async ({ limit }: { limit?: number }) => { - try { - const browserViewId = browserViewService.getActiveBrowserViewId(); - await ensureCDPMonitoring(browserViewId!, browserViewService); - - const monitor = getMonitorOrThrow(browserViewId); - const maxErrors = Math.min(limit || 50, 200); - - // Get console errors (error type only) - const consoleErrors = monitor.consoleLogs - .filter(log => log.type === 'error') - .slice(-maxErrors) - .map(log => ({ - source: 'console', - type: 'error', - message: log.message, - location: log.location, - timestamp: log.timestamp - })); - - // Get failed network requests (4xx, 5xx, or status 0) - const networkErrors = monitor.networkResponses - .filter(res => res.status >= 400 || res.status === 0) - .slice(-maxErrors) - .map(res => { - const request = monitor.networkRequests.find(req => req.requestId === res.requestId); - return { - source: 'network', - type: res.status === 0 ? 'network_failure' : `http_${res.status}`, - message: res.status === 0 ? res.statusText : `${res.status} ${res.statusText}`, - url: res.url || request?.url, - method: request?.method, - timestamp: res.timestamp - }; - }); - - // Combine and sort by timestamp (most recent first) - const allErrors = [...consoleErrors, ...networkErrors] - .sort((a, b) => b.timestamp - a.timestamp) - .slice(0, maxErrors); - - return { - content: [{ - type: 'text' as const, - text: JSON.stringify({ - success: true, - errorCount: allErrors.length, - consoleErrorCount: consoleErrors.length, - networkErrorCount: networkErrors.length, - errors: allErrors - }, null, 2) - }] - }; - } catch (error) { - const errorMessage = error instanceof Error ? error.message : 'Unknown error'; - return { - content: [{ - type: 'text' as const, - text: JSON.stringify({ - success: false, - isError: true, - error: errorMessage - }) - }], - isError: true - }; - } - } - ); - - // -------------------------------------------------------------- - // TOOL: Get Console Logs (All types) - // -------------------------------------------------------------- - server.tool( - 'browser_get_console_logs', - '[Roopik IDE] Get console output from the browser (console.log, warn, error, etc.). Use type filter to focus on specific log types. For errors only, prefer browser_get_errors.', - { - limit: z.number().optional().describe('Maximum logs to return (default: 50)'), - type: z.enum(['log', 'debug', 'info', 'warn', 'error']).optional().describe('Filter by log type') - }, - async ({ limit, type }: { limit?: number; type?: ConsoleLog['type'] }) => { - try { - const browserViewId = browserViewService.getActiveBrowserViewId(); - await ensureCDPMonitoring(browserViewId!, browserViewService); - - const monitor = getMonitorOrThrow(browserViewId); - - let logs = monitor.consoleLogs; - if (type) { - logs = logs.filter(log => log.type === type); - } - - const maxLogs = Math.min(limit || 50, 500); - const result = logs.slice(-maxLogs).reverse(); - - return { - content: [{ - type: 'text' as const, - text: JSON.stringify({ - success: true, - count: result.length, - total: logs.length, - logs: result.map(log => ({ - type: log.type, - message: log.message, - location: log.location, - timestamp: log.timestamp - })) - }, null, 2) - }] - }; - } catch (error) { - const errorMessage = error instanceof Error ? error.message : 'Unknown error'; - return { - content: [{ - type: 'text' as const, - text: JSON.stringify({ - success: false, - isError: true, - error: errorMessage - }) - }], - isError: true - }; - } - } - ); - - // -------------------------------------------------------------- - // TOOL: Get Performance Metrics - // -------------------------------------------------------------- - server.tool( - 'browser_get_performance', - '[Roopik IDE] Get performance metrics from the browser including Web Vitals (LCP, CLS) and runtime metrics (JS heap, DOM nodes, layout count). Uses Chrome DevTools Protocol for accurate measurements.', - {}, - async () => { - try { - const browserViewId = browserViewService.getActiveBrowserViewId(); - if (browserViewId === undefined) { - return { - content: [{ - type: 'text' as const, - text: JSON.stringify({ - success: false, - isError: true, - error: 'No browser is open. Start a project first with project_start.' - }) - }], - isError: true - }; - } - - // Ensure CDP is attached - await browserViewService.attachDebugger(browserViewId); - - // Get performance metrics via CDP - const metricsResult = await browserViewService.sendCDPCommand(browserViewId, 'Performance.getMetrics') as { metrics: Array<{ name: string; value: number }> }; - - // Get Web Vitals via script injection - const webVitalsScript = ` - (function() { - const result = {}; - - // LCP (Largest Contentful Paint) - if (window.PerformanceObserver) { - const lcpEntries = performance.getEntriesByType('largest-contentful-paint'); - if (lcpEntries.length > 0) { - result.lcp = lcpEntries[lcpEntries.length - 1].startTime; - } - } - - // CLS (Cumulative Layout Shift) - const layoutShiftEntries = performance.getEntriesByType('layout-shift'); - if (layoutShiftEntries.length > 0) { - result.cls = layoutShiftEntries.reduce((sum, entry) => sum + (entry.hadRecentInput ? 0 : entry.value), 0); - } - - // FCP (First Contentful Paint) - const fcpEntries = performance.getEntriesByType('paint').filter(e => e.name === 'first-contentful-paint'); - if (fcpEntries.length > 0) { - result.fcp = fcpEntries[0].startTime; - } - - // Navigation timing - const nav = performance.getEntriesByType('navigation')[0]; - if (nav) { - result.ttfb = nav.responseStart; - result.domContentLoaded = nav.domContentLoadedEventEnd; - result.load = nav.loadEventEnd; - } - - return result; - })() - `; - - const webVitals = await browserViewService.executeScript(browserViewId, webVitalsScript); - - // Format metrics - const metrics: Record = {}; - if (metricsResult?.metrics) { - for (const metric of metricsResult.metrics) { - metrics[metric.name] = metric.value; - } - } - - return { - content: [{ - type: 'text' as const, - text: JSON.stringify({ - success: true, - webVitals: webVitals || {}, - runtimeMetrics: { - jsHeapUsedSize: metrics['JSHeapUsedSize'], - jsHeapTotalSize: metrics['JSHeapTotalSize'], - domNodes: metrics['Nodes'], - layoutCount: metrics['LayoutCount'], - recalcStyleCount: metrics['RecalcStyleCount'], - layoutDuration: metrics['LayoutDuration'], - recalcStyleDuration: metrics['RecalcStyleDuration'], - scriptDuration: metrics['ScriptDuration'], - taskDuration: metrics['TaskDuration'] - } - }, null, 2) - }] - }; - } catch (error) { - const errorMessage = error instanceof Error ? error.message : 'Unknown error'; - return { - content: [{ - type: 'text' as const, - text: JSON.stringify({ - success: false, - isError: true, - error: errorMessage - }) - }], - isError: true - }; - } - } - ); - - // -------------------------------------------------------------- - // TOOL: Set Viewport Size - // -------------------------------------------------------------- - server.tool( - 'browser_set_viewport', - `[Roopik IDE] Set or clear browser viewport override for responsive design testing. Uses CDP Emulation. - -Provide width/height to set a specific size (e.g., mobile 375x812). -Call with NO parameters to clear override and restore natural browser size. - -Common viewport sizes: -- Mobile: 375x667 (iPhone SE), 390x844 (iPhone 12/13/14) -- Tablet: 768x1024 (iPad), 820x1180 (iPad Air) -- Desktop: 1280x720, 1920x1080`, - { - width: z.number().optional().describe('Viewport width in pixels. Omit to clear override.'), - height: z.number().optional().describe('Viewport height in pixels. Omit to clear override.'), - deviceScaleFactor: z.number().optional().describe('Device scale factor (default: 1)'), - mobile: z.boolean().optional().describe('Emulate mobile device (default: false)') - }, - async ({ width, height, deviceScaleFactor, mobile }: { width?: number; height?: number; deviceScaleFactor?: number; mobile?: boolean }) => { - try { - const browserViewId = browserViewService.getActiveBrowserViewId(); - if (browserViewId === undefined) { - return { - content: [{ - type: 'text' as const, - text: JSON.stringify({ - success: false, - isError: true, - error: 'No browser is open. Start a project first with project_start.' - }) - }], - isError: true - }; - } - - // Ensure CDP is attached - await browserViewService.attachDebugger(browserViewId); - - // If width/height not provided, clear the viewport override - if (width === undefined || height === undefined) { - await browserViewService.sendCDPCommand(browserViewId, 'Emulation.clearDeviceMetricsOverride'); - return { - content: [{ - type: 'text' as const, - text: JSON.stringify({ - success: true, - message: 'Viewport override cleared - restored to natural browser size' - }) - }] - }; - } - - // Set viewport using CDP Emulation - await browserViewService.sendCDPCommand(browserViewId, 'Emulation.setDeviceMetricsOverride', { - width, - height, - deviceScaleFactor: deviceScaleFactor ?? 1, - mobile: mobile ?? false - }); - - return { - content: [{ - type: 'text' as const, - text: JSON.stringify({ - success: true, - viewport: { - width, - height, - deviceScaleFactor: deviceScaleFactor ?? 1, - mobile: mobile ?? false - }, - message: `Viewport set to ${width}x${height}${mobile ? ' (mobile)' : ''}` - }) - }] - }; - } catch (error) { - const errorMessage = error instanceof Error ? error.message : 'Unknown error'; - return { - content: [{ - type: 'text' as const, - text: JSON.stringify({ - success: false, - isError: true, - error: errorMessage - }) - }], - isError: true - }; - } - } - ); - - // -------------------------------------------------------------- - // TOOL: Get Network Requests - // -------------------------------------------------------------- - server.tool( - 'browser_get_network_requests', - `[Roopik IDE] Get captured network requests and responses. Requires CDP monitoring. - -Useful for: -- Debugging API calls (see request/response details) -- Finding failed requests (status 4xx, 5xx) -- Checking what resources are being loaded -- Verifying API endpoints are correct - -For errors only, use browser_get_errors which includes failed network requests.`, - { - urlFilter: z.string().optional().describe('Filter requests by URL substring'), - method: z.string().optional().describe('Filter by HTTP method (GET, POST, etc.)'), - statusFilter: z.enum(['success', 'error', 'all']).optional() - .describe('Filter by status: success (2xx-3xx), error (4xx-5xx or failed), all'), - limit: z.number().optional().describe('Maximum number of requests to return (default: 100, max: 500)') - }, - async ({ urlFilter, method, statusFilter, limit }: { urlFilter?: string; method?: string; statusFilter?: 'success' | 'error' | 'all'; limit?: number }) => { - try { - const browserViewId = browserViewService.getActiveBrowserViewId(); - await ensureCDPMonitoring(browserViewId!, browserViewService); - - const monitor = getMonitorOrThrow(browserViewId); - const maxItems = Math.min(limit || 100, 500); - - // Build request/response pairs - let results = monitor.networkRequests.map(req => { - const response = monitor.networkResponses.find(res => res.requestId === req.requestId); - return { - method: req.method, - url: req.url, - status: response?.status, - statusText: response?.statusText, - mimeType: response?.mimeType, - duration: response?.duration, - timestamp: req.timestamp, - hasResponse: !!response - }; - }); - - // Apply URL filter - if (urlFilter) { - results = results.filter(r => r.url.includes(urlFilter)); - } - - // Apply method filter - if (method) { - results = results.filter(r => r.method.toUpperCase() === method.toUpperCase()); - } - - // Apply status filter - if (statusFilter === 'success') { - results = results.filter(r => r.status !== undefined && r.status >= 200 && r.status < 400); - } else if (statusFilter === 'error') { - results = results.filter(r => r.status === undefined || r.status >= 400 || r.status === 0); - } - // 'all' or undefined = no filter - - // Get most recent - results = results.slice(-maxItems).reverse(); - - return { - content: [{ - type: 'text' as const, - text: JSON.stringify({ - success: true, - count: results.length, - totalTracked: monitor.networkRequests.length, - filters: { urlFilter, method, statusFilter: statusFilter || 'all' }, - requests: results - }, null, 2) - }] - }; - } catch (error) { - const errorMessage = error instanceof Error ? error.message : 'Unknown error'; - return { - content: [{ - type: 'text' as const, - text: JSON.stringify({ - success: false, - isError: true, - error: errorMessage - }) - }], - isError: true - }; - } - } - ); - - // -------------------------------------------------------------- - // TOOL: Get Browser State - // -------------------------------------------------------------- - server.tool( - 'browser_get_state', - '[Roopik IDE] Get browser state information (open/closed, current URL, title).', - {}, - async () => { - try { - const browserViewId = browserViewService.getActiveBrowserViewId(); - - // Get current URL and title if browser is open - let currentUrl: string | undefined; - let title: string | undefined; - let devServerRunning = false; - - if (browserViewId !== undefined) { - currentUrl = await browserViewService.executeScript(browserViewId, 'window.location.href') as string; - title = await browserViewService.executeScript(browserViewId, 'document.title') as string; - devServerRunning = true; - } - - return { - content: [{ - type: 'text' as const, - text: JSON.stringify({ - success: true, - browserOpen: browserViewId !== undefined, - browserViewId, - currentUrl, - title, - devServerRunning, - message: browserViewId !== undefined - ? `Browser is open at ${currentUrl}` - : 'No browser is currently open. Use browser_open or project_start to open one.' - }, null, 2) - }] - }; - } catch (error) { - const errorMessage = error instanceof Error ? error.message : 'Unknown error'; - return { - content: [{ - type: 'text' as const, - text: JSON.stringify({ - success: false, - isError: true, - error: errorMessage - }) - }], - isError: true - }; - } - } - ); -} diff --git a/src/vs/workbench/contrib/roopik/electron-main/mcp/tools/canvasTools.ts b/src/vs/workbench/contrib/roopik/electron-main/mcp/tools/canvasTools.ts deleted file mode 100644 index 5526938bc..000000000 --- a/src/vs/workbench/contrib/roopik/electron-main/mcp/tools/canvasTools.ts +++ /dev/null @@ -1,595 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Roopik. All rights reserved. - * Licensed under the MIT License. - *--------------------------------------------------------------------------------------------*/ - -/** - * Canvas & Component Tools - * - * MCP tools for Canvas and Component operations in Canvas Mode. - * - Canvas tools: list, get active, create - * - Component tools: add, batch add, remove, get info, list, rebuild - * - * These tools let AI agents manage canvases and components for live preview. - */ - -import { resolve, normalize } from '../../../../../../base/common/path.js'; -import type { ICanvasService } from '../../../common/canvas/canvasService.js'; -import type { ComponentService } from '../../component/componentService.js'; - -/** - * Resolve a path that may be relative or absolute. - * Handles various path formats from AI agents: - * - Absolute: C:\project\src, /home/user/project - * - Relative: src/Button, ./src/Button, ../other - * - Mixed separators: src\Button, ./src\Button - * - * Cross-platform: Works on Windows, Linux, and macOS. - * On non-Windows platforms, backslashes are converted to forward slashes - * since backslash is a valid filename character on Unix systems. - * - * @param inputPath - Path from AI agent (may be relative or absolute) - * @param workspacePath - The workspace root path - * @returns Absolute path - */ -function resolvePath(inputPath: string, workspacePath: string): string { - // On non-Windows platforms, convert backslashes to forward slashes - // because backslash is a valid filename character on Unix systems - // but AI agents often send Windows-style paths regardless of platform - let sanitizedInput = inputPath; - if (process.platform !== 'win32') { - sanitizedInput = inputPath.replace(/\\/g, '/'); - } - - // Normalize to handle ./.. segments and platform-specific separators - const normalizedInput = normalize(sanitizedInput); - - // resolve handles both cases: - // - If normalizedInput is absolute, returns normalizedInput - // - If normalizedInput is relative, resolves it against workspacePath - const result = resolve(workspacePath, normalizedInput); - - // Debug log to help troubleshoot path resolution issues - console.log(`[MCP] Path resolution: "${inputPath}" -> "${result}" (workspace: "${workspacePath}", platform: ${process.platform})`); - - return result; -} - -/** - * Register all canvas and component MCP tools - * - * @param server - McpServer instance (dynamically imported) - * @param z - Zod validation library (dynamically imported) - * @param canvasService - Canvas service instance - * @param componentService - Component service instance - */ -export function registerCanvasTools( - // eslint-disable-next-line @typescript-eslint/no-explicit-any - server: any, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - z: any, - canvasService: ICanvasService, - componentService: ComponentService -): void { - - // ============================================================================ - // CANVAS TOOLS (3) - // ============================================================================ - - // -------------------------------------------------------------- - // TOOL: List All Canvases - // -------------------------------------------------------------- - server.tool( - 'canvas_list', - '[Roopik IDE] List all Canvases in the workspace. Canvases are visual workspaces in the IDE where components are displayed for live preview. Returns canvas names, component counts, and timestamps.', - { - nameFilter: z.string().optional().describe('Optional filter to search canvas names'), - sortBy: z.enum(['name', 'updatedAt', 'createdAt', 'componentCount']).optional().describe('Sort field (default: updatedAt)'), - sortDirection: z.enum(['asc', 'desc']).optional().describe('Sort direction (default: desc)') - }, - async ({ nameFilter, sortBy, sortDirection }: { nameFilter?: string; sortBy?: 'name' | 'updatedAt' | 'createdAt' | 'componentCount'; sortDirection?: 'asc' | 'desc' }) => { - try { - const canvases = await canvasService.listCanvasesAsync({ - nameFilter, - sortBy, - sortDirection - }); - - return { - content: [{ - type: 'text' as const, - text: JSON.stringify({ - success: true, - canvases: canvases.map(c => ({ - id: c.id, - name: c.name, - componentCount: c.componentCount, - description: c.description, - icon: c.icon, - color: c.color, - createdAt: c.createdAt, - updatedAt: c.updatedAt - })), - count: canvases.length - }) - }] - }; - } catch (error) { - const errorMessage = error instanceof Error ? error.message : 'Unknown error'; - return { - content: [{ - type: 'text' as const, - text: JSON.stringify({ - success: false, - isError: true, - error: errorMessage - }) - }], - isError: true - }; - } - } - ); - - // -------------------------------------------------------------- - // TOOL: Get Active Canvas - // -------------------------------------------------------------- - server.tool( - 'canvas_get_active', - '[Roopik IDE] Get the Canvas currently open/focused in the IDE. Use this to know which Canvas the user is viewing, so you can add components to it. Returns null if no Canvas is open.', - {}, - async () => { - try { - const focusedCanvasId = await canvasService.getFocusedCanvasIdAsync(); - - if (!focusedCanvasId) { - return { - content: [{ - type: 'text' as const, - text: JSON.stringify({ - success: true, - activeCanvas: null, - message: 'No canvas is currently focused' - }) - }] - }; - } - - const canvas = await canvasService.getCanvasAsync(focusedCanvasId); - if (!canvas) { - return { - content: [{ - type: 'text' as const, - text: JSON.stringify({ - success: true, - activeCanvas: null, - message: 'Focused canvas not found' - }) - }] - }; - } - - return { - content: [{ - type: 'text' as const, - text: JSON.stringify({ - success: true, - activeCanvas: { - id: canvas.id, - name: canvas.name, - componentCount: canvas.componentCount, - description: canvas.description, - icon: canvas.icon, - color: canvas.color, - createdAt: canvas.createdAt, - updatedAt: canvas.updatedAt, - isOpen: canvas.isOpen, - isFocused: canvas.isFocused - } - }) - }] - }; - } catch (error) { - const errorMessage = error instanceof Error ? error.message : 'Unknown error'; - return { - content: [{ - type: 'text' as const, - text: JSON.stringify({ - success: false, - isError: true, - error: errorMessage - }) - }], - isError: true - }; - } - } - ); - - // -------------------------------------------------------------- - // TOOL: Create Canvas - // -------------------------------------------------------------- - server.tool( - 'canvas_create', - '[Roopik IDE] Create a new Canvas in the IDE for organizing and previewing components. If a Canvas with the same name exists, returns the existing one. Canvases appear as tabs in the IDE where components are visually rendered.', - { - name: z.string().describe('Canvas display name (e.g., "Login Components")') - }, - async ({ name }: { name: string }) => { - try { - const result = await canvasService.createCanvas(name); - - return { - content: [{ - type: 'text' as const, - text: JSON.stringify({ - success: true, - canvasId: result.canvasId, - isNew: result.isNew, - canvas: { - id: result.canvas.id, - name: result.canvas.name, - componentCount: result.canvas.componentCount, - description: result.canvas.description, - icon: result.canvas.icon, - color: result.canvas.color, - createdAt: result.canvas.createdAt, - updatedAt: result.canvas.updatedAt - }, - message: result.isNew ? 'Canvas created successfully' : 'Canvas already exists with this name' - }) - }] - }; - } catch (error) { - const errorMessage = error instanceof Error ? error.message : 'Unknown error'; - return { - content: [{ - type: 'text' as const, - text: JSON.stringify({ - success: false, - isError: true, - error: errorMessage - }) - }], - isError: true - }; - } - } - ); - - // ============================================================================ - // COMPONENT TOOLS (6) - // ============================================================================ - - // -------------------------------------------------------------- - // TOOL: Add Component (Import from local folder) - // -------------------------------------------------------------- - server.tool( - 'component_add', - '[Roopik IDE] Add a component to the visual Canvas for live preview. The component will be bundled and displayed in the IDE canvas where users can see it rendered. Auto-detects entry file and framework from the folder. Supports both absolute and relative paths (relative to workspace).', - { - canvasId: z.string().optional().describe('Canvas ID to add component to (optional, uses active canvas if not provided)'), - folderPath: z.string().describe('Path to component folder or file. Can be absolute (e.g., C:\\project\\src\\Button) or relative to workspace (e.g., src/Button or ./src/Button)'), - name: z.string().optional().describe('Component name (optional, auto-detected from folder/file if not provided)'), - entryFile: z.string().optional().describe('Entry file name relative to folder (optional, auto-detected if not provided)'), - framework: z.enum(['react', 'vue', 'svelte', 'solid', 'preact', 'html']).optional().describe('Framework type (optional, auto-detected if not provided)') - }, - async ({ canvasId, folderPath, name, entryFile, framework }: { - canvasId?: string; - folderPath: string; - name?: string; - entryFile?: string; - framework?: 'react' | 'vue' | 'svelte' | 'solid' | 'preact' | 'html'; - }) => { - try { - // Resolve path (handles both absolute and relative paths) - const workspacePath = canvasService.getWorkspacePath(); - if (!workspacePath) { - return { - content: [{ - type: 'text' as const, - text: JSON.stringify({ - success: false, - isError: true, - error: 'No workspace is open. Please open a folder/workspace first.', - folderPath - }) - }], - isError: true - }; - } - const resolvedPath = resolvePath(folderPath, workspacePath); - - const component = await componentService.addComponent({ - folderPath: resolvedPath, - canvasId, - componentName: name, - entryFile, - framework, - origin: 'ai' - }); - - return { - content: [{ - type: 'text' as const, - text: JSON.stringify({ - success: true, - component: { - id: component.id, - canvasId: component.canvasId, - folderPath: component.folderPath, - entryFile: component.entryFile, - framework: component.framework, - buildState: component.buildState, - contentHash: component.contentHash, - componentName: component.componentName, - origin: component.origin, - createdAt: component.createdAt, - updatedAt: component.updatedAt - } - }) - }] - }; - } catch (error) { - const errorMessage = error instanceof Error ? error.message : 'Unknown error'; - return { - content: [{ - type: 'text' as const, - text: JSON.stringify({ - success: false, - isError: true, - error: errorMessage, - folderPath - }) - }], - isError: true - }; - } - } - ); - - // -------------------------------------------------------------- - // TOOL: Add Multiple Components (Batch) - // -------------------------------------------------------------- - server.tool( - 'component_add_batch', - '[Roopik IDE] Batch add multiple components to the visual Canvas. Efficient for adding component variants or multiple components at once. All components will be bundled and displayed in the IDE canvas for live preview. Supports both absolute and relative paths (relative to workspace).', - { - components: z.array(z.object({ - canvasId: z.string().optional().describe('Canvas ID (optional, uses active canvas)'), - folderPath: z.string().describe('Path to component folder or file. Can be absolute or relative to workspace'), - name: z.string().optional().describe('Component name (optional, auto-detected)'), - entryFile: z.string().optional().describe('Entry file (optional, auto-detected)'), - framework: z.enum(['react', 'vue', 'svelte', 'solid', 'preact', 'html']).optional().describe('Framework (optional, auto-detected)') - })).describe('Array of component definitions to add') - }, - async ({ components }: { - components: Array<{ - canvasId?: string; - folderPath: string; - name?: string; - entryFile?: string; - framework?: 'react' | 'vue' | 'svelte' | 'solid' | 'preact' | 'html'; - }>; - }) => { - try { - // Resolve all paths (handles both absolute and relative paths) - const workspacePath = canvasService.getWorkspacePath(); - const requests = components.map(c => ({ - folderPath: resolvePath(c.folderPath, workspacePath), - canvasId: c.canvasId, - componentName: c.name, - entryFile: c.entryFile, - framework: c.framework, - origin: 'ai' as const - })); - - const created = await componentService.addComponents(requests); - - return { - content: [{ - type: 'text' as const, - text: JSON.stringify({ - success: true, - count: created.length, - components: created.map(c => ({ - id: c.id, - canvasId: c.canvasId, - componentName: c.componentName, - folderPath: c.folderPath, - framework: c.framework, - buildState: c.buildState - })) - }) - }] - }; - } catch (error) { - const errorMessage = error instanceof Error ? error.message : 'Unknown error'; - return { - content: [{ - type: 'text' as const, - text: JSON.stringify({ - success: false, - isError: true, - error: errorMessage - }) - }], - isError: true - }; - } - } - ); - - // -------------------------------------------------------------- - // TOOL: Delete Component - // -------------------------------------------------------------- - server.tool( - 'component_remove', - '[Roopik IDE] Remove component from canvas. Set deleteSourceCode=true to automatically delete source files - do NOT manually delete files with terminal commands.', - { - componentId: z.string().describe('Component ID to delete'), - deleteSourceCode: z.boolean().optional().describe('Set to true to delete source code from disk (default: false). When true, both UI removal and file deletion are handled automatically.') - }, - async ({ componentId, deleteSourceCode }: { componentId: string; deleteSourceCode?: boolean }) => { - try { - await componentService.deleteComponent(componentId, deleteSourceCode); - - return { - content: [{ - type: 'text' as const, - text: JSON.stringify({ - success: true, - componentId, - deletedSourceCode: deleteSourceCode ?? false - }) - }] - }; - } catch (error) { - const errorMessage = error instanceof Error ? error.message : 'Unknown error'; - return { - content: [{ - type: 'text' as const, - text: JSON.stringify({ - success: false, - isError: true, - error: errorMessage, - componentId - }) - }], - isError: true - }; - } - } - ); - - // -------------------------------------------------------------- - // TOOL: Get Component Info - // -------------------------------------------------------------- - server.tool( - 'component_get_info', - '[Roopik IDE] Get full component status from the Canvas: build state (building/ready/error), error details, CDN URLs for preview. Use this to check if a component built successfully or to get error messages for debugging.', - { - componentId: z.string().describe('Component ID to get info for') - }, - async ({ componentId }: { componentId: string }) => { - try { - // Use the new unified getComponentInfo() which includes everything - const info = await componentService.getComponentInfo(componentId); - - return { - content: [{ - type: 'text' as const, - text: JSON.stringify({ - success: true, - component: info - }) - }] - }; - } catch (error) { - const errorMessage = error instanceof Error ? error.message : 'Unknown error'; - return { - content: [{ - type: 'text' as const, - text: JSON.stringify({ - success: false, - isError: true, - error: errorMessage, - componentId - }) - }], - isError: true - }; - } - } - ); - - // -------------------------------------------------------------- - // TOOL: List Components in Canvas - // -------------------------------------------------------------- - server.tool( - 'component_list', - '[Roopik IDE] List all components currently displayed on a Canvas. Returns component IDs, names, frameworks, and build states. Use this to see what components are available for preview in the IDE.', - { - canvasId: z.string().describe('Canvas ID to list components from') - }, - async ({ canvasId }: { canvasId: string }) => { - try { - const components = componentService.getComponentsForCanvas(canvasId); - - return { - content: [{ - type: 'text' as const, - text: JSON.stringify({ - success: true, - canvasId, - components: components.map(c => ({ - id: c.id, - componentName: c.componentName, - framework: c.framework, - entryFile: c.entryFile, - buildState: c.buildState - })) - }) - }] - }; - } catch (error) { - const errorMessage = error instanceof Error ? error.message : 'Unknown error'; - return { - content: [{ - type: 'text' as const, - text: JSON.stringify({ - success: false, - isError: true, - error: errorMessage, - canvasId - }) - }], - isError: true - }; - } - } - ); - - // -------------------------------------------------------------- - // TOOL: Rebuild Component - // -------------------------------------------------------------- - server.tool( - 'component_rebuild', - '[Roopik IDE] Trigger a rebuild of a component on the Canvas. Use this after fixing code errors to refresh the live preview. Check component_get_info afterward to verify the build succeeded.', - { - componentId: z.string().describe('Component ID to rebuild') - }, - async ({ componentId }: { componentId: string }) => { - try { - await componentService.rebuildComponent(componentId); - - return { - content: [{ - type: 'text' as const, - text: JSON.stringify({ - success: true, - componentId, - message: 'Rebuild queued - use getComponentInfo to check build state' - }) - }] - }; - } catch (error) { - const errorMessage = error instanceof Error ? error.message : 'Unknown error'; - return { - content: [{ - type: 'text' as const, - text: JSON.stringify({ - success: false, - isError: true, - error: errorMessage, - componentId - }) - }], - isError: true - }; - } - } - ); - - // console.log('[MCP] Registered 9 canvas/component tools: canvas_list, canvas_get_active, canvas_create, component_add, component_add_batch, component_remove, component_get_info, component_list, component_rebuild'); -} diff --git a/src/vs/workbench/contrib/roopik/electron-main/mcp/tools/contextPrompts.ts b/src/vs/workbench/contrib/roopik/electron-main/mcp/tools/contextPrompts.ts deleted file mode 100644 index 096c03c0e..000000000 --- a/src/vs/workbench/contrib/roopik/electron-main/mcp/tools/contextPrompts.ts +++ /dev/null @@ -1,424 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Roopik. All rights reserved. - * Licensed under the MIT License. - *--------------------------------------------------------------------------------------------*/ - -/** - * Contextual Prompts - * - * Pre-written workflow guides that teach AI agents how to chain MCP tools effectively. - * These are NOT tools - they're recipes for common tasks. - */ - -/** - * Register all contextual prompts with the MCP server - * - * @param server - McpServer instance (dynamically imported) - */ -export function registerContextPrompts( - // eslint-disable-next-line @typescript-eslint/no-explicit-any - server: any -): void { - - // -------------------------------------------------------------- - // PROMPT: How to Start a Project - // -------------------------------------------------------------- - server.prompt( - 'how-to-start-project', - 'Step-by-step guide for starting a development server for an existing project', - async () => { - return { - messages: [{ - role: 'user' as const, - content: { - type: 'text' as const, - text: `# How to Start a Project in Roopik - -This workflow teaches you how to start a development server for an existing project. - -## Step-by-Step Process: - -### 1. Detect the Project Framework -First, call **roopik_detectFramework** to identify the project type: -- Provide the project path -- Returns: framework (react/vue/angular/nextjs/etc), packageManager (npm/yarn/pnpm), detectedScripts - -### 2. Start the Development Server -Call **roopik_startProject** with the detected information: -- Provide projectPath, framework, and packageManager -- The dev server will start in the background -- Returns immediately with success status - -### 3. Wait for the Server to Be Ready -Call **roopik_getActiveProject** repeatedly (every 2-3 seconds) until: -- status === 'running' -- url is available (e.g., http://localhost:3000) -- This usually takes 5-15 seconds - -### 4. Navigate to the Running App -Once running, call **roopik_navigate** to view the app: -- Get the browserViewId from the active canvas -- Use the url from getActiveProject -- The browser will load the running application - -### 5. Take a Screenshot (Optional) -Call **roopik_takeScreenshot** to see the initial state: -- Helps verify the app loaded correctly -- Provides visual feedback to the user - -## Common Pitfalls: -- Don't navigate before status === 'running' (server not ready yet) -- Don't start multiple servers for the same project -- Always check for existing running projects first with getActiveProject - -## Example Tool Chain: -detectFramework → startProject → (wait) → getActiveProject → navigate → takeScreenshot` - } - }] - }; - } - ); - - // -------------------------------------------------------------- - // PROMPT: How to Create a Component - // -------------------------------------------------------------- - server.prompt( - 'how-to-create-component', - 'Step-by-step guide for creating a new canvas component', - async () => { - return { - messages: [{ - role: 'user' as const, - content: { - type: 'text' as const, - text: `# How to Create a Component in Roopik - -This workflow teaches you how to create a new component on a canvas. - -## Step-by-Step Process: - -### 1. Get or Create a Canvas -Call **roopik_getActiveCanvas** to find the current canvas: -- If no active canvas, call **roopik_createCanvas** with a name -- Returns canvasId which you'll need for the next steps - -### 2. Create the Component -Call **roopik_createComponent** with: -- canvasId: from step 1 -- name: component name (e.g., "Button", "Card") -- type: 'html', 'react', 'vue', 'angular' -- initialCode: the HTML/JSX/template code -- styles: CSS/SCSS/LESS code (optional) -- Returns componentId and metadata - -### 3. Wait for Build to Complete -Call **roopik_getComponentStatus** every 1-2 seconds until: -- buildStatus === 'success' -- buildErrors.length === 0 -- This usually takes 2-5 seconds - -### 4. Check the Build Result -If buildStatus === 'failed': -- Read buildErrors array for specific error messages -- Most common: syntax errors, missing imports, invalid CSS -- Fix the code and call **roopik_updateComponent** - -### 5. View the Component (Optional) -Once build succeeds: -- The component will be visible in the canvas -- Call **roopik_takeScreenshot** to see the rendered result -- Or call **roopik_listComponents** to verify it appears in the list - -## Common Pitfalls: -- Don't create components with duplicate names in the same canvas -- Always wait for build completion before checking visual results -- Remember to provide valid code for the component type (JSX for React, template for Vue, etc.) - -## Example Tool Chain: -getActiveCanvas → createComponent → (wait) → getComponentStatus → takeScreenshot` - } - }] - }; - } - ); - - // -------------------------------------------------------------- - // PROMPT: How to Inspect & Fix CSS - // -------------------------------------------------------------- - server.prompt( - 'how-to-inspect-css', - 'Step-by-step guide for inspecting CSS and finding source files to edit', - async () => { - return { - messages: [{ - role: 'user' as const, - content: { - type: 'text' as const, - text: `# How to Inspect & Fix CSS in Roopik - -This workflow teaches you how to inspect CSS and resolve styles to their source files. - -## Step-by-Step Process: - -### 1. Navigate to the Page -Call **roopik_navigate** to load the page with the element you want to inspect: -- Get browserViewId from the active canvas -- Provide the URL (usually http://localhost:3000 or similar) -- Wait for page to fully load - -### 2. Take a Screenshot (Optional but Helpful) -Call **roopik_takeScreenshot** to see the visual state: -- Helps you describe what element you're targeting -- Useful for confirming the issue exists - -### 3. Inspect the Element's CSS -Call **roopik_inspectElement** with: -- browserViewId: from step 1 -- selector: CSS selector (e.g., ".btn-primary", "#header", "button.submit") -- includeInherited: true (to see inherited styles) -- includeUserAgent: false (usually don't need browser defaults) - -### 4. Analyze the CSS Data -The response provides precise source file information: -- **matchedRules**: Array of CSS rules that apply to this element - - Each rule has: selector, file (absolute path), location { line, column } - - Properties with isOverridden: true are being overridden by more specific rules - - Specificity scores help understand cascade order -- **properties**: Final computed styles with values -- **cssInJs**: Detection of CSS-in-JS frameworks (styled-components, emotion, etc.) -- **inheritedStyles**: Styles from parent elements - -### 5. Find the Source File to Edit -Look at matchedRules to find where to make changes: -- Filter by origin !== 'user-agent' to skip browser defaults -- Look for highest specificity rules that aren't overridden -- Use the **file** and **location** fields to know exactly where to edit -- Example: "/project/src/styles/button.scss" at line 45, column 3 - -### 6. Make the Edit -Use your file editing capabilities to: -- Open the source file from matchedRules[].file -- Go to the line number from matchedRules[].location.line -- Modify the CSS property -- Save the file - -### 7. Reload and Verify -Call **roopik_reload** to see the changes: -- browserViewId: same as step 1 -- ignoreCache: true (for hard reload) -- Then call takeScreenshot or inspectElement again to verify the fix - -## Key Benefit - Source File Resolution: -- Traditional tools: "This element has color: red" (but where is it defined?) -- With source maps: "color: red is defined in button.scss at line 45:3, overriding button.css line 12:5" -- Resolves SCSS/LESS/PostCSS to original source files -- Detects CSS-in-JS with component file locations -- Shows the full cascade: what's overridden and why - -## Common Pitfalls: -- Don't inspect before the page is fully loaded -- For dynamic elements, wait for them to appear in the DOM -- Some styles might be from CSS-in-JS (check cssInJs field) -- Check isOverridden: if true, your edit won't have visible effect - -## Example Tool Chain: -navigate → takeScreenshot → inspectElement → (analyze matchedRules) → (edit file) → reload → takeScreenshot` - } - }] - }; - } - ); - - // -------------------------------------------------------------- - // PROMPT: Full Development Workflow - // -------------------------------------------------------------- - server.prompt( - 'full-dev-workflow', - 'Complete workflow from starting a project to creating and previewing components', - async () => { - return { - messages: [{ - role: 'user' as const, - content: { - type: 'text' as const, - text: `# Full Development Workflow in Roopik - -This workflow teaches you the complete cycle of starting a project, creating components, and previewing results. - -## Step-by-Step Process: - -### Phase 1: Start the Project -1. Call **roopik_detectFramework** to identify project type -2. Call **roopik_startProject** with detected info -3. Poll **roopik_getActiveProject** until status === 'running' - -### Phase 2: Setup Canvas -4. Call **roopik_getActiveCanvas** to check for existing canvas -5. If no canvas, call **roopik_createCanvas** with a meaningful name -6. Store the canvasId for all subsequent operations - -### Phase 3: Create Components -7. Call **roopik_createComponent** for each component you want to add: - - Provide canvasId from Phase 2 - - Use appropriate type (react/vue/html) - - Include both code and styles -8. After each component, poll **roopik_getComponentStatus** until buildStatus === 'success' -9. If build fails, read buildErrors and fix the code with **roopik_updateComponent** - -### Phase 4: Preview in Browser -10. Call **roopik_navigate** to load the canvas preview: - - Get browserViewId from canvas - - URL will be the canvas preview URL (usually http://localhost:3001/canvas/) -11. Call **roopik_takeScreenshot** to see the rendered components -12. Optionally call **roopik_inspectElement** to check specific element styles - -### Phase 5: Iterate and Refine -13. Based on the screenshot or inspection results: - - Call **roopik_updateComponent** to modify component code/styles - - Wait for build completion with getComponentStatus - - Call **roopik_reload** to refresh the preview - - Take another screenshot to verify changes - -### Phase 6: Final Validation -14. Call **roopik_listComponents** to verify all components exist -15. Call **roopik_getCanvasDetails** to see metadata and stats -16. Take a final screenshot to show the complete result - -## Real-World Example: -User: "Create a landing page with a header, hero section, and footer" - -You would: -1. Start project (Phase 1) -2. Create canvas called "LandingPage" (Phase 2) -3. Create 3 components (Phase 3): - - "Header" with nav markup and styles - - "Hero" with headline and CTA - - "Footer" with links and copyright -4. Preview and screenshot (Phase 4) -5. User feedback: "Make header sticky" -6. Inspect header CSS, find source file (Phase 5) -7. Update component with position: sticky -8. Reload and verify (Phase 5) -9. Show final result (Phase 6) - -## Common Pitfalls: -- Don't create components before project is running -- Don't navigate to preview before builds complete -- Don't forget to reload after making changes -- Always check getActiveProject before starting a new project - -## Tool Chain Summary: -detectFramework → startProject → getActiveProject → getActiveCanvas/createCanvas → createComponent × N → getComponentStatus × N → navigate → takeScreenshot → (iterate with inspectElement/updateComponent/reload) → listComponents → getCanvasDetails` - } - }] - }; - } - ); - - // -------------------------------------------------------------- - // PROMPT: How to Debug Build Errors - // -------------------------------------------------------------- - server.prompt( - 'how-to-debug-build-errors', - 'Step-by-step guide for diagnosing and fixing component build failures', - async () => { - return { - messages: [{ - role: 'user' as const, - content: { - type: 'text' as const, - text: `# How to Debug Build Errors in Roopik - -This workflow teaches you how to diagnose and fix component build failures. - -## Step-by-Step Process: - -### 1. Identify the Failure -Call **roopik_getComponentStatus** for the failing component: -- Check buildStatus: if 'failed', proceed with debugging -- Look at buildErrors array for error messages -- Check lastBuildTime to see when it failed - -### 2. Analyze Error Types -Common build errors fall into categories: - -**Syntax Errors:** -- Unclosed tags, brackets, or quotes -- Invalid JSX/template syntax -- Typos in component code -- Example: "Unexpected token" or "Expected }" - -**Import Errors:** -- Missing imports for used components/functions -- Incorrect import paths -- Example: "Cannot find module" or "Module not found" - -**Type Errors (TypeScript/JSX):** -- Invalid prop types -- Missing required props -- Type mismatches -- Example: "Property 'X' does not exist" - -**CSS Errors:** -- Invalid CSS syntax -- Unknown CSS properties -- Malformed selectors -- Example: "Unknown property" or "Parse error" - -### 3. Get Component Details -Call **roopik_getComponent** to see the current code: -- Returns code, styles, and metadata -- Check for obvious syntax issues -- Verify the component type matches the code (JSX for React, etc.) - -### 4. Fix the Code -Based on the error category, call **roopik_updateComponent** with fixes: -- For syntax errors: fix brackets, quotes, tags -- For import errors: add missing imports -- For type errors: adjust props or add type annotations -- For CSS errors: fix CSS syntax - -### 5. Wait for Rebuild -After updating, poll **roopik_getComponentStatus** again: -- Wait for buildStatus to change from 'building' to 'success' or 'failed' -- If still failing, read new buildErrors (may reveal next issue) -- Repeat steps 3-5 until build succeeds - -### 6. Verify Visual Result -Once buildStatus === 'success': -- Call **roopik_navigate** to view the canvas -- Call **roopik_takeScreenshot** to see the rendered component -- Verify the component looks and works as expected - -## Debugging Strategies: - -**Strategy 1: Simplify** -- Strip down to minimal working code -- Gradually add back features to isolate the problem - -**Strategy 2: Check Examples** -- Call **roopik_listComponents** to see other working components -- Call **roopik_getComponent** on a working component for reference -- Copy patterns that work - -**Strategy 3: Framework-Specific Checks** -- React: Valid JSX, proper hooks usage, correct prop names -- Vue: Valid template syntax, proper directives, correct script setup -- Angular: Valid template, proper decorators, correct module imports - -## Common Pitfalls: -- Don't ignore buildErrors array - it has the exact error message -- Don't make multiple changes at once - fix one error at a time -- Don't forget to wait for rebuild completion before checking results -- Don't assume CSS errors mean invalid CSS - might be SCSS/LESS syntax - -## Example Tool Chain: -getComponentStatus → (analyze buildErrors) → getComponent → updateComponent → (wait) → getComponentStatus → navigate → takeScreenshot` - } - }] - }; - } - ); - - // console.log('[MCP] Registered 5 contextual prompts (start-project, create-component, inspect-css, full-dev-workflow, debug-build-errors)'); -} diff --git a/src/vs/workbench/contrib/roopik/electron-main/mcp/tools/projectTools.ts b/src/vs/workbench/contrib/roopik/electron-main/mcp/tools/projectTools.ts deleted file mode 100644 index a7b8d2770..000000000 --- a/src/vs/workbench/contrib/roopik/electron-main/mcp/tools/projectTools.ts +++ /dev/null @@ -1,239 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Roopik. All rights reserved. - * Licensed under the MIT License. - *--------------------------------------------------------------------------------------------*/ - -/** - * Project Mode Tools - * - * MCP tools for dev server lifecycle in Project Mode. - * These give AI agents control over the live preview browser. - */ - -import { resolve, normalize } from '../../../../../../base/common/path.js'; -import type { DevServerService } from '../../projectMode/devServer/devServerService.js'; -import type { BrowserViewService } from '../../projectMode/browserViewService.js'; -import type { IRoopikStorageService } from '../../../common/storage/storageService.js'; - -/** - * Resolve a path that may be relative or absolute. - * Handles various path formats from AI agents: - * - Absolute: C:\project\src, /home/user/project - * - Relative: src/Button, ./src/Button, ../other - * - Mixed separators: src\Button, ./src\Button - * - * Cross-platform: Works on Windows, Linux, and macOS. - * On non-Windows platforms, backslashes are converted to forward slashes - * since backslash is a valid filename character on Unix systems. - * - * @param inputPath - Path from AI agent (may be relative or absolute) - * @param workspacePath - The workspace root path - * @returns Absolute path - */ -function resolvePath(inputPath: string, workspacePath: string): string { - // On non-Windows platforms, convert backslashes to forward slashes - // because backslash is a valid filename character on Unix systems - // but AI agents often send Windows-style paths regardless of platform - let sanitizedInput = inputPath; - if (process.platform !== 'win32') { - sanitizedInput = inputPath.replace(/\\/g, '/'); - } - - // Normalize to handle ./.. segments and platform-specific separators - const normalizedInput = normalize(sanitizedInput); - - // resolve handles both cases: - // - If normalizedInput is absolute, returns normalizedInput - // - If normalizedInput is relative, resolves it against workspacePath - const result = resolve(workspacePath, normalizedInput); - - // Debug log to help troubleshoot path resolution issues - console.log(`[MCP] Path resolution: "${inputPath}" -> "${result}" (workspace: "${workspacePath}", platform: ${process.platform})`); - - return result; -} - -/** - * Register all project-related MCP tools - * - * @param server - McpServer instance (dynamically imported) - * @param z - Zod validation library (dynamically imported) - * @param devServerService - DevServer service instance - * @param browserViewService - BrowserView service instance (for closing browser on project stop) - * @param storageService - Storage service for workspace path resolution - */ -export function registerProjectTools( - // eslint-disable-next-line @typescript-eslint/no-explicit-any - server: any, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - z: any, - devServerService: DevServerService, - browserViewService: BrowserViewService, - storageService: IRoopikStorageService -): void { - - // -------------------------------------------------------------- - // TOOL: Get Active Project - // -------------------------------------------------------------- - server.tool( - 'project_get_active', - '[Roopik IDE] Get the currently running project in Project Mode. Returns the dev server URL, port, framework, and project path. Use this to check if a project is running before using browser tools.', - {}, - async () => { - try { - const runningServer = await devServerService.getRunningServer(); - - if (!runningServer) { - return { - content: [{ - type: 'text' as const, - text: JSON.stringify({ - success: true, - hasActiveProject: false, - message: 'No project is currently running. Use project_start to start one.' - }) - }] - }; - } - - return { - content: [{ - type: 'text' as const, - text: JSON.stringify({ - success: true, - hasActiveProject: true, - projectPath: runningServer.projectRoot, - url: runningServer.url, - port: runningServer.port, - state: runningServer.state, - framework: runningServer.framework, - frameworkDisplayName: runningServer.frameworkDisplayName - }) - }] - }; - } catch (error: unknown) { - const message = error instanceof Error ? error.message : String(error); - return { - content: [{ - type: 'text' as const, - text: JSON.stringify({ - success: false, - isError: true, - error: message - }) - }], - isError: true - }; - } - } - ); - - // -------------------------------------------------------------- - // TOOL: Start Project - // -------------------------------------------------------------- - server.tool( - 'project_start', - '[Roopik IDE] Start a dev server for a project and open it in the IDE browser. The browser will automatically navigate to the dev server URL. Use project_get_active to check if already running. Supports both absolute and relative paths (relative to workspace).', - { - projectPath: z.string().describe('Path to the project folder. Can be absolute (e.g., C:\\project) or relative to workspace (e.g., . or ./my-app or my-app)'), - port: z.number().optional().describe('Preferred port number (optional, auto-selects if not provided)') - }, - async ({ projectPath, port }: { projectPath: string; port?: number }) => { - try { - // Resolve path (handles both absolute and relative paths) - // Use getWorkspaceRootPath() - NOT getWorkspacePath() which returns .roopik/ folder - const workspacePath = storageService.getWorkspaceRootPath(); - const resolvedPath = resolvePath(projectPath, workspacePath); - - const url = await devServerService.startServer({ - projectRoot: resolvedPath, - port - }); - - return { - content: [{ - type: 'text' as const, - text: JSON.stringify({ - success: true, - url, - projectPath, - message: `Dev server started at ${url}. Browser is now showing the project.` - }) - }] - }; - } catch (error: unknown) { - const message = error instanceof Error ? error.message : String(error); - return { - content: [{ - type: 'text' as const, - text: JSON.stringify({ - success: false, - isError: true, - error: message, - projectPath - }) - }], - isError: true - }; - } - } - ); - - // -------------------------------------------------------------- - // TOOL: Stop Project - // -------------------------------------------------------------- - server.tool( - 'project_stop', - '[Roopik IDE] Stop the currently running dev server. No parameters needed - automatically stops whatever project is active.', - {}, - async () => { - try { - const runningServer = await devServerService.getRunningServer(); - - if (!runningServer) { - return { - content: [{ - type: 'text' as const, - text: JSON.stringify({ - success: true, - message: 'No project is currently running' - }) - }] - }; - } - - const projectPath = runningServer.projectRoot; - await devServerService.stopServer(projectPath); - - // Close browser editor UI (fires event to renderer process) - browserViewService.requestBrowserClose(); - - return { - content: [{ - type: 'text' as const, - text: JSON.stringify({ - success: true, - projectPath, - message: 'Dev server stopped and browser closed' - }) - }] - }; - } catch (error: unknown) { - const message = error instanceof Error ? error.message : String(error); - return { - content: [{ - type: 'text' as const, - text: JSON.stringify({ - success: false, - isError: true, - error: message - }) - }], - isError: true - }; - } - } - ); - - // console.log('[MCP] Registered 3 project tools: project_get_active, project_start, project_stop'); -} diff --git a/src/vs/workbench/contrib/roopik/electron-main/projectMode/browserViewService.ts b/src/vs/workbench/contrib/roopik/electron-main/projectMode/browserViewService.ts index 8ef70de17..fe1de49d2 100644 --- a/src/vs/workbench/contrib/roopik/electron-main/projectMode/browserViewService.ts +++ b/src/vs/workbench/contrib/roopik/electron-main/projectMode/browserViewService.ts @@ -15,7 +15,7 @@ import { Disposable } from '../../../../../base/common/lifecycle.js'; import { CDPCssService } from './cssResolvers/cdpCssService.js'; import { StyleSourceOrchestrator } from './cssResolvers/styleSourceOrchestrator.js'; import contextMenu from 'electron-context-menu'; -import { cleanupCDPMonitoring } from '../mcp/tools/browserTools.js'; +import { cleanupCDPMonitoring } from '../mcp/executor/browserExecutor.js'; import { ILoggerService } from '../../../../../platform/log/common/log.js'; import { getRoopikLogger } from '../../common/roopikLogger.js'; From 46af98cb7d8dc53ea9ca160a4e0dcdfbe792340e Mon Sep 17 00:00:00 2001 From: Dinesh Yadav <13635627+HumbleBee14@users.noreply.github.com> Date: Fri, 30 Jan 2026 17:25:26 -0500 Subject: [PATCH 2/8] Add unified Roopik tool services Introduce unified tool service implementations used by both WebSocket MCP and Native IPC: CDPMonitorService, BrowserToolService, ProjectToolService, CanvasToolService, and ComponentToolService, plus an index export file. --- .../electron-main/tools/browserToolService.ts | 696 ++++++++++++++++++ .../electron-main/tools/canvasToolService.ts | 172 +++++ .../electron-main/tools/cdpMonitorService.ts | 467 ++++++++++++ .../tools/componentToolService.ts | 258 +++++++ .../roopik/electron-main/tools/index.ts | 30 + .../electron-main/tools/projectToolService.ts | 143 ++++ 6 files changed, 1766 insertions(+) create mode 100644 src/vs/workbench/contrib/roopik/electron-main/tools/browserToolService.ts create mode 100644 src/vs/workbench/contrib/roopik/electron-main/tools/canvasToolService.ts create mode 100644 src/vs/workbench/contrib/roopik/electron-main/tools/cdpMonitorService.ts create mode 100644 src/vs/workbench/contrib/roopik/electron-main/tools/componentToolService.ts create mode 100644 src/vs/workbench/contrib/roopik/electron-main/tools/index.ts create mode 100644 src/vs/workbench/contrib/roopik/electron-main/tools/projectToolService.ts diff --git a/src/vs/workbench/contrib/roopik/electron-main/tools/browserToolService.ts b/src/vs/workbench/contrib/roopik/electron-main/tools/browserToolService.ts new file mode 100644 index 000000000..1de1e7743 --- /dev/null +++ b/src/vs/workbench/contrib/roopik/electron-main/tools/browserToolService.ts @@ -0,0 +1,696 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Roopik. All rights reserved. + * Licensed under the MIT License. + *--------------------------------------------------------------------------------------------*/ + +/** + * Browser Tool Service + * + * Unified implementation of all browser tools. + * Single source of truth - used by both WebSocket MCP and Native IPC. + */ + +import type { BrowserViewService } from '../projectMode/browserViewService.js'; +import type { CDPMonitorService } from './cdpMonitorService.js'; +import type { + ToolResult, + BrowserOpenResult, + BrowserScreenshotResult, + BrowserNavigateResult, + BrowserActionResult, + BrowserConsoleLogsResult, + BrowserErrorsResult, + BrowserPerformanceResult, + BrowserStateResult, + ElementInspectionResult, + ScriptExecutionResult, + BrowserViewportResult, + BrowserNetworkRequestsResult, +} from '../mcp/executor/types.js'; + +// ============================================================================ +// Browser Tool Service +// ============================================================================ + +export class BrowserToolService { + constructor( + private readonly browserViewService: BrowserViewService, + private readonly cdpMonitorService: CDPMonitorService + ) {} + + // ========================================================================== + // Browser Open/Close + // ========================================================================== + + async open(url?: string): Promise> { + try { + const browserViewId = this.browserViewService.getActiveBrowserViewId(); + + if (browserViewId === undefined) { + // Browser not open - fire event for renderer to open it + this.browserViewService.requestBrowserOpen(url); + + return { + success: true, + data: { + message: 'Browser open request sent. The browser will open shortly.', + url: url || undefined + } + }; + } + + // Browser is already open - navigate if URL provided + if (url) { + await this.browserViewService.navigate(browserViewId, url); + // Enable CDP monitoring proactively + await this.cdpMonitorService.ensureMonitoring(browserViewId); + return { + success: true, + data: { + browserViewId, + url, + message: `Navigated to ${url}` + } + }; + } + + return { + success: true, + data: { + browserViewId, + message: 'Browser is already open' + } + }; + } catch (error) { + return { + success: false, + error: error instanceof Error ? error.message : 'Failed to open browser' + }; + } + } + + async close(): Promise> { + try { + const browserViewId = this.browserViewService.getActiveBrowserViewId(); + + if (browserViewId === undefined) { + return { + success: true, + data: { message: 'Browser is not open' } + }; + } + + // Cleanup CDP monitoring + this.cdpMonitorService.cleanup(browserViewId); + + // Close browser using event-based approach + this.browserViewService.requestBrowserClose(); + + return { + success: true, + data: { message: 'Browser close request sent' } + }; + } catch (error) { + return { + success: false, + error: error instanceof Error ? error.message : 'Failed to close browser' + }; + } + } + + // ========================================================================== + // Screenshot + // ========================================================================== + + async screenshot(): Promise> { + try { + const browserViewId = this.browserViewService.getActiveBrowserViewId(); + + if (browserViewId === undefined) { + return { success: false, error: 'No browser is open. Use browser_open first.' }; + } + + // takeScreenshotWithMetadata returns {image: dataUrl, width, height, devicePixelRatio} + const screenshot = await this.browserViewService.takeScreenshotWithMetadata(browserViewId); + + return { + success: true, + data: { + image: screenshot.image, + format: 'data-url', + width: screenshot.width, + height: screenshot.height, + devicePixelRatio: screenshot.devicePixelRatio, + viewport: { + width: screenshot.width, + height: screenshot.height, + devicePixelRatio: screenshot.devicePixelRatio + } + } + }; + } catch (error) { + return { + success: false, + error: error instanceof Error ? error.message : 'Failed to take screenshot' + }; + } + } + + // ========================================================================== + // Navigation + // ========================================================================== + + async navigate(url: string): Promise> { + try { + const browserViewId = this.browserViewService.getActiveBrowserViewId(); + + if (browserViewId === undefined) { + return { success: false, error: 'No browser is open. Use browser_open first.' }; + } + + await this.browserViewService.navigate(browserViewId, url); + + // Enable CDP monitoring proactively + await this.cdpMonitorService.ensureMonitoring(browserViewId); + + return { + success: true, + data: { + browserViewId, + url, + message: `Navigated to ${url}` + } + }; + } catch (error) { + return { + success: false, + error: error instanceof Error ? error.message : 'Failed to navigate' + }; + } + } + + async reload(ignoreCache?: boolean): Promise> { + try { + const browserViewId = this.browserViewService.getActiveBrowserViewId(); + + if (browserViewId === undefined) { + return { success: false, error: 'No browser is open. Use browser_open first.' }; + } + + await this.browserViewService.reload(browserViewId, ignoreCache); + + // Enable CDP monitoring proactively + await this.cdpMonitorService.ensureMonitoring(browserViewId); + + // Get current URL from navigation state + const navState = await this.browserViewService.getNavigationState(browserViewId); + const currentUrl = navState.url || 'unknown'; + + return { + success: true, + data: { + browserViewId, + url: currentUrl, + message: `Reloaded page${ignoreCache ? ' (cache ignored)' : ''}` + } + }; + } catch (error) { + return { + success: false, + error: error instanceof Error ? error.message : 'Failed to reload' + }; + } + } + + // ========================================================================== + // Input Actions + // ========================================================================== + + async actionInput(params: { + action: 'click' | 'right_click' | 'double_click' | 'hover' | 'drag' | 'type' | 'press' | 'scroll'; + coordinate?: string; + text?: string; + key?: string; + modifiers?: string[]; + deltaX?: number; + deltaY?: number; + }): Promise> { + try { + const browserViewId = this.browserViewService.getActiveBrowserViewId(); + + if (browserViewId === undefined) { + return { success: false, error: 'No browser is open. Use browser_open first.' }; + } + + const { action, coordinate, text, key, modifiers, deltaX, deltaY } = params; + + // Parse coordinates if provided + let x: number | undefined; + let y: number | undefined; + if (coordinate) { + const parts = coordinate.split(',').map(p => parseFloat(p.trim())); + if (parts.length === 2 && !isNaN(parts[0]) && !isNaN(parts[1])) { + [x, y] = parts; + } + } + + // Execute action based on type + switch (action) { + case 'click': + case 'right_click': + case 'double_click': + case 'hover': + if (x === undefined || y === undefined) { + return { success: false, error: `${action} requires coordinate parameter (e.g., "100,200")` }; + } + await this.browserViewService.sendMouseEvent(browserViewId, action, x, y); + break; + + case 'type': + if (!text) { + return { success: false, error: 'type action requires text parameter' }; + } + await this.browserViewService.sendTypeEvent(browserViewId, text); + break; + + case 'press': + if (!key) { + return { success: false, error: 'press action requires key parameter' }; + } + await this.browserViewService.sendKeyEvent(browserViewId, key, modifiers); + break; + + case 'scroll': + await this.browserViewService.sendScrollEvent(browserViewId, deltaX || 0, deltaY || 0, x, y); + break; + + case 'drag': + return { success: false, error: 'drag action not yet implemented' }; + + default: + return { success: false, error: `Unknown action: ${action}` }; + } + + return { + success: true, + data: { + browserViewId, + action, + success: true, + message: `Executed ${action} action` + } + }; + } catch (error) { + return { + success: false, + error: error instanceof Error ? error.message : 'Failed to execute action' + }; + } + } + + // ========================================================================== + // Script Execution + // ========================================================================== + + async executeScript(script: string): Promise> { + try { + const browserViewId = this.browserViewService.getActiveBrowserViewId(); + + if (browserViewId === undefined) { + return { success: false, error: 'No browser is open. Use browser_open first.' }; + } + + const result = await this.browserViewService.executeScript(browserViewId, script); + + return { + success: true, + data: { + result, + type: typeof result + } + }; + } catch (error) { + return { + success: false, + error: error instanceof Error ? error.message : 'Failed to execute script' + }; + } + } + + // ========================================================================== + // Element Inspection + // ========================================================================== + + async inspectElement(selector: string, includeInherited?: boolean): Promise> { + try { + const browserViewId = this.browserViewService.getActiveBrowserViewId(); + + if (browserViewId === undefined) { + return { success: false, error: 'No browser is open. Use browser_open first.' }; + } + + // Get project root - for now use empty string (CSS source resolution still works) + // TODO: Add DevServerService dependency to get actual project root + const projectRoot = ''; + + const result = await this.browserViewService.getElementStyles({ + browserViewId, + target: selector, + projectRoot, + includeInherited: includeInherited ?? false, + includeUserAgent: false + }); + + if (!result.success || !result.data) { + return { + success: false, + error: result.error || 'Failed to get element styles' + }; + } + + const styleInfo = result.data; + + return { + success: true, + data: { + selector, + element: { + tag: styleInfo.tagName, + classes: styleInfo.classes, + componentName: styleInfo.componentName, + componentSource: styleInfo.htmlSource?.file + }, + matchedRules: styleInfo.matchedRules, + inlineStyles: styleInfo.inlineStyles, + inheritedStyles: styleInfo.inheritedStyles, + properties: styleInfo.computedStyles as unknown[] | undefined, + cssInJs: styleInfo.cssInJs + } + }; + } catch (error) { + return { + success: false, + error: error instanceof Error ? error.message : 'Failed to inspect element' + }; + } + } + + // ========================================================================== + // CDP Monitoring - Console Logs, Errors, Network + // ========================================================================== + + async getConsoleLogs(options?: { + types?: string[]; + since?: number; + limit?: number; + clear?: boolean; + }): Promise> { + try { + const browserViewId = this.browserViewService.getActiveBrowserViewId(); + + if (browserViewId === undefined) { + return { success: false, error: 'No browser is open. Use browser_open first.' }; + } + + // Ensure monitoring is active + await this.cdpMonitorService.ensureMonitoring(browserViewId); + + const logs = this.cdpMonitorService.getConsoleLogs(browserViewId, options); + + return { + success: true, + data: { + logs: logs.map(log => ({ + id: log.id, + timestamp: log.timestamp, + type: log.type, + message: log.message, + args: log.args, + location: log.location + })), + count: logs.length + } + }; + } catch (error) { + return { + success: false, + error: error instanceof Error ? error.message : 'Failed to get console logs' + }; + } + } + + async getErrors(limit?: number): Promise> { + try { + const browserViewId = this.browserViewService.getActiveBrowserViewId(); + + if (browserViewId === undefined) { + return { success: false, error: 'No browser is open. Use browser_open first.' }; + } + + // Ensure monitoring is active + await this.cdpMonitorService.ensureMonitoring(browserViewId); + + const errors = this.cdpMonitorService.getErrors(browserViewId, limit); + + return { + success: true, + data: { + errors, + count: errors.length + } + }; + } catch (error) { + return { + success: false, + error: error instanceof Error ? error.message : 'Failed to get errors' + }; + } + } + + async getNetworkRequests(options?: { + urlFilter?: string; + method?: string; + statusFilter?: 'success' | 'error' | 'all'; + limit?: number; + }): Promise> { + try { + const browserViewId = this.browserViewService.getActiveBrowserViewId(); + + if (browserViewId === undefined) { + return { success: false, error: 'No browser is open. Use browser_open first.' }; + } + + // Ensure monitoring is active + await this.cdpMonitorService.ensureMonitoring(browserViewId); + + const requests = this.cdpMonitorService.getNetworkRequests(browserViewId, options); + + return { + success: true, + data: { + requests, + count: requests.length + } + }; + } catch (error) { + return { + success: false, + error: error instanceof Error ? error.message : 'Failed to get network requests' + }; + } + } + + // ========================================================================== + // Performance + // ========================================================================== + + async getPerformance(): Promise> { + try { + const browserViewId = this.browserViewService.getActiveBrowserViewId(); + + if (browserViewId === undefined) { + return { success: false, error: 'No browser is open. Use browser_open first.' }; + } + + // Get performance metrics via JavaScript + const webVitals = await this.browserViewService.executeScript(browserViewId, ` + (function() { + const timing = performance.timing || {}; + const entries = performance.getEntriesByType?.('navigation')?.[0] || {}; + const paint = performance.getEntriesByType?.('paint') || []; + + return { + lcp: entries.largestContentfulPaint, + fcp: paint.find(p => p.name === 'first-contentful-paint')?.startTime, + ttfb: entries.responseStart - entries.requestStart, + domContentLoaded: timing.domContentLoadedEventEnd - timing.navigationStart, + load: timing.loadEventEnd - timing.navigationStart + }; + })() + `) as Record; + + // Get runtime metrics via CDP + let runtimeMetrics: Record = {}; + + try { + await this.browserViewService.attachDebugger(browserViewId); + const result = await this.browserViewService.sendCDPCommand(browserViewId, 'Performance.getMetrics', {}); + const metrics = result.metrics as Array<{ name: string; value: number }>; + for (const metric of metrics) { + runtimeMetrics[metric.name] = metric.value; + } + } catch { + // Performance metrics not available + } + + return { + success: true, + data: { + webVitals: { + lcp: webVitals?.lcp, + fcp: webVitals?.fcp, + ttfb: webVitals?.ttfb, + domContentLoaded: webVitals?.domContentLoaded, + load: webVitals?.load + }, + runtimeMetrics: { + jsHeapUsedSize: runtimeMetrics.JSHeapUsedSize, + jsHeapTotalSize: runtimeMetrics.JSHeapTotalSize, + domNodes: runtimeMetrics.Nodes, + layoutCount: runtimeMetrics.LayoutCount, + recalcStyleCount: runtimeMetrics.RecalcStyleCount, + layoutDuration: runtimeMetrics.LayoutDuration, + recalcStyleDuration: runtimeMetrics.RecalcStyleDuration, + scriptDuration: runtimeMetrics.ScriptDuration, + taskDuration: runtimeMetrics.TaskDuration + } + } + }; + } catch (error) { + return { + success: false, + error: error instanceof Error ? error.message : 'Failed to get performance metrics' + }; + } + } + + // ========================================================================== + // Browser State + // ========================================================================== + + async getState(): Promise> { + try { + const browserViewId = this.browserViewService.getActiveBrowserViewId(); + // Note: devServerRunning state would need to be passed via different means + // For now, return false - actual dev server status should be checked via project_get_active + const devServerRunning = false; + + if (browserViewId === undefined) { + return { + success: true, + data: { + browserOpen: false, + devServerRunning, + message: 'Browser is not open' + } + }; + } + + // Get navigation state which includes url, title, isLoading + const navState = await this.browserViewService.getNavigationState(browserViewId); + + return { + success: true, + data: { + browserOpen: true, + browserViewId, + currentUrl: navState.url, + title: navState.title, + isLoading: navState.isLoading, + devServerRunning, + message: 'Browser is open' + } + }; + } catch (error) { + return { + success: false, + error: error instanceof Error ? error.message : 'Failed to get browser state' + }; + } + } + + // ========================================================================== + // Viewport + // ========================================================================== + + async setViewport(params?: { + width?: number; + height?: number; + deviceScaleFactor?: number; + mobile?: boolean; + }): Promise> { + try { + const browserViewId = this.browserViewService.getActiveBrowserViewId(); + + if (browserViewId === undefined) { + return { success: false, error: 'No browser is open. Use browser_open first.' }; + } + + // Attach debugger for CDP commands + try { + await this.browserViewService.attachDebugger(browserViewId); + } catch { + // May already be attached + } + + // If no params or no width/height, clear the override + if (!params || (params.width === undefined && params.height === undefined)) { + // Clear device metrics override via CDP + await this.browserViewService.sendCDPCommand(browserViewId, 'Emulation.clearDeviceMetricsOverride', {}); + + // Get actual viewport size + const size = this.browserViewService.getViewportSize(browserViewId); + + return { + success: true, + data: { + width: size?.width || 0, + height: size?.height || 0, + deviceScaleFactor: 1, + mobile: false, + message: 'Viewport override cleared, using natural browser size' + } + }; + } + + // Set viewport override via CDP + const width = params.width || 1280; + const height = params.height || 720; + const deviceScaleFactor = params.deviceScaleFactor || 1; + const mobile = params.mobile || false; + + await this.browserViewService.sendCDPCommand(browserViewId, 'Emulation.setDeviceMetricsOverride', { + width, + height, + deviceScaleFactor, + mobile + }); + + return { + success: true, + data: { + width, + height, + deviceScaleFactor, + mobile, + message: `Viewport set to ${width}x${height}${mobile ? ' (mobile)' : ''}` + } + }; + } catch (error) { + return { + success: false, + error: error instanceof Error ? error.message : 'Failed to set viewport' + }; + } + } +} diff --git a/src/vs/workbench/contrib/roopik/electron-main/tools/canvasToolService.ts b/src/vs/workbench/contrib/roopik/electron-main/tools/canvasToolService.ts new file mode 100644 index 000000000..2b74be384 --- /dev/null +++ b/src/vs/workbench/contrib/roopik/electron-main/tools/canvasToolService.ts @@ -0,0 +1,172 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Roopik. All rights reserved. + * Licensed under the MIT License. + *--------------------------------------------------------------------------------------------*/ + +/** + * Canvas Tool Service + * + * Unified implementation of canvas tools. + * Single source of truth - used by both WebSocket MCP and Native IPC. + */ + +import type { ICanvasService } from '../../common/canvas/canvasService.js'; +import type { + ToolResult, + CanvasListResult, + CanvasInfo, +} from '../mcp/executor/types.js'; + +// ============================================================================ +// Canvas Tool Service +// ============================================================================ + +export class CanvasToolService { + constructor( + private readonly canvasService: ICanvasService + ) {} + + // ========================================================================== + // List Canvases + // ========================================================================== + + async list(options?: { + nameFilter?: string; + sortBy?: 'name' | 'updatedAt' | 'createdAt' | 'componentCount'; + sortDirection?: 'asc' | 'desc'; + }): Promise> { + try { + let canvases = await this.canvasService.listCanvasesAsync(); + + // Filter by name + if (options?.nameFilter) { + const filter = options.nameFilter.toLowerCase(); + canvases = canvases.filter(c => c.name.toLowerCase().includes(filter)); + } + + // Sort + if (options?.sortBy) { + const direction = options.sortDirection === 'desc' ? -1 : 1; + canvases.sort((a, b) => { + switch (options.sortBy) { + case 'name': + return direction * a.name.localeCompare(b.name); + case 'updatedAt': + return direction * ((a.updatedAt || 0) - (b.updatedAt || 0)); + case 'createdAt': + return direction * ((a.createdAt || 0) - (b.createdAt || 0)); + case 'componentCount': + return direction * ((a.componentCount || 0) - (b.componentCount || 0)); + default: + return 0; + } + }); + } + + const result: CanvasInfo[] = canvases.map(c => ({ + id: c.id, + name: c.name, + description: c.description, + componentCount: c.componentCount || 0 + })); + + return { + success: true, + data: { + canvases: result, + count: result.length + } + }; + } catch (error) { + return { + success: false, + error: error instanceof Error ? error.message : 'Failed to list canvases' + }; + } + } + + // ========================================================================== + // Get Active Canvas + // ========================================================================== + + async getActive(): Promise> { + try { + // Get the focused canvas ID first + const canvasId = await this.canvasService.getFocusedCanvasIdAsync(); + + if (!canvasId) { + return { + success: true, + data: null + }; + } + + // Then get the full canvas data + const canvas = await this.canvasService.getCanvasAsync(canvasId); + + if (!canvas) { + return { + success: true, + data: null + }; + } + + return { + success: true, + data: { + id: canvas.id, + name: canvas.name, + description: canvas.description, + componentCount: canvas.componentCount || 0 + } + }; + } catch (error) { + return { + success: false, + error: error instanceof Error ? error.message : 'Failed to get active canvas' + }; + } + } + + // ========================================================================== + // Create Canvas + // ========================================================================== + + async create(name: string): Promise> { + try { + // createCanvas handles duplicates - returns existing if name matches + const result = await this.canvasService.createCanvas(name); + + // If canvas already existed, get its data + if (!result.isNew && result.canvas) { + return { + success: true, + data: { + id: result.canvas.id, + name: result.canvas.name, + description: result.canvas.description, + componentCount: result.canvas.componentCount || 0 + } + }; + } + + // New canvas created - get its data + const canvas = await this.canvasService.getCanvasAsync(result.canvasId); + + return { + success: true, + data: { + id: result.canvasId, + name: canvas?.name || name, + description: canvas?.description, + componentCount: 0 + } + }; + } catch (error) { + return { + success: false, + error: error instanceof Error ? error.message : 'Failed to create canvas' + }; + } + } +} diff --git a/src/vs/workbench/contrib/roopik/electron-main/tools/cdpMonitorService.ts b/src/vs/workbench/contrib/roopik/electron-main/tools/cdpMonitorService.ts new file mode 100644 index 000000000..43471495d --- /dev/null +++ b/src/vs/workbench/contrib/roopik/electron-main/tools/cdpMonitorService.ts @@ -0,0 +1,467 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Roopik. All rights reserved. + * Licensed under the MIT License. + *--------------------------------------------------------------------------------------------*/ + +/** + * CDP Monitor Service + * + * Centralized Chrome DevTools Protocol monitoring. + * Single source of truth for console logs, errors, and network requests. + * + * Used by both WebSocket MCP (external agents) and Native IPC (Dio agent). + */ + +import type { BrowserViewService } from '../projectMode/browserViewService.js'; + +// ============================================================================ +// CDP Monitor Types +// ============================================================================ + +export interface ConsoleLog { + id: string; + timestamp: number; + type: 'log' | 'debug' | 'info' | 'warn' | 'error' | 'dir' | 'dirxml' | 'table' | 'trace' | 'clear' | 'assert'; + message: string; + args?: unknown[]; + location?: string; + stackTrace?: { callFrames: Array<{ url: string; lineNumber: number; columnNumber: number; functionName: string }> }; +} + +export interface NetworkRequest { + id: string; + requestId: string; + timestamp: number; + method: string; + url: string; + headers: Record; + postData?: string; +} + +export interface NetworkResponse { + id: string; + requestId: string; + timestamp: number; + status: number; + statusText: string; + url: string; + headers: Record; + mimeType?: string; + duration?: number; +} + +interface CDPMonitor { + browserViewId: number; + consoleLogs: ConsoleLog[]; + networkRequests: NetworkRequest[]; + networkResponses: NetworkResponse[]; + requestStartTimes: Map; + cleanupFunctions: Array<() => void>; +} + +// ============================================================================ +// CDP Monitor Service +// ============================================================================ + +export class CDPMonitorService { + private readonly monitors = new Map(); + + constructor( + private readonly browserViewService: BrowserViewService + ) {} + + // ========================================================================== + // Public API + // ========================================================================== + + /** + * Ensure CDP monitoring is active for a browser view. + * Safe to call multiple times - only sets up once per browserViewId. + */ + async ensureMonitoring(browserViewId: number): Promise { + // Cleanup stale monitors from other browser views + for (const [monitoredId] of this.monitors) { + if (monitoredId !== browserViewId) { + this.cleanup(monitoredId); + } + } + + if (this.monitors.has(browserViewId)) { + return; + } + + const monitor: CDPMonitor = { + browserViewId, + consoleLogs: [], + networkRequests: [], + networkResponses: [], + requestStartTimes: new Map(), + cleanupFunctions: [] + }; + + // Attach debugger (throws on failure) + try { + await this.browserViewService.attachDebugger(browserViewId); + } catch { + return; + } + + // Enable Runtime domain for console + await this.browserViewService.sendCDPCommand(browserViewId, 'Runtime.enable', {}); + + // Register CDP event listener - single listener handles all events + const cdpCleanup = this.browserViewService.onCDPEvent(browserViewId, (method, params) => { + switch (method) { + case 'Runtime.consoleAPICalled': + this.handleConsoleMessage(monitor, params as Parameters[1]); + break; + case 'Network.requestWillBeSent': + this.handleNetworkRequest(monitor, params as Parameters[1]); + break; + case 'Network.responseReceived': + this.handleNetworkResponse(monitor, params as Parameters[1]); + break; + case 'Network.loadingFailed': + this.handleNetworkFailed(monitor, params as Parameters[1]); + break; + } + }); + monitor.cleanupFunctions.push(cdpCleanup); + + // Enable Network domain for requests + await this.browserViewService.sendCDPCommand(browserViewId, 'Network.enable', {}); + + this.monitors.set(browserViewId, monitor); + } + + /** + * Get console logs for a browser view. + */ + getConsoleLogs(browserViewId: number, options?: { + types?: string[]; + since?: number; + limit?: number; + clear?: boolean; + }): ConsoleLog[] { + const monitor = this.monitors.get(browserViewId); + if (!monitor) { + return []; + } + + let logs = monitor.consoleLogs; + + // Filter by types + if (options?.types && options.types.length > 0) { + logs = logs.filter(log => options.types!.includes(log.type)); + } + + // Filter by timestamp + if (options?.since) { + logs = logs.filter(log => log.timestamp >= options.since!); + } + + // Limit results + if (options?.limit && options.limit > 0) { + logs = logs.slice(-options.limit); + } + + // Clear after reading if requested + if (options?.clear) { + monitor.consoleLogs = []; + } + + return logs; + } + + /** + * Get errors (console errors + network failures) for a browser view. + */ + getErrors(browserViewId: number, limit?: number): Array<{ + id: string; + timestamp: number; + source: 'console' | 'network'; + type: string; + message: string; + location?: string; + url?: string; + method?: string; + }> { + const monitor = this.monitors.get(browserViewId); + if (!monitor) { + return []; + } + + const errors: Array<{ + id: string; + timestamp: number; + source: 'console' | 'network'; + type: string; + message: string; + location?: string; + url?: string; + method?: string; + }> = []; + + // Console errors + for (const log of monitor.consoleLogs) { + if (log.type === 'error') { + errors.push({ + id: log.id, + timestamp: log.timestamp, + source: 'console', + type: 'error', + message: log.message, + location: log.location + }); + } + } + + // Network failures (4xx, 5xx, or failed) + for (const response of monitor.networkResponses) { + if (response.status >= 400 || response.status === 0) { + const request = monitor.networkRequests.find(r => r.requestId === response.requestId); + errors.push({ + id: response.id, + timestamp: response.timestamp, + source: 'network', + type: response.status === 0 ? 'failed' : `${response.status}`, + message: response.status === 0 ? 'Request failed' : response.statusText, + url: response.url, + method: request?.method + }); + } + } + + // Sort by timestamp + errors.sort((a, b) => a.timestamp - b.timestamp); + + // Apply limit + if (limit && limit > 0) { + return errors.slice(-limit); + } + + return errors; + } + + /** + * Get network requests for a browser view. + */ + getNetworkRequests(browserViewId: number, options?: { + urlFilter?: string; + method?: string; + statusFilter?: 'success' | 'error' | 'all'; + limit?: number; + }): Array<{ + id: string; + method: string; + url: string; + status?: number; + statusText?: string; + mimeType?: string; + duration?: number; + timestamp: number; + requestHeaders?: Record; + responseHeaders?: Record; + }> { + const monitor = this.monitors.get(browserViewId); + if (!monitor) { + return []; + } + + // Join requests with responses + let requests = monitor.networkRequests.map(req => { + const response = monitor.networkResponses.find(r => r.requestId === req.requestId); + return { + id: req.id, + method: req.method, + url: req.url, + status: response?.status, + statusText: response?.statusText, + mimeType: response?.mimeType, + duration: response?.duration, + timestamp: req.timestamp, + requestHeaders: req.headers, + responseHeaders: response?.headers + }; + }); + + // Filter by URL + if (options?.urlFilter) { + const filter = options.urlFilter.toLowerCase(); + requests = requests.filter(r => r.url.toLowerCase().includes(filter)); + } + + // Filter by method + if (options?.method) { + const method = options.method.toUpperCase(); + requests = requests.filter(r => r.method === method); + } + + // Filter by status + if (options?.statusFilter && options.statusFilter !== 'all') { + if (options.statusFilter === 'success') { + requests = requests.filter(r => r.status && r.status >= 200 && r.status < 400); + } else if (options.statusFilter === 'error') { + requests = requests.filter(r => !r.status || r.status >= 400); + } + } + + // Apply limit (max 500) + const limit = Math.min(options?.limit || 100, 500); + return requests.slice(-limit); + } + + /** + * Cleanup CDP monitoring for a browser view. + */ + cleanup(browserViewId: number): void { + const monitor = this.monitors.get(browserViewId); + if (monitor) { + for (const cleanup of monitor.cleanupFunctions) { + try { + cleanup(); + } catch { + // Ignore cleanup errors + } + } + this.monitors.delete(browserViewId); + } + } + + /** + * Check if monitoring is active for a browser view. + */ + isMonitoring(browserViewId: number): boolean { + return this.monitors.has(browserViewId); + } + + // ========================================================================== + // Internal Handlers + // ========================================================================== + + private handleConsoleMessage(monitor: CDPMonitor, params: { + type: string; + args: Array<{ type: string; value?: unknown; unserializableValue?: string; description?: string }>; + timestamp: number; + stackTrace?: { callFrames: Array<{ url: string; lineNumber: number; columnNumber: number; functionName: string }> }; + }): void { + const { type, args, timestamp, stackTrace } = params; + + const messages = args.map(arg => { + if (arg.value !== undefined) { + return typeof arg.value === 'object' ? JSON.stringify(arg.value) : String(arg.value); + } + if (arg.unserializableValue) { + return arg.unserializableValue; + } + if (arg.description) { + return arg.description; + } + return '[object]'; + }); + + const log: ConsoleLog = { + id: `console-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`, + timestamp: timestamp * 1000, + type: type as ConsoleLog['type'], + message: messages.join(' '), + args: args.map(a => a.value), + stackTrace + }; + + if (stackTrace?.callFrames?.[0]) { + const frame = stackTrace.callFrames[0]; + log.location = `${frame.url}:${frame.lineNumber + 1}:${frame.columnNumber + 1}`; + } + + monitor.consoleLogs.push(log); + + // Keep max 1000 logs + if (monitor.consoleLogs.length > 1000) { + monitor.consoleLogs.shift(); + } + } + + private handleNetworkRequest(monitor: CDPMonitor, params: { + requestId: string; + request: { url: string; method: string; headers: Record; postData?: string }; + timestamp: number; + }): void { + const { requestId, request, timestamp } = params; + + monitor.requestStartTimes.set(requestId, timestamp); + + const networkRequest: NetworkRequest = { + id: `req-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`, + requestId, + timestamp: timestamp * 1000, + method: request.method, + url: request.url, + headers: request.headers || {}, + postData: request.postData + }; + + monitor.networkRequests.push(networkRequest); + + // Keep max 500 requests + if (monitor.networkRequests.length > 500) { + monitor.networkRequests.shift(); + } + } + + private handleNetworkResponse(monitor: CDPMonitor, params: { + requestId: string; + response: { url: string; status: number; statusText: string; headers: Record; mimeType?: string }; + timestamp: number; + }): void { + const { requestId, response, timestamp } = params; + + const startTime = monitor.requestStartTimes.get(requestId); + const duration = startTime ? (timestamp - startTime) * 1000 : undefined; + + const networkResponse: NetworkResponse = { + id: `res-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`, + requestId, + timestamp: timestamp * 1000, + status: response.status, + statusText: response.statusText, + url: response.url, + headers: response.headers || {}, + mimeType: response.mimeType, + duration + }; + + monitor.networkResponses.push(networkResponse); + + // Keep max 500 responses + if (monitor.networkResponses.length > 500) { + monitor.networkResponses.shift(); + } + } + + private handleNetworkFailed(monitor: CDPMonitor, params: { + requestId: string; + timestamp: number; + errorText?: string; + }): void { + const { requestId, timestamp } = params; + + const request = monitor.networkRequests.find(r => r.requestId === requestId); + + const networkResponse: NetworkResponse = { + id: `res-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`, + requestId, + timestamp: timestamp * 1000, + status: 0, + statusText: 'Failed', + url: request?.url || 'unknown', + headers: {} + }; + + monitor.networkResponses.push(networkResponse); + + if (monitor.networkResponses.length > 500) { + monitor.networkResponses.shift(); + } + } +} diff --git a/src/vs/workbench/contrib/roopik/electron-main/tools/componentToolService.ts b/src/vs/workbench/contrib/roopik/electron-main/tools/componentToolService.ts new file mode 100644 index 000000000..ef3b8e5a7 --- /dev/null +++ b/src/vs/workbench/contrib/roopik/electron-main/tools/componentToolService.ts @@ -0,0 +1,258 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Roopik. All rights reserved. + * Licensed under the MIT License. + *--------------------------------------------------------------------------------------------*/ + +/** + * Component Tool Service + * + * Unified implementation of component tools. + * Single source of truth - used by both WebSocket MCP and Native IPC. + */ + +import type { ComponentService } from '../component/componentService.js'; +import type { ICanvasService } from '../../common/canvas/canvasService.js'; +import type { + ToolResult, + ComponentListResult, + ComponentInfo, +} from '../mcp/executor/types.js'; + +// ============================================================================ +// Component Add Params +// ============================================================================ + +export interface ComponentAddParams { + canvasId?: string; + folderPath: string; + name?: string; + entryFile?: string; + framework?: 'react' | 'vue' | 'svelte' | 'solid' | 'preact' | 'html'; +} + +export interface ComponentAddResult { + id: string; + name: string; + path: string; + canvasId: string; + framework?: string; + message: string; +} + +export interface ComponentRemoveResult { + removed: boolean; + message: string; +} + +export interface ComponentRebuildResult { + rebuilding: boolean; + message: string; +} + +// ============================================================================ +// Component Tool Service +// ============================================================================ + +export class ComponentToolService { + constructor( + private readonly componentService: ComponentService, + private readonly canvasService: ICanvasService + ) {} + + // ========================================================================== + // Add Component + // ========================================================================== + + async add(params: ComponentAddParams): Promise> { + try { + // Get canvas ID (use active if not provided) + let canvasId = params.canvasId; + if (!canvasId) { + const activeCanvasId = await this.canvasService.getFocusedCanvasIdAsync(); + if (!activeCanvasId) { + return { + success: false, + error: 'No active canvas. Create a canvas first with canvas_create.' + }; + } + canvasId = activeCanvasId; + } + + // Add component + const component = await this.componentService.addComponent({ + canvasId, + folderPath: params.folderPath, + componentName: params.name, + entryFile: params.entryFile, + framework: params.framework + }); + + return { + success: true, + data: { + id: component.id, + name: component.componentName || params.name || 'Component', + path: params.folderPath, + canvasId, + framework: component.framework, + message: `Component added to canvas` + } + }; + } catch (error) { + return { + success: false, + error: error instanceof Error ? error.message : 'Failed to add component' + }; + } + } + + // ========================================================================== + // Add Multiple Components + // ========================================================================== + + async addBatch(components: ComponentAddParams[]): Promise> { + try { + const results: ComponentAddResult[] = []; + + for (const params of components) { + const result = await this.add(params); + if (result.success && result.data) { + results.push(result.data); + } + } + + return { + success: true, + data: results + }; + } catch (error) { + return { + success: false, + error: error instanceof Error ? error.message : 'Failed to add components' + }; + } + } + + // ========================================================================== + // Remove Component + // ========================================================================== + + async remove(componentId: string, deleteSourceCode?: boolean): Promise> { + try { + await this.componentService.deleteComponent(componentId, deleteSourceCode); + + return { + success: true, + data: { + removed: true, + message: `Component removed${deleteSourceCode ? ' (source deleted)' : ''}` + } + }; + } catch (error) { + return { + success: false, + error: error instanceof Error ? error.message : 'Failed to remove component' + }; + } + } + + // ========================================================================== + // Get Component Info + // ========================================================================== + + async getInfo(componentId: string): Promise> { + try { + // getComponent is sync - returns Component | undefined + const component = this.componentService.getComponent(componentId); + + if (!component) { + return { + success: false, + error: `Component not found: ${componentId}` + }; + } + + // Get comprehensive info via async getComponentInfo for build state + const componentInfo = await this.componentService.getComponentInfo(componentId); + + // Map buildStatus from ComponentInfo to the status field expected by MCP + // ComponentInfo.buildStatus is 'building' | 'ready' | 'error', MCP adds 'pending' + const status = componentInfo.buildStatus === 'building' ? 'building' : + componentInfo.buildStatus === 'ready' ? 'ready' : + componentInfo.buildStatus === 'error' ? 'error' : 'pending'; + + return { + success: true, + data: { + id: component.id, + name: component.componentName, + path: component.folderPath, + canvasId: component.canvasId, + status, + buildStatus: componentInfo.buildStatus, + buildErrors: componentInfo.buildErrorInfo?.message ? [componentInfo.buildErrorInfo.message] : undefined + } + }; + } catch (error) { + return { + success: false, + error: error instanceof Error ? error.message : 'Failed to get component info' + }; + } + } + + // ========================================================================== + // List Components + // ========================================================================== + + async list(canvasId: string): Promise> { + try { + // getComponentsForCanvas is sync + const components = this.componentService.getComponentsForCanvas(canvasId); + + const result: ComponentInfo[] = components.map(c => ({ + id: c.id, + name: c.componentName, + path: c.folderPath, + canvasId: c.canvasId, + status: c.buildState.status + })); + + return { + success: true, + data: { + components: result, + count: result.length + } + }; + } catch (error) { + return { + success: false, + error: error instanceof Error ? error.message : 'Failed to list components' + }; + } + } + + // ========================================================================== + // Rebuild Component + // ========================================================================== + + async rebuild(componentId: string): Promise> { + try { + await this.componentService.rebuildComponent(componentId); + + return { + success: true, + data: { + rebuilding: true, + message: 'Component rebuild triggered' + } + }; + } catch (error) { + return { + success: false, + error: error instanceof Error ? error.message : 'Failed to rebuild component' + }; + } + } +} diff --git a/src/vs/workbench/contrib/roopik/electron-main/tools/index.ts b/src/vs/workbench/contrib/roopik/electron-main/tools/index.ts new file mode 100644 index 000000000..2f321a6af --- /dev/null +++ b/src/vs/workbench/contrib/roopik/electron-main/tools/index.ts @@ -0,0 +1,30 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Roopik. All rights reserved. + * Licensed under the MIT License. + *--------------------------------------------------------------------------------------------*/ + +/** + * Unified Tool Services + * + * Single source of truth for all tool implementations. + * Used by both WebSocket MCP (external agents) and Native IPC (Dio agent). + * + * Architecture: + * - CDPMonitorService: Centralized Chrome DevTools Protocol monitoring + * - BrowserToolService: All browser tools (14 tools) + * - ProjectToolService: Dev server tools (3 tools) + * - CanvasToolService: Canvas management tools (3 tools) + * - ComponentToolService: Component management tools (6 tools) + */ + +export { CDPMonitorService } from './cdpMonitorService.js'; +export type { ConsoleLog, NetworkRequest, NetworkResponse } from './cdpMonitorService.js'; + +export { BrowserToolService } from './browserToolService.js'; + +export { ProjectToolService } from './projectToolService.js'; + +export { CanvasToolService } from './canvasToolService.js'; + +export { ComponentToolService } from './componentToolService.js'; +export type { ComponentAddParams, ComponentAddResult, ComponentRemoveResult, ComponentRebuildResult } from './componentToolService.js'; diff --git a/src/vs/workbench/contrib/roopik/electron-main/tools/projectToolService.ts b/src/vs/workbench/contrib/roopik/electron-main/tools/projectToolService.ts new file mode 100644 index 000000000..7d9399ecd --- /dev/null +++ b/src/vs/workbench/contrib/roopik/electron-main/tools/projectToolService.ts @@ -0,0 +1,143 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Roopik. All rights reserved. + * Licensed under the MIT License. + *--------------------------------------------------------------------------------------------*/ + +/** + * Project Tool Service + * + * Unified implementation of project/dev server tools. + * Single source of truth - used by both WebSocket MCP and Native IPC. + */ + +import type { DevServerService } from '../projectMode/devServer/devServerService.js'; +import type { + ToolResult, + ProjectServerInfo, + ProjectStartResult, + ProjectStopResult, +} from '../mcp/executor/types.js'; + +// ============================================================================ +// Project Tool Service +// ============================================================================ + +export class ProjectToolService { + constructor( + private readonly devServerService: DevServerService + ) {} + + // ========================================================================== + // Get Active Project + // ========================================================================== + + async getActive(): Promise> { + try { + const serverInfo = await this.devServerService.getRunningServer(); + + if (!serverInfo) { + return { + success: true, + data: null + }; + } + + // Map DevServerState to ProjectServerInfo.status + // DevServerState: 'stopped' | 'starting' | 'running' | 'error' + // ProjectServerInfo.status: 'starting' | 'running' | 'stopping' | 'stopped' + const status = serverInfo.state === 'error' ? 'stopped' as const : serverInfo.state; + + return { + success: true, + data: { + url: serverInfo.url, + projectRoot: serverInfo.projectRoot, + framework: serverInfo.framework, + status + } + }; + } catch (error) { + return { + success: false, + error: error instanceof Error ? error.message : 'Failed to get project status' + }; + } + } + + // ========================================================================== + // Start Project + // ========================================================================== + + async start(projectPath: string, port?: number): Promise> { + try { + // Check if already running + const runningServer = await this.devServerService.getRunningServer(); + if (runningServer) { + return { + success: false, + error: `Dev server already running at ${runningServer.url}. Stop it first with project_stop.` + }; + } + + // Start the dev server - returns URL on success, throws on failure + const url = await this.devServerService.startServer({ + projectRoot: projectPath, + port + }); + + // Get server info to retrieve framework + const serverInfo = await this.devServerService.getRunningServer(); + + return { + success: true, + data: { + url, + projectRoot: projectPath, + framework: serverInfo?.framework, + message: `Dev server starting for ${projectPath}` + } + }; + } catch (error) { + return { + success: false, + error: error instanceof Error ? error.message : 'Failed to start project' + }; + } + } + + // ========================================================================== + // Stop Project + // ========================================================================== + + async stop(): Promise> { + try { + const runningServer = await this.devServerService.getRunningServer(); + + if (!runningServer) { + return { + success: true, + data: { + stopped: true, + message: 'No dev server is running' + } + }; + } + + // Stop all servers (single server constraint means this stops the one running server) + await this.devServerService.stopAllServers(); + + return { + success: true, + data: { + stopped: true, + message: 'Dev server stopped' + } + }; + } catch (error) { + return { + success: false, + error: error instanceof Error ? error.message : 'Failed to stop project' + }; + } + } +} From 9e8c803318775f08cae6ad8fb10c23a5466aab08 Mon Sep 17 00:00:00 2001 From: Dinesh Yadav <13635627+HumbleBee14@users.noreply.github.com> Date: Fri, 30 Jan 2026 17:51:39 -0500 Subject: [PATCH 3/8] Phase 3: WebSocket MCP migrated - Migrate ToolExecutor to unified tool services Refactor ToolExecutor as part of Phase 3 migration to use unified tool services from electron-main/tools. Replaces the old BrowserExecutor/CanvasExecutor/ProjectExecutor with CDPMonitorService, BrowserToolService, CanvasToolService, ComponentToolService and ProjectToolService, constructs CDPMonitorService first (used by BrowserToolService) and delegates all tool routing to these services. Keeps the storageService parameter (renamed _storageService) for backward compatibility; this file is now a thin adapter/dispatcher that forwards tool calls to the unified services. --- eslint.config.js | 7 +- .../mcp/executor/toolExecutor.ts | 106 ++++++++++-------- 2 files changed, 66 insertions(+), 47 deletions(-) diff --git a/eslint.config.js b/eslint.config.js index 5ddfc7106..b67f7d244 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -2152,9 +2152,12 @@ export default tseslint.config( ], } }, - // ROOPIK: Override header rule for roopik extension + // ROOPIK: Override header rule for roopik files (extension + core) { - files: ['extensions/roopik/**/*.{ts,tsx,js,jsx}'], + files: [ + 'extensions/roopik/**/*.{ts,tsx,js,jsx}', + 'src/vs/workbench/contrib/roopik/**/*.{ts,tsx,js,jsx}' + ], plugins: { header: pluginHeader }, rules: { 'header/header': [2, 'block', [ diff --git a/src/vs/workbench/contrib/roopik/electron-main/mcp/executor/toolExecutor.ts b/src/vs/workbench/contrib/roopik/electron-main/mcp/executor/toolExecutor.ts index 313b2c4f5..a59905cc5 100644 --- a/src/vs/workbench/contrib/roopik/electron-main/mcp/executor/toolExecutor.ts +++ b/src/vs/workbench/contrib/roopik/electron-main/mcp/executor/toolExecutor.ts @@ -7,8 +7,12 @@ * Tool Executor - Central Dispatcher * * Single entry point for all MCP tool calls. - * Routes tool calls to the appropriate executor (browser, canvas, project). - * Both HTTP MCP and WebSocket MCP call this dispatcher. + * Routes tool calls to the appropriate unified tool service. + * + * Architecture (Phase 3 Migration): + * - Uses unified tool services from electron-main/tools/ + * - Services are single source of truth for all tool implementations + * - This file is now a thin adapter that routes and delegates */ import type { BrowserViewService } from '../../projectMode/browserViewService.js'; @@ -17,9 +21,13 @@ import type { ICanvasService } from '../../../common/canvas/canvasService.js'; import type { ComponentService } from '../../component/componentService.js'; import type { DevServerService } from '../../projectMode/devServer/devServerService.js'; import type { ToolResult } from './types.js'; -import { BrowserExecutor } from './browserExecutor.js'; -import { CanvasExecutor } from './canvasExecutor.js'; -import { ProjectExecutor } from './projectExecutor.js'; + +// Import unified tool services +import { CDPMonitorService } from '../../tools/cdpMonitorService.js'; +import { BrowserToolService } from '../../tools/browserToolService.js'; +import { CanvasToolService } from '../../tools/canvasToolService.js'; +import { ComponentToolService } from '../../tools/componentToolService.js'; +import { ProjectToolService } from '../../tools/projectToolService.js'; // ============================================================================ // Tool Call Types @@ -41,20 +49,28 @@ export interface ToolCallResult { // ============================================================================ export class ToolExecutor { - private readonly browserExecutor: BrowserExecutor; - private readonly canvasExecutor: CanvasExecutor; - private readonly projectExecutor: ProjectExecutor; + // Unified tool services (Phase 3 migration) + private readonly cdpMonitorService: CDPMonitorService; + private readonly browserToolService: BrowserToolService; + private readonly canvasToolService: CanvasToolService; + private readonly componentToolService: ComponentToolService; + private readonly projectToolService: ProjectToolService; constructor( browserViewService: BrowserViewService, - storageService: IRoopikStorageService, + _storageService: IRoopikStorageService, // Kept for backward compatibility, no longer used canvasService: ICanvasService, componentService: ComponentService, devServerService: DevServerService ) { - this.browserExecutor = new BrowserExecutor(browserViewService, storageService); - this.canvasExecutor = new CanvasExecutor(canvasService, componentService); - this.projectExecutor = new ProjectExecutor(devServerService, browserViewService, storageService); + // Create CDPMonitorService first (used by BrowserToolService) + this.cdpMonitorService = new CDPMonitorService(browserViewService); + + // Create unified tool services + this.browserToolService = new BrowserToolService(browserViewService, this.cdpMonitorService); + this.canvasToolService = new CanvasToolService(canvasService); + this.componentToolService = new ComponentToolService(componentService, canvasService); + this.projectToolService = new ProjectToolService(devServerService); } /** @@ -132,28 +148,28 @@ export class ToolExecutor { } // ========================================================================== - // Browser Tool Routing (14 tools) + // Browser Tool Routing (14 tools) - Delegates to BrowserToolService // ========================================================================== private async executeBrowserTool(tool: string, params: Record): Promise> { switch (tool) { case 'browser_open': - return this.browserExecutor.open(params.url as string | undefined); + return this.browserToolService.open(params.url as string | undefined); case 'browser_close': - return this.browserExecutor.close(); + return this.browserToolService.close(); case 'browser_screenshot': - return this.browserExecutor.screenshot(); + return this.browserToolService.screenshot(); case 'browser_navigate': - return this.browserExecutor.navigate(params.url as string); + return this.browserToolService.navigate(params.url as string); case 'browser_reload': - return this.browserExecutor.reload(params.ignoreCache as boolean | undefined); + return this.browserToolService.reload(params.ignoreCache as boolean | undefined); case 'browser_action_input': - return this.browserExecutor.actionInput({ + return this.browserToolService.actionInput({ action: params.action as 'click' | 'right_click' | 'double_click' | 'hover' | 'drag' | 'type' | 'press' | 'scroll', coordinate: params.coordinate as string | undefined, text: params.text as string | undefined, @@ -164,19 +180,19 @@ export class ToolExecutor { }); case 'browser_execute_script': - return this.browserExecutor.executeScript(params.script as string); + return this.browserToolService.executeScript(params.script as string); case 'browser_inspect_element': - return this.browserExecutor.inspectElement({ - selector: params.selector as string, - includeInherited: params.includeInherited as boolean | undefined, - }); + return this.browserToolService.inspectElement( + params.selector as string, + params.includeInherited as boolean | undefined + ); case 'browser_get_errors': - return this.browserExecutor.getErrors(params.limit as number | undefined); + return this.browserToolService.getErrors(params.limit as number | undefined); case 'browser_get_console_logs': - return this.browserExecutor.getConsoleLogs({ + return this.browserToolService.getConsoleLogs({ types: params.types as string[] | undefined, since: params.since as number | undefined, limit: params.limit as number | undefined, @@ -184,14 +200,14 @@ export class ToolExecutor { }); case 'browser_get_performance': - return this.browserExecutor.getPerformance(); + return this.browserToolService.getPerformance(); case 'browser_get_state': - return this.browserExecutor.getState(); + return this.browserToolService.getState(); case 'browser_set_viewport': // If no params or no width/height, pass undefined to clear viewport - return this.browserExecutor.setViewport( + return this.browserToolService.setViewport( Object.keys(params).length === 0 ? undefined : { width: params.width as number | undefined, height: params.height as number | undefined, @@ -201,7 +217,7 @@ export class ToolExecutor { ); case 'browser_get_network_requests': - return this.browserExecutor.getNetworkRequests({ + return this.browserToolService.getNetworkRequests({ urlFilter: params.urlFilter as string | undefined, method: params.method as string | undefined, statusFilter: params.statusFilter as 'success' | 'error' | 'all' | undefined, @@ -217,23 +233,23 @@ export class ToolExecutor { } // ========================================================================== - // Canvas Tool Routing (3 tools) + // Canvas Tool Routing (3 tools) - Delegates to CanvasToolService // ========================================================================== private async executeCanvasTool(tool: string, params: Record): Promise> { switch (tool) { case 'canvas_list': - return this.canvasExecutor.listCanvases({ + return this.canvasToolService.list({ nameFilter: params.nameFilter as string | undefined, sortBy: params.sortBy as 'name' | 'updatedAt' | 'createdAt' | 'componentCount' | undefined, sortDirection: params.sortDirection as 'asc' | 'desc' | undefined, }); case 'canvas_get_active': - return this.canvasExecutor.getActiveCanvas(); + return this.canvasToolService.getActive(); case 'canvas_create': - return this.canvasExecutor.createCanvas(params.name as string); + return this.canvasToolService.create(params.name as string); default: return { @@ -244,13 +260,13 @@ export class ToolExecutor { } // ========================================================================== - // Component Tool Routing (6 tools) + // Component Tool Routing (6 tools) - Delegates to ComponentToolService // ========================================================================== private async executeComponentTool(tool: string, params: Record): Promise> { switch (tool) { case 'component_add': - return this.canvasExecutor.addComponent({ + return this.componentToolService.add({ canvasId: params.canvasId as string | undefined, folderPath: params.folderPath as string, name: params.name as string | undefined, @@ -259,7 +275,7 @@ export class ToolExecutor { }); case 'component_add_batch': - return this.canvasExecutor.addComponentBatch(params.components as Array<{ + return this.componentToolService.addBatch(params.components as Array<{ canvasId?: string; folderPath: string; name?: string; @@ -268,19 +284,19 @@ export class ToolExecutor { }>); case 'component_remove': - return this.canvasExecutor.removeComponent( + return this.componentToolService.remove( params.componentId as string, params.deleteSourceCode as boolean | undefined ); case 'component_get_info': - return this.canvasExecutor.getComponentInfo(params.componentId as string); + return this.componentToolService.getInfo(params.componentId as string); case 'component_list': - return this.canvasExecutor.listComponents(params.canvasId as string); + return this.componentToolService.list(params.canvasId as string); case 'component_rebuild': - return this.canvasExecutor.rebuildComponent(params.componentId as string); + return this.componentToolService.rebuild(params.componentId as string); default: return { @@ -291,22 +307,22 @@ export class ToolExecutor { } // ========================================================================== - // Project Tool Routing (3 tools) + // Project Tool Routing (3 tools) - Delegates to ProjectToolService // ========================================================================== private async executeProjectTool(tool: string, params: Record): Promise> { switch (tool) { case 'project_get_active': - return this.projectExecutor.getActiveProject(); + return this.projectToolService.getActive(); case 'project_start': - return this.projectExecutor.startProject( + return this.projectToolService.start( params.projectPath as string, params.port as number | undefined ); case 'project_stop': - return this.projectExecutor.stopProject(); + return this.projectToolService.stop(); default: return { From ad1938174c8f184a9da982324b5cade368414afd Mon Sep 17 00:00:00 2001 From: Dinesh Yadav <13635627+HumbleBee14@users.noreply.github.com> Date: Fri, 30 Jan 2026 20:32:21 -0500 Subject: [PATCH 4/8] Migrate Roopik channel to unified tool services Phase 4 migration: RoopikToolsChannel now instantiates and delegates to unified tool services (CDPMonitorService, BrowserToolService, CanvasToolService, ComponentToolService, ProjectToolService) instead of containing inlined browser/CDP/canvas logic. Many handlers (screenshot, close, action input, navigate, reload, executeScript, inspectElement, CDP APIs, viewport/network requests, project/canvas/component ops) were refactored to call the new services and responses adapted where needed. The legacy browser executor implementation was removed and MCP executor modules (browserExecutor, canvasExecutor, projectExecutor) are being consolidated into the unified tool services. This centralizes logic, reuses CDP monitoring, and simplifies the IPC channel to a thin orchestration layer. --- .../channel/roopikToolsChannel.ts | 552 ++------ .../mcp/executor/browserExecutor.ts | 1171 ----------------- .../mcp/executor/canvasExecutor.ts | 363 ----- .../electron-main/mcp/executor/index.ts | 12 +- .../mcp/executor/projectExecutor.ts | 157 --- .../mcp/executor/toolExecutor.ts | 8 +- .../electron-main/mcp/executor/types.ts | 15 +- .../projectMode/browserViewService.ts | 2 +- .../electron-main/tools/browserToolService.ts | 15 +- .../electron-main/tools/canvasToolService.ts | 16 +- .../electron-main/tools/cdpMonitorService.ts | 67 +- .../roopik/electron-main/tools/index.ts | 2 +- .../electron-main/tools/projectToolService.ts | 1 + 13 files changed, 207 insertions(+), 2174 deletions(-) delete mode 100644 src/vs/workbench/contrib/roopik/electron-main/mcp/executor/browserExecutor.ts delete mode 100644 src/vs/workbench/contrib/roopik/electron-main/mcp/executor/canvasExecutor.ts delete mode 100644 src/vs/workbench/contrib/roopik/electron-main/mcp/executor/projectExecutor.ts diff --git a/src/vs/workbench/contrib/roopik/electron-main/channel/roopikToolsChannel.ts b/src/vs/workbench/contrib/roopik/electron-main/channel/roopikToolsChannel.ts index e1e4b7803..091a0bedf 100644 --- a/src/vs/workbench/contrib/roopik/electron-main/channel/roopikToolsChannel.ts +++ b/src/vs/workbench/contrib/roopik/electron-main/channel/roopikToolsChannel.ts @@ -9,10 +9,11 @@ * IPC channel that exposes Roopik IDE tools to extensions (agent roopik-dio). * This provides a persistent, timeout-free connection for AI agent tool calls. * - * Architecture: - * - Extension (agent roopik-dio) -> IChannel.call() -> RoopikToolsChannel.call() -> Services - * - Replaces HTTP-based MCP for internal agent communication - * - Same tools as MCP but via direct IPC (faster, no timeouts) + * Architecture : + * - Extension (agent roopik-dio) -> IChannel.call() -> RoopikToolsChannel.call() -> Unified Services + * - Uses unified tool services from electron-main/tools/ (single source of truth) + * - Channel acts as thin orchestration layer: path resolution, workspace context, seamless UX + * - Same services used by external agents (Claude Code, Cursor, Codex) via WebSocket MCP * * Tool Naming Convention: category_action (e.g., browser_navigate, component_add) * @@ -38,6 +39,13 @@ import type { ICanvasService } from '../../common/canvas/canvasService.js'; import type { IRoopikStorageService } from '../../common/storage/storageService.js'; import { ROOPIK_TOOLS_CHANNEL_NAME, RoopikToolResult } from '../../common/tools/types.js'; +// Import unified tool services (Phase 4 migration) +import { CDPMonitorService } from '../tools/cdpMonitorService.js'; +import { BrowserToolService } from '../tools/browserToolService.js'; +import { CanvasToolService } from '../tools/canvasToolService.js'; +import { ComponentToolService } from '../tools/componentToolService.js'; +import { ProjectToolService } from '../tools/projectToolService.js'; + // Re-export for backwards compatibility with app.ts import export { ROOPIK_TOOLS_CHANNEL_NAME, RoopikToolResult }; @@ -46,16 +54,37 @@ export { ROOPIK_TOOLS_CHANNEL_NAME, RoopikToolResult }; * * Provides direct IPC access to all Roopik IDE capabilities for extensions. * This is the core-side handler that receives calls from agent roopik-dio. + * + * Architecture: + * - Unified tool services handle all tool logic (single source of truth) + * - Channel provides orchestration: path resolution, workspace context, seamless UX + * - Same implementation powers both internal (Dio) and external (Claude, Cursor) agents */ export class RoopikToolsChannel implements IServerChannel { + // Unified tool services (Phase 4 migration - same as ToolExecutor) + private readonly cdpMonitorService: CDPMonitorService; + private readonly browserToolService: BrowserToolService; + private readonly canvasToolService: CanvasToolService; + private readonly componentToolService: ComponentToolService; + private readonly projectToolService: ProjectToolService; + constructor( private readonly browserViewService: BrowserViewService, private readonly devServerService: DevServerService, private readonly componentService: ComponentService, private readonly canvasService: ICanvasService, private readonly storageService: IRoopikStorageService - ) { } + ) { + // Create CDPMonitorService first (used by BrowserToolService) + this.cdpMonitorService = new CDPMonitorService(browserViewService); + + // Create unified tool services + this.browserToolService = new BrowserToolService(browserViewService, this.cdpMonitorService); + this.canvasToolService = new CanvasToolService(canvasService); + this.componentToolService = new ComponentToolService(componentService, canvasService); + this.projectToolService = new ProjectToolService(devServerService); + } /** * Handle event subscriptions from extensions @@ -225,7 +254,6 @@ export class RoopikToolsChannel implements IServerChannel { return { success: true, data: { - browserViewId: existingBrowserViewId, url: args.url, message: `Navigated existing browser to ${args.url}` } @@ -234,7 +262,6 @@ export class RoopikToolsChannel implements IServerChannel { return { success: true, data: { - browserViewId: existingBrowserViewId, message: 'Browser is already open' } }; @@ -407,7 +434,6 @@ export class RoopikToolsChannel implements IServerChannel { success: true, data: { browserOpen: true, - browserViewId, currentUrl: navState.url, title: navState.title, isLoading: navState.isLoading, @@ -424,60 +450,22 @@ export class RoopikToolsChannel implements IServerChannel { } private async handleScreenshot(): Promise { - const browserViewId = this.browserViewService.getActiveBrowserViewId(); - if (browserViewId === undefined) { - return { - success: false, - error: 'No browser is open. Use browser_open or project_start first.' - }; - } - - // Use takeScreenshotWithMetadata to include viewport dimensions for pixel-perfect clicking - const result = await this.browserViewService.takeScreenshotWithMetadata(browserViewId); - return { - success: true, - data: { - image: result.image, - format: 'data-url', - // Viewport metadata for coordinate scaling - viewport: { - width: result.width, - height: result.height, - devicePixelRatio: result.devicePixelRatio - } - } - }; + // Delegate to unified BrowserToolService + return this.browserToolService.screenshot(); } /** * Close the browser view - * Uses event-based approach to trigger proper cleanup via editor tab close + * Delegates to unified BrowserToolService which handles event-based cleanup */ private async handleBrowserClose(): Promise { - const browserViewId = this.browserViewService.getActiveBrowserViewId(); - if (browserViewId === undefined) { - return { - success: true, - data: { message: 'No browser is open' } - }; - } - - // Fire event for renderer to close the editor tab properly - // This triggers the full cleanup chain: EditorTabInput.dispose -> destroyBrowserNow - // which stops dev server, destroys browser view, and cleans up all state - // DO NOT call destroyBrowserView directly - it leaves zombie editor tabs! - this.browserViewService.requestBrowserClose(); - return { - success: true, - data: { message: 'Browser close request sent. The browser will close shortly.' } - }; + // Delegate to unified BrowserToolService + return this.browserToolService.close(); } /** * Perform browser input actions (click, type, press, scroll, hover, drag) - * - * Coordinate format: 'x,y@WIDTHxHEIGHT' (e.g., '450,203@900x600') - * The coordinates are automatically scaled to the actual viewport size. + * Delegates to unified BrowserToolService. */ private async handleBrowserAction(args: { action: string; @@ -488,248 +476,66 @@ export class RoopikToolsChannel implements IServerChannel { deltaX?: number; deltaY?: number; }): Promise { - const browserViewId = this.browserViewService.getActiveBrowserViewId(); - if (browserViewId === undefined) { - return { - success: false, - error: 'No browser is open. Use browser_open or project_start first.' - }; - } - - const { action, coordinate, text, key, modifiers, deltaX, deltaY } = args; - - try { - // Parse coordinate string: 'x,y@WIDTHxHEIGHT' - let x = 0, y = 0, refWidth: number | undefined, refHeight: number | undefined; - - if (coordinate) { - const match = coordinate.match(/^(\d+),(\d+)(?:@(\d+)x(\d+))?$/); - if (!match) { - return { - success: false, - error: `Invalid coordinate format: '${coordinate}'. Expected 'x,y' or 'x,y@WIDTHxHEIGHT'` - }; - } - x = parseInt(match[1], 10); - y = parseInt(match[2], 10); - if (match[3] && match[4]) { - refWidth = parseInt(match[3], 10); - refHeight = parseInt(match[4], 10); - } - } - - switch (action) { - case 'click': - case 'right_click': - case 'double_click': - case 'hover': - if (!coordinate) { - return { success: false, error: `'${action}' requires 'coordinate' parameter` }; - } - await this.browserViewService.sendMouseEvent( - browserViewId, - action as 'click' | 'right_click' | 'double_click' | 'hover', - x, y, refWidth, refHeight - ); - return { - success: true, - data: { action, coordinate, message: `${action} at (${x}, ${y})` } - }; - - case 'drag': - // For drag, coordinate is start, and we need end coordinates - // Format: coordinate='startX,startY@WIDTHxHEIGHT', deltaX/deltaY for end offset - if (!coordinate || deltaX === undefined || deltaY === undefined) { - return { success: false, error: "'drag' requires 'coordinate' (start) and 'deltaX'/'deltaY' (offset)" }; - } - await this.browserViewService.sendDragEvent( - browserViewId, - x, y, x + deltaX, y + deltaY, - refWidth, refHeight - ); - return { - success: true, - data: { action, from: { x, y }, to: { x: x + deltaX, y: y + deltaY }, message: 'Drag completed' } - }; - - case 'type': - if (!text) { - return { success: false, error: "'type' requires 'text' parameter" }; - } - await this.browserViewService.sendTypeEvent(browserViewId, text); - return { - success: true, - data: { action, text, message: `Typed ${text.length} characters` } - }; - - case 'press': - if (!key) { - return { success: false, error: "'press' requires 'key' parameter" }; - } - await this.browserViewService.sendKeyEvent(browserViewId, key, modifiers); - return { - success: true, - data: { action, key, modifiers, message: `Pressed ${key}` } - }; - - case 'scroll': - if (deltaX === undefined && deltaY === undefined) { - return { success: false, error: "'scroll' requires 'deltaX' and/or 'deltaY' parameters" }; - } - await this.browserViewService.sendScrollEvent( - browserViewId, - deltaX ?? 0, - deltaY ?? 0, - coordinate ? x : undefined, - coordinate ? y : undefined - ); - return { - success: true, - data: { action, deltaX: deltaX ?? 0, deltaY: deltaY ?? 0, message: 'Scrolled' } - }; - - default: - return { - success: false, - error: `Unknown action: '${action}'. Valid actions: click, right_click, double_click, hover, drag, type, press, scroll` - }; - } - } catch (error) { - return { - success: false, - error: `Browser action failed: ${error instanceof Error ? error.message : String(error)}` - }; - } + // Delegate to unified BrowserToolService + return this.browserToolService.actionInput({ + action: args.action as 'click' | 'right_click' | 'double_click' | 'hover' | 'drag' | 'type' | 'press' | 'scroll', + coordinate: args.coordinate, + text: args.text, + key: args.key, + modifiers: args.modifiers, + deltaX: args.deltaX, + deltaY: args.deltaY + }); } private async handleNavigate(args: { url: string }): Promise { - const browserViewId = this.browserViewService.getActiveBrowserViewId(); - if (browserViewId === undefined) { - return { - success: false, - error: 'No browser is open. Use browser_open or project_start first.' - }; - } - - await this.browserViewService.navigate(browserViewId, args.url); - return { - success: true, - data: { url: args.url, message: `Navigated to ${args.url}` } - }; + // Delegate to unified BrowserToolService + return this.browserToolService.navigate(args.url); } private async handleReload(args: { ignoreCache?: boolean }): Promise { - const browserViewId = this.browserViewService.getActiveBrowserViewId(); - if (browserViewId === undefined) { - return { - success: false, - error: 'No browser is open. Use browser_open or project_start first.' - }; - } - - await this.browserViewService.reload(browserViewId, args.ignoreCache); - return { - success: true, - data: { - hardReload: args.ignoreCache || false, - message: args.ignoreCache ? 'Page hard-reloaded (cache cleared)' : 'Page reloaded' - } - }; + // Delegate to unified BrowserToolService + return this.browserToolService.reload(args.ignoreCache); } private async handleExecuteScript(args: { script: string }): Promise { - const browserViewId = this.browserViewService.getActiveBrowserViewId(); - if (browserViewId === undefined) { - return { - success: false, - error: 'No browser is open. Use browser_open or project_start first.' - }; - } - - const result = await this.browserViewService.executeScript(browserViewId, args.script); - return { - success: true, - data: { result } - }; + // Delegate to unified BrowserToolService + return this.browserToolService.executeScript(args.script); } private async handleInspectElement(args: { selector: string; includeInherited?: boolean }): Promise { - const browserViewId = this.browserViewService.getActiveBrowserViewId(); - if (browserViewId === undefined) { - return { - success: false, - error: 'No browser is open. Use browser_open or project_start first.' - }; - } - + // Delegate to unified BrowserToolService with workspace path for CSS source resolution const workspacePath = this.storageService.getWorkspacePath(); - const result = await this.browserViewService.getElementStyles({ - browserViewId, - target: args.selector, - projectRoot: workspacePath, - includeUserAgent: false, - includeInherited: args.includeInherited ?? true - }); - - if (!result.success || !result.data) { - return { - success: false, - error: result.error || 'Element not found' - }; - } - - const data = result.data; - return { - success: true, - data: { - selector: args.selector, - element: { - tag: data.tagName, - classes: data.classes, - componentName: data.componentName, - componentSource: data.htmlSource - }, - matchedRules: data.matchedRules?.map((rule: any) => ({ - selector: rule.selector, - file: rule.file, - location: rule.location, - properties: rule.properties, - specificity: rule.specificity, - origin: rule.origin - })), - inlineStyles: data.inlineStyles, - inheritedStyles: data.inheritedStyles, - properties: data.properties, - cssInJs: data.cssInJs - } - }; + return this.browserToolService.inspectElement( + args.selector, + args.includeInherited ?? true, + workspacePath + ); } // ======================================================================== - // CDP Tool Handlers + // CDP Tool Handlers (Phase 4: Now using unified CDPMonitorService!) // ======================================================================== private async handleGetErrors(args: { limit?: number }): Promise { - // CDP monitoring is handled via the existing MCP infrastructure - // For IPC, we delegate to the same underlying service - // This will be connected once we wire up CDP monitoring to the channel - return { - success: false, - error: 'CDP tools require browser to be open with monitoring enabled. Use project_start first.' - }; + // Delegate to unified BrowserToolService which uses CDPMonitorService + // This now works! Same CDP monitoring shared with external agents (Claude Code, Cursor) + return this.browserToolService.getErrors(args.limit); } - private async handleGetConsoleLogs(args: { limit?: number; type?: string }): Promise { - return { - success: false, - error: 'CDP tools require browser to be open with monitoring enabled. Use project_start first.' - }; + private async handleGetConsoleLogs(args: { limit?: number; types?: string[]; since?: number; clear?: boolean }): Promise { + // Delegate to unified BrowserToolService which uses CDPMonitorService + return this.browserToolService.getConsoleLogs({ + limit: args.limit, + types: args.types, + since: args.since, + clear: args.clear + }); } /** * Set or clear browser viewport override. - * Provide width/height to set a specific size. Call with no args to clear and restore natural size. - * Uses CDP Emulation.setDeviceMetricsOverride / clearDeviceMetricsOverride. + * Delegates to unified BrowserToolService. */ private async handleSetViewport(args?: { width?: number; @@ -737,80 +543,31 @@ export class RoopikToolsChannel implements IServerChannel { deviceScaleFactor?: number; mobile?: boolean; }): Promise { - const browserViewId = this.browserViewService.getActiveBrowserViewId(); - if (browserViewId === undefined) { - return { - success: false, - error: 'No browser is open. Use browser_open or project_start first.' - }; - } - - try { - // Attach debugger if not already attached - await this.browserViewService.attachDebugger(browserViewId); - - // If width/height not provided, clear the viewport override - if (!args?.width || !args?.height) { - await this.browserViewService.sendCDPCommand(browserViewId, 'Emulation.clearDeviceMetricsOverride'); - - return { - success: true, - data: { - width: 0, - height: 0, - deviceScaleFactor: 1, - mobile: false, - message: 'Viewport override cleared - restored to natural browser size' - } - }; - } - - const { width, height, deviceScaleFactor = 1, mobile = false } = args; - - // Set viewport via CDP Emulation - await this.browserViewService.sendCDPCommand(browserViewId, 'Emulation.setDeviceMetricsOverride', { - width, - height, - deviceScaleFactor, - mobile - }); - - return { - success: true, - data: { - width, - height, - deviceScaleFactor, - mobile, - message: `Viewport set to ${width}x${height} (scale: ${deviceScaleFactor}, mobile: ${mobile})` - } - }; - } catch (error) { - return { - success: false, - error: `Failed to set viewport: ${error instanceof Error ? error.message : String(error)}` - }; - } + // Delegate to unified BrowserToolService + // Pass undefined to clear viewport, or object with dimensions to set + return this.browserToolService.setViewport( + args && (args.width || args.height) ? args : undefined + ); } /** * Get network requests captured by CDP. - * Note: This requires CDP Network domain to be enabled and monitoring active. - * For IPC channel, this delegates to the MCP infrastructure which handles CDP monitoring. + * Phase 4: Now delegates to unified BrowserToolService which uses CDPMonitorService. */ - private async handleGetNetworkRequests(_args: { + private async handleGetNetworkRequests(args: { urlFilter?: string; method?: string; statusFilter?: string; limit?: number; }): Promise { - // Network request monitoring requires persistent CDP event listeners - // which are currently managed by the MCP browserExecutor's cdpMonitors. - // For native IPC, we would need to centralize CDP monitoring in BrowserViewService. - return { - success: false, - error: 'Network request monitoring requires CDP monitoring enabled. This feature is available via MCP tools. For native IPC, use browser_execute_script to query performance.getEntries() for network timing.' - }; + // Delegate to unified BrowserToolService which uses CDPMonitorService + // This now works! Same CDP monitoring shared with external agents (Claude Code, Cursor) + return this.browserToolService.getNetworkRequests({ + urlFilter: args.urlFilter, + method: args.method, + statusFilter: args.statusFilter as 'success' | 'error' | 'all' | undefined, + limit: args.limit + }); } // ======================================================================== @@ -818,9 +575,14 @@ export class RoopikToolsChannel implements IServerChannel { // ======================================================================== private async handleGetActiveProject(): Promise { - const runningServer = await this.devServerService.getRunningServer(); + // Delegate to unified ProjectToolService, adapt response for Dio agent format + const result = await this.projectToolService.getActive(); - if (!runningServer) { + if (!result.success) { + return result; + } + + if (!result.data) { return { success: true, data: { @@ -834,12 +596,12 @@ export class RoopikToolsChannel implements IServerChannel { success: true, data: { hasActiveProject: true, - projectPath: runningServer.projectRoot, - url: runningServer.url, - port: runningServer.port, - state: runningServer.state, - framework: runningServer.framework, - frameworkDisplayName: runningServer.frameworkDisplayName + projectPath: result.data.projectRoot, + url: result.data.url, + port: result.data.port, + state: result.data.status, + framework: result.data.framework, + frameworkDisplayName: result.data.frameworkDisplayName } }; } @@ -898,73 +660,31 @@ export class RoopikToolsChannel implements IServerChannel { } // ======================================================================== - // Workspace/Canvas Tool Handlers + // Workspace/Canvas Tool Handlers (Phase 4: Using unified CanvasToolService) // ======================================================================== private async handleListCanvases(args: { nameFilter?: string; sortBy?: string; sortDirection?: string }): Promise { - const canvases = await this.canvasService.listCanvasesAsync({ + // Delegate to unified CanvasToolService + return this.canvasToolService.list({ nameFilter: args.nameFilter, - sortBy: args.sortBy as any, - sortDirection: args.sortDirection as any + sortBy: args.sortBy as 'name' | 'updatedAt' | 'createdAt' | 'componentCount' | undefined, + sortDirection: args.sortDirection as 'asc' | 'desc' | undefined }); - - return { - success: true, - data: { - canvases: canvases.map(c => ({ - id: c.id, - name: c.name, - componentCount: c.componentCount, - description: c.description, - icon: c.icon, - color: c.color, - createdAt: c.createdAt, - updatedAt: c.updatedAt - })), - count: canvases.length - } - }; } private async handleGetActiveCanvas(): Promise { - const focusedCanvasId = await this.canvasService.getFocusedCanvasIdAsync(); + // Delegate to unified CanvasToolService, wrap response for Dio agent format + const result = await this.canvasToolService.getActive(); - if (!focusedCanvasId) { - return { - success: true, - data: { - activeCanvas: null, - message: 'No canvas is currently focused' - } - }; - } - - const canvas = await this.canvasService.getCanvasAsync(focusedCanvasId); - if (!canvas) { - return { - success: true, - data: { - activeCanvas: null, - message: 'Focused canvas not found' - } - }; + if (!result.success) { + return result; } return { success: true, data: { - activeCanvas: { - id: canvas.id, - name: canvas.name, - componentCount: canvas.componentCount, - description: canvas.description, - icon: canvas.icon, - color: canvas.color, - createdAt: canvas.createdAt, - updatedAt: canvas.updatedAt, - isOpen: canvas.isOpen, - isFocused: canvas.isFocused - } + activeCanvas: result.data, + message: result.data ? undefined : 'No canvas is currently focused' } }; } @@ -1070,50 +790,22 @@ export class RoopikToolsChannel implements IServerChannel { } private async handleRemoveComponent(args: { componentId: string; deleteSourceCode?: boolean }): Promise { - await this.componentService.deleteComponent(args.componentId, args.deleteSourceCode); - - return { - success: true, - data: { componentId: args.componentId, deletedSourceCode: args.deleteSourceCode ?? false } - }; + // Delegate to unified ComponentToolService + return this.componentToolService.remove(args.componentId, args.deleteSourceCode); } private async handleGetComponentInfo(args: { componentId: string }): Promise { - const info = await this.componentService.getComponentInfo(args.componentId); - - return { - success: true, - data: { component: info } - }; + // Delegate to unified ComponentToolService + return this.componentToolService.getInfo(args.componentId); } private async handleListComponents(args: { canvasId: string }): Promise { - const components = this.componentService.getComponentsForCanvas(args.canvasId); - - return { - success: true, - data: { - canvasId: args.canvasId, - components: components.map(c => ({ - id: c.id, - componentName: c.componentName, - framework: c.framework, - entryFile: c.entryFile, - buildState: c.buildState - })) - } - }; + // Delegate to unified ComponentToolService + return this.componentToolService.list(args.canvasId); } private async handleRebuildComponent(args: { componentId: string }): Promise { - await this.componentService.rebuildComponent(args.componentId); - - return { - success: true, - data: { - componentId: args.componentId, - message: 'Rebuild queued - use component_get_info to check build state' - } - }; + // Delegate to unified ComponentToolService + return this.componentToolService.rebuild(args.componentId); } } diff --git a/src/vs/workbench/contrib/roopik/electron-main/mcp/executor/browserExecutor.ts b/src/vs/workbench/contrib/roopik/electron-main/mcp/executor/browserExecutor.ts deleted file mode 100644 index 35c37a8cc..000000000 --- a/src/vs/workbench/contrib/roopik/electron-main/mcp/executor/browserExecutor.ts +++ /dev/null @@ -1,1171 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Roopik. All rights reserved. - * Licensed under the MIT License. - *--------------------------------------------------------------------------------------------*/ - -/** - * Browser Tool Executor - * - * Unified execution logic for browser tools. - * Called by both HTTP MCP transport and WebSocket transport. - */ - -import type { BrowserViewService } from '../../projectMode/browserViewService.js'; -import type { IRoopikStorageService } from '../../../common/storage/storageService.js'; -import type { - ToolResult, - BrowserOpenResult, - BrowserScreenshotResult, - BrowserNavigateResult, - BrowserActionResult, - BrowserConsoleLogsResult, - BrowserErrorsResult, - BrowserPerformanceResult, - BrowserStateResult, - ElementInspectionResult, - ScriptExecutionResult, - BrowserViewportResult, - BrowserNetworkRequestsResult, -} from './types.js'; - -// ============================================================================ -// CDP Monitoring State (shared across all calls) -// ============================================================================ - -interface ConsoleLog { - id: string; - timestamp: number; - type: 'log' | 'debug' | 'info' | 'warn' | 'error' | 'dir' | 'dirxml' | 'table' | 'trace' | 'clear' | 'assert'; - message: string; - args?: unknown[]; - location?: string; - stackTrace?: { callFrames: Array<{ url: string; lineNumber: number; columnNumber: number; functionName: string }> }; -} - -interface NetworkRequest { - id: string; - requestId: string; - timestamp: number; - method: string; - url: string; - headers: Record; - postData?: string; -} - -interface NetworkResponse { - id: string; - requestId: string; - timestamp: number; - status: number; - statusText: string; - url: string; - headers: Record; - mimeType?: string; - duration?: number; -} - -interface CDPMonitor { - browserViewId: number; - consoleLogs: ConsoleLog[]; - networkRequests: NetworkRequest[]; - networkResponses: NetworkResponse[]; - requestStartTimes: Map; - cleanupFunctions: Array<() => void>; -} - -const cdpMonitors = new Map(); - -/** - * Cleanup CDP monitoring for a specific browser view. - * Exported for use by BrowserViewService when closing views. - */ -export function cleanupCDPMonitoring(browserViewId: number): void { - const monitor = cdpMonitors.get(browserViewId); - if (monitor) { - for (const cleanup of monitor.cleanupFunctions) { - try { - cleanup(); - } catch { - // Ignore cleanup errors - } - } - cdpMonitors.delete(browserViewId); - } -} - -// ============================================================================ -// Browser Executor Class -// ============================================================================ - -export class BrowserExecutor { - constructor( - private readonly browserViewService: BrowserViewService, - private readonly storageService: IRoopikStorageService - ) {} - - // ========================================================================== - // Browser Open - // ========================================================================== - - async open(url?: string): Promise> { - try { - const browserViewId = this.browserViewService.getActiveBrowserViewId(); - - if (browserViewId === undefined) { - // Browser not open - fire event for renderer to open it - this.browserViewService.requestBrowserOpen(url); - - return { - success: true, - data: { - message: 'Browser open request sent. The browser will open shortly.', - url: url || undefined - } - }; - } - - // Browser is already open - navigate if URL provided - if (url) { - await this.browserViewService.navigate(browserViewId, url); - // Enable CDP monitoring proactively to capture errors from page load - await this.ensureCDPMonitoring(browserViewId); - return { - success: true, - data: { - browserViewId, - url, - message: `Navigated to ${url}` - } - }; - } - - // Enable CDP monitoring even if not navigating (browser already open) - await this.ensureCDPMonitoring(browserViewId); - - return { - success: true, - data: { - browserViewId, - message: 'Browser is already open' - } - }; - } catch (error) { - return { - success: false, - error: error instanceof Error ? error.message : 'Unknown error' - }; - } - } - - // ========================================================================== - // Browser Screenshot - // ========================================================================== - - async screenshot(): Promise> { - try { - const browserViewId = this.browserViewService.getActiveBrowserViewId(); - if (browserViewId === undefined) { - return { - success: false, - error: 'No browser is open. Start a project first with project_start.' - }; - } - - const result = await this.browserViewService.takeScreenshotWithMetadata(browserViewId); - - return { - success: true, - data: { - image: result.image, - format: 'data-url', - viewport: { - width: result.width, - height: result.height, - devicePixelRatio: result.devicePixelRatio - } - } - }; - } catch (error) { - return { - success: false, - error: error instanceof Error ? error.message : 'Unknown error' - }; - } - } - - // ========================================================================== - // Browser Navigate - // ========================================================================== - - async navigate(url: string): Promise> { - try { - const browserViewId = this.browserViewService.getActiveBrowserViewId(); - if (browserViewId === undefined) { - return { - success: false, - error: 'No browser is open. Use browser_open first.' - }; - } - - await this.browserViewService.navigate(browserViewId, url); - - // Enable CDP monitoring proactively to capture errors from page load - await this.ensureCDPMonitoring(browserViewId); - - return { - success: true, - data: { - browserViewId, - url, - message: `Navigated to ${url}` - } - }; - } catch (error) { - return { - success: false, - error: error instanceof Error ? error.message : 'Unknown error' - }; - } - } - - // ========================================================================== - // Browser Reload - // ========================================================================== - - async reload(hardReload?: boolean): Promise> { - try { - const browserViewId = this.browserViewService.getActiveBrowserViewId(); - if (browserViewId === undefined) { - return { - success: false, - error: 'No browser is open.' - }; - } - - await this.browserViewService.reload(browserViewId, hardReload); - - // Enable CDP monitoring proactively to capture errors from reload - await this.ensureCDPMonitoring(browserViewId); - - return { - success: true, - data: { - message: hardReload ? 'Hard reload completed' : 'Reload completed' - } - }; - } catch (error) { - return { - success: false, - error: error instanceof Error ? error.message : 'Unknown error' - }; - } - } - - // ========================================================================== - // Browser Action Input (click, type, scroll, etc.) - // ========================================================================== - - async actionInput(params: { - action: 'click' | 'right_click' | 'double_click' | 'hover' | 'drag' | 'type' | 'press' | 'scroll'; - coordinate?: string; // 'x,y' or 'x,y@WIDTHxHEIGHT' - text?: string; - key?: string; - modifiers?: string[]; - deltaX?: number; - deltaY?: number; - }): Promise> { - try { - const browserViewId = this.browserViewService.getActiveBrowserViewId(); - if (browserViewId === undefined) { - return { - success: false, - error: 'No browser is open.' - }; - } - - const { action, coordinate, text, key, modifiers, deltaX, deltaY } = params; - - // Parse coordinate string: 'x,y@WIDTHxHEIGHT' - let x = 0, y = 0, refWidth: number | undefined, refHeight: number | undefined; - - if (coordinate) { - const match = coordinate.match(/^(\d+),(\d+)(?:@(\d+)x(\d+))?$/); - if (!match) { - return { - success: false, - error: `Invalid coordinate format: '${coordinate}'. Expected 'x,y' or 'x,y@WIDTHxHEIGHT'` - }; - } - x = parseInt(match[1], 10); - y = parseInt(match[2], 10); - if (match[3] && match[4]) { - refWidth = parseInt(match[3], 10); - refHeight = parseInt(match[4], 10); - } - } - - let message = ''; - - switch (action) { - case 'click': - case 'right_click': - case 'double_click': - case 'hover': - if (!coordinate) { - return { success: false, error: `'${action}' requires 'coordinate' parameter` }; - } - await this.browserViewService.sendMouseEvent( - browserViewId, - action as 'click' | 'right_click' | 'double_click' | 'hover', - x, y, refWidth, refHeight - ); - message = `${action} at (${x}, ${y})`; - break; - - case 'drag': - if (!coordinate || deltaX === undefined || deltaY === undefined) { - return { success: false, error: "'drag' requires 'coordinate' and 'deltaX'/'deltaY'" }; - } - await this.browserViewService.sendDragEvent( - browserViewId, - x, y, x + deltaX, y + deltaY, - refWidth, refHeight - ); - message = 'Drag completed'; - break; - - case 'type': - if (!text) { - return { success: false, error: "'type' requires 'text' parameter" }; - } - await this.browserViewService.sendTypeEvent(browserViewId, text); - message = `Typed ${text.length} characters`; - break; - - case 'press': - if (!key) { - return { success: false, error: "'press' requires 'key' parameter" }; - } - await this.browserViewService.sendKeyEvent(browserViewId, key, modifiers); - message = `Pressed ${key}`; - break; - - case 'scroll': - if (deltaX === undefined && deltaY === undefined) { - return { success: false, error: "'scroll' requires 'deltaX' and/or 'deltaY'" }; - } - await this.browserViewService.sendScrollEvent( - browserViewId, - deltaX ?? 0, - deltaY ?? 0, - coordinate ? x : undefined, - coordinate ? y : undefined - ); - message = 'Scrolled'; - break; - - default: - return { success: false, error: `Unknown action: ${action}` }; - } - - return { - success: true, - data: { - browserViewId, - action, - success: true, - message - } - }; - } catch (error) { - return { - success: false, - error: error instanceof Error ? error.message : 'Unknown error' - }; - } - } - - // ========================================================================== - // Browser Execute Script - // ========================================================================== - - async executeScript(script: string): Promise> { - try { - const browserViewId = this.browserViewService.getActiveBrowserViewId(); - if (browserViewId === undefined) { - return { - success: false, - error: 'No browser is open.' - }; - } - - const result = await this.browserViewService.executeScript(browserViewId, script); - - return { - success: true, - data: { - result, - type: typeof result - } - }; - } catch (error) { - return { - success: false, - error: error instanceof Error ? error.message : 'Unknown error' - }; - } - } - - // ========================================================================== - // Browser Inspect Element - // ========================================================================== - - async inspectElement(params: { - selector: string; - includeInherited?: boolean; - }): Promise> { - try { - const browserViewId = this.browserViewService.getActiveBrowserViewId(); - if (browserViewId === undefined) { - return { - success: false, - error: 'No browser is open.' - }; - } - - const workspacePath = this.storageService.getWorkspacePath(); - - const result = await this.browserViewService.getElementStyles({ - browserViewId, - target: params.selector, - projectRoot: workspacePath, - includeUserAgent: false, - includeInherited: params.includeInherited ?? true - }); - - if (!result.success || !result.data) { - return { - success: false, - error: result.error || 'Element not found' - }; - } - - const data = result.data; - - return { - success: true, - data: { - selector: params.selector, - element: { - tag: data.tagName, - classes: data.classes, - componentName: data.componentName, - componentSource: data.htmlSource - }, - matchedRules: data.matchedRules?.map((rule: any) => ({ - selector: rule.selector, - file: rule.file, - location: rule.location, - properties: rule.properties, - specificity: rule.specificity, - origin: rule.origin - })), - inlineStyles: data.inlineStyles, - inheritedStyles: data.inheritedStyles, - properties: data.properties, - cssInJs: data.cssInJs - } - }; - } catch (error) { - return { - success: false, - error: error instanceof Error ? error.message : 'Unknown error' - }; - } - } - - // ========================================================================== - // Browser Get Console Logs (requires CDP) - // ========================================================================== - - async getConsoleLogs(options?: { - types?: string[]; - since?: number; - limit?: number; - clear?: boolean; - }): Promise> { - try { - const browserViewId = this.browserViewService.getActiveBrowserViewId(); - if (browserViewId === undefined) { - return { - success: false, - error: 'No browser is open.' - }; - } - - await this.ensureCDPMonitoring(browserViewId); - const monitor = cdpMonitors.get(browserViewId); - - if (!monitor) { - return { - success: false, - error: 'CDP monitoring not initialized.' - }; - } - - let logs = [...monitor.consoleLogs]; - - // Filter by types - if (options?.types && options.types.length > 0) { - logs = logs.filter(log => options.types!.includes(log.type)); - } - - // Filter by timestamp - if (options?.since) { - logs = logs.filter(log => log.timestamp >= options.since!); - } - - // Apply limit - if (options?.limit) { - logs = logs.slice(-options.limit); - } - - // Clear if requested - if (options?.clear) { - monitor.consoleLogs = []; - } - - return { - success: true, - data: { - logs: logs.map(log => ({ - id: log.id, - timestamp: log.timestamp, - type: log.type, - message: log.message, - args: log.args, - location: log.location - })), - count: logs.length - } - }; - } catch (error) { - return { - success: false, - error: error instanceof Error ? error.message : 'Unknown error' - }; - } - } - - // ========================================================================== - // Browser Get Errors (requires CDP) - Console errors + Network failures - // ========================================================================== - - async getErrors(limit?: number): Promise> { - try { - const browserViewId = this.browserViewService.getActiveBrowserViewId(); - if (browserViewId === undefined) { - return { - success: false, - error: 'No browser is open.' - }; - } - - await this.ensureCDPMonitoring(browserViewId); - const monitor = cdpMonitors.get(browserViewId); - - if (!monitor) { - return { - success: false, - error: 'CDP monitoring not initialized.' - }; - } - - const maxErrors = Math.min(limit || 50, 200); - - // Get console errors (error type only) - const consoleErrors = monitor.consoleLogs - .filter(log => log.type === 'error') - .slice(-maxErrors) - .map(log => ({ - id: log.id, - timestamp: log.timestamp, - source: 'console' as const, - type: 'error' as const, - message: log.message, - location: log.location - })); - - // Get failed network requests (4xx, 5xx, or status 0) - const networkErrors = monitor.networkResponses - .filter(res => res.status >= 400 || res.status === 0) - .slice(-maxErrors) - .map(res => { - const request = monitor.networkRequests.find(req => req.requestId === res.requestId); - return { - id: res.id, - timestamp: res.timestamp, - source: 'network' as const, - type: res.status === 0 ? 'network_failure' : `http_${res.status}`, - message: res.status === 0 ? res.statusText : `${res.status} ${res.statusText}`, - url: res.url || request?.url, - method: request?.method - }; - }); - - // Combine and sort by timestamp (most recent first) - const allErrors = [...consoleErrors, ...networkErrors] - .sort((a, b) => b.timestamp - a.timestamp) - .slice(0, maxErrors); - - return { - success: true, - data: { - errors: allErrors, - count: allErrors.length - } - }; - } catch (error) { - return { - success: false, - error: error instanceof Error ? error.message : 'Unknown error' - }; - } - } - - // ========================================================================== - // Browser Close - // ========================================================================== - - async close(): Promise> { - try { - const browserViewId = this.browserViewService.getActiveBrowserViewId(); - if (browserViewId === undefined) { - return { - success: true, - data: { message: 'No browser was open' } - }; - } - - // Cleanup CDP monitoring - this.cleanupCDPMonitoring(browserViewId); - - // Close browser using event-based approach (renderer handles UI cleanup) - this.browserViewService.requestBrowserClose(); - - return { - success: true, - data: { message: 'Browser close request sent' } - }; - } catch (error) { - return { - success: false, - error: error instanceof Error ? error.message : 'Unknown error' - }; - } - } - - // ========================================================================== - // Browser Get Performance Metrics - // ========================================================================== - - async getPerformance(): Promise> { - try { - const browserViewId = this.browserViewService.getActiveBrowserViewId(); - if (browserViewId === undefined) { - return { - success: false, - error: 'No browser is open.' - }; - } - - // Ensure CDP is attached - await this.browserViewService.attachDebugger(browserViewId); - - // Get performance metrics via CDP - const metricsResult = await this.browserViewService.sendCDPCommand(browserViewId, 'Performance.getMetrics') as { metrics: Array<{ name: string; value: number }> }; - - // Get Web Vitals via script injection - const webVitalsScript = ` - (function() { - const result = {}; - - // LCP (Largest Contentful Paint) - if (window.PerformanceObserver) { - const lcpEntries = performance.getEntriesByType('largest-contentful-paint'); - if (lcpEntries.length > 0) { - result.lcp = lcpEntries[lcpEntries.length - 1].startTime; - } - } - - // CLS (Cumulative Layout Shift) - const layoutShiftEntries = performance.getEntriesByType('layout-shift'); - if (layoutShiftEntries.length > 0) { - result.cls = layoutShiftEntries.reduce((sum, entry) => sum + (entry.hadRecentInput ? 0 : entry.value), 0); - } - - // FCP (First Contentful Paint) - const fcpEntries = performance.getEntriesByType('paint').filter(e => e.name === 'first-contentful-paint'); - if (fcpEntries.length > 0) { - result.fcp = fcpEntries[0].startTime; - } - - // Navigation timing - const nav = performance.getEntriesByType('navigation')[0]; - if (nav) { - result.ttfb = nav.responseStart; - result.domContentLoaded = nav.domContentLoadedEventEnd; - result.load = nav.loadEventEnd; - } - - return result; - })() - `; - - const webVitals = await this.browserViewService.executeScript(browserViewId, webVitalsScript) as Record; - - // Format metrics - const metrics: Record = {}; - if (metricsResult?.metrics) { - for (const metric of metricsResult.metrics) { - metrics[metric.name] = metric.value; - } - } - - return { - success: true, - data: { - webVitals: webVitals || {}, - runtimeMetrics: { - jsHeapUsedSize: metrics['JSHeapUsedSize'], - jsHeapTotalSize: metrics['JSHeapTotalSize'], - domNodes: metrics['Nodes'], - layoutCount: metrics['LayoutCount'], - recalcStyleCount: metrics['RecalcStyleCount'], - layoutDuration: metrics['LayoutDuration'], - recalcStyleDuration: metrics['RecalcStyleDuration'], - scriptDuration: metrics['ScriptDuration'], - taskDuration: metrics['TaskDuration'] - } - } - }; - } catch (error) { - return { - success: false, - error: error instanceof Error ? error.message : 'Unknown error' - }; - } - } - - // ========================================================================== - // Browser Set Viewport (requires CDP) - // Smart design: omit width/height to clear/reset viewport override - // ========================================================================== - - async setViewport(params?: { - width?: number; - height?: number; - deviceScaleFactor?: number; - mobile?: boolean; - }): Promise> { - try { - const browserViewId = this.browserViewService.getActiveBrowserViewId(); - if (browserViewId === undefined) { - return { - success: false, - error: 'No browser is open.' - }; - } - - // Ensure CDP is attached - await this.browserViewService.attachDebugger(browserViewId); - - // If width/height not provided, clear the viewport override (restore to natural size) - if (!params?.width || !params?.height) { - await this.browserViewService.sendCDPCommand(browserViewId, 'Emulation.clearDeviceMetricsOverride'); - - return { - success: true, - data: { - width: 0, - height: 0, - deviceScaleFactor: 1, - mobile: false, - message: 'Viewport override cleared - restored to natural browser size' - } - }; - } - - const { width, height, deviceScaleFactor = 1, mobile = false } = params; - - // Set viewport via CDP Emulation - await this.browserViewService.sendCDPCommand(browserViewId, 'Emulation.setDeviceMetricsOverride', { - width, - height, - deviceScaleFactor, - mobile - }); - - return { - success: true, - data: { - width, - height, - deviceScaleFactor, - mobile, - message: `Viewport set to ${width}x${height} (scale: ${deviceScaleFactor}, mobile: ${mobile})` - } - }; - } catch (error) { - return { - success: false, - error: error instanceof Error ? error.message : 'Unknown error' - }; - } - } - - // ========================================================================== - // Browser Get Network Requests (requires CDP) - // ========================================================================== - - async getNetworkRequests(options?: { - urlFilter?: string; - method?: string; - statusFilter?: 'success' | 'error' | 'all'; - limit?: number; - }): Promise> { - try { - const browserViewId = this.browserViewService.getActiveBrowserViewId(); - if (browserViewId === undefined) { - return { - success: false, - error: 'No browser is open.' - }; - } - - await this.ensureCDPMonitoring(browserViewId); - const monitor = cdpMonitors.get(browserViewId); - - if (!monitor) { - return { - success: false, - error: 'CDP monitoring not initialized.' - }; - } - - // Combine requests and responses - let requests = monitor.networkRequests.map(req => { - const response = monitor.networkResponses.find(res => res.requestId === req.requestId); - return { - id: req.id, - method: req.method, - url: req.url, - status: response?.status, - statusText: response?.statusText, - mimeType: response?.mimeType, - duration: response?.duration, - timestamp: req.timestamp, - requestHeaders: req.headers, - responseHeaders: response?.headers - }; - }); - - // Apply URL filter - if (options?.urlFilter) { - const filter = options.urlFilter.toLowerCase(); - requests = requests.filter(r => r.url.toLowerCase().includes(filter)); - } - - // Apply method filter - if (options?.method) { - const method = options.method.toUpperCase(); - requests = requests.filter(r => r.method.toUpperCase() === method); - } - - // Apply status filter - if (options?.statusFilter && options.statusFilter !== 'all') { - if (options.statusFilter === 'success') { - requests = requests.filter(r => r.status && r.status >= 200 && r.status < 400); - } else if (options.statusFilter === 'error') { - requests = requests.filter(r => !r.status || r.status >= 400 || r.status === 0); - } - } - - // Apply limit - const limit = Math.min(options?.limit || 100, 500); - requests = requests.slice(-limit); - - return { - success: true, - data: { - requests, - count: requests.length - } - }; - } catch (error) { - return { - success: false, - error: error instanceof Error ? error.message : 'Unknown error' - }; - } - } - - // ========================================================================== - // Browser Get State - // ========================================================================== - - async getState(): Promise> { - try { - const browserViewId = this.browserViewService.getActiveBrowserViewId(); - - // Get browser state if open - let currentUrl: string | undefined; - let title: string | undefined; - let isLoading = false; - let devServerRunning = false; - - if (browserViewId !== undefined) { - // Get URL and title via script execution - const stateScript = `({ url: window.location.href, title: document.title })`; - const state = await this.browserViewService.executeScript(browserViewId, stateScript) as { url: string; title: string }; - currentUrl = state?.url; - title = state?.title; - devServerRunning = true; - } - - return { - success: true, - data: { - browserOpen: browserViewId !== undefined, - browserViewId, - currentUrl, - title, - isLoading, - devServerRunning, - message: browserViewId !== undefined - ? `Browser is open at ${currentUrl}` - : 'No browser is currently open. Use browser_open or project_start to open one.' - } - }; - } catch (error) { - return { - success: false, - error: error instanceof Error ? error.message : 'Unknown error' - }; - } - } - - // ========================================================================== - // CDP Monitoring Helpers - // ========================================================================== - - private async ensureCDPMonitoring(browserViewId: number): Promise { - // Clean up stale monitors - for (const [monitoredId] of cdpMonitors) { - if (monitoredId !== browserViewId) { - this.cleanupCDPMonitoring(monitoredId); - } - } - - if (cdpMonitors.has(browserViewId)) { - return; - } - - const monitor: CDPMonitor = { - browserViewId, - consoleLogs: [], - networkRequests: [], - networkResponses: [], - requestStartTimes: new Map(), - cleanupFunctions: [] - }; - - await this.browserViewService.attachDebugger(browserViewId); - await this.browserViewService.sendCDPCommand(browserViewId, 'Runtime.enable'); - await this.browserViewService.sendCDPCommand(browserViewId, 'Network.enable'); - await this.browserViewService.sendCDPCommand(browserViewId, 'Page.enable'); - - // Console logging - const consoleCleanup = this.browserViewService.onCDPEvent(browserViewId, (method: string, params: unknown) => { - if (method === 'Runtime.consoleAPICalled') { - this.handleConsoleMessage(monitor, params as any); - } else if (method === 'Runtime.exceptionThrown') { - this.handleException(monitor, params as any); - } - }); - monitor.cleanupFunctions.push(consoleCleanup); - - // Network monitoring - const networkCleanup = this.browserViewService.onCDPEvent(browserViewId, (method: string, params: unknown) => { - if (method === 'Network.requestWillBeSent') { - this.handleNetworkRequest(monitor, params as any); - } else if (method === 'Network.responseReceived') { - this.handleNetworkResponse(monitor, params as any); - } else if (method === 'Network.loadingFailed') { - this.handleNetworkFailure(monitor, params as any); - } - }); - monitor.cleanupFunctions.push(networkCleanup); - - cdpMonitors.set(browserViewId, monitor); - } - - private cleanupCDPMonitoring(browserViewId: number): void { - const monitor = cdpMonitors.get(browserViewId); - if (monitor) { - for (const cleanup of monitor.cleanupFunctions) { - try { - cleanup(); - } catch { - // Ignore cleanup errors - } - } - cdpMonitors.delete(browserViewId); - } - } - - private handleConsoleMessage(monitor: CDPMonitor, params: { - type: string; - args: Array<{ type: string; value?: any; unserializableValue?: string; description?: string }>; - timestamp: number; - stackTrace?: { callFrames: Array<{ url: string; lineNumber: number; columnNumber: number; functionName: string }> }; - }): void { - const { type, args, timestamp, stackTrace } = params; - - const messages = args.map(arg => { - if (arg.value !== undefined) { - return typeof arg.value === 'object' ? JSON.stringify(arg.value) : String(arg.value); - } - if (arg.unserializableValue) return arg.unserializableValue; - if (arg.description) return arg.description; - return '[Object]'; - }); - - const location = stackTrace?.callFrames?.[0] - ? `${stackTrace.callFrames[0].url}:${stackTrace.callFrames[0].lineNumber}:${stackTrace.callFrames[0].columnNumber}` - : undefined; - - const log: ConsoleLog = { - id: `console-${Date.now()}-${Math.random()}`, - timestamp: timestamp * 1000, - type: type as ConsoleLog['type'], - message: messages.join(' '), - args: args.map(a => a.value), - location, - stackTrace - }; - - monitor.consoleLogs.push(log); - if (monitor.consoleLogs.length > 1000) { - monitor.consoleLogs.shift(); - } - } - - private handleException(monitor: CDPMonitor, params: { - timestamp: number; - exceptionDetails: { - text: string; - url?: string; - lineNumber?: number; - columnNumber?: number; - stackTrace?: { callFrames: Array<{ url: string; lineNumber: number; columnNumber: number; functionName: string }> }; - }; - }): void { - const { timestamp, exceptionDetails } = params; - - const location = exceptionDetails.url && exceptionDetails.lineNumber !== undefined - ? `${exceptionDetails.url}:${exceptionDetails.lineNumber}:${exceptionDetails.columnNumber}` - : undefined; - - // Log exceptions as console errors (matches browserTools.ts behavior) - const log: ConsoleLog = { - id: `exception-${Date.now()}-${Math.random()}`, - timestamp: timestamp * 1000, - type: 'error', - message: exceptionDetails.text, - location, - stackTrace: exceptionDetails.stackTrace - }; - - monitor.consoleLogs.push(log); - if (monitor.consoleLogs.length > 1000) { - monitor.consoleLogs.shift(); - } - } - - private handleNetworkRequest(monitor: CDPMonitor, params: { - requestId: string; - request: { url: string; method: string; headers: Record; postData?: string }; - timestamp: number; - }): void { - const { requestId, request, timestamp } = params; - - const networkRequest: NetworkRequest = { - id: `req-${Date.now()}-${Math.random()}`, - requestId, - timestamp: timestamp * 1000, - method: request.method, - url: request.url, - headers: request.headers || {}, - postData: request.postData - }; - - monitor.networkRequests.push(networkRequest); - monitor.requestStartTimes.set(requestId, timestamp); - - if (monitor.networkRequests.length > 500) { - monitor.networkRequests.shift(); - } - } - - private handleNetworkResponse(monitor: CDPMonitor, params: { - requestId: string; - response: { url: string; status: number; statusText: string; headers: Record; mimeType?: string }; - timestamp: number; - }): void { - const { requestId, response, timestamp } = params; - - const startTime = monitor.requestStartTimes.get(requestId); - const duration = startTime ? (timestamp - startTime) * 1000 : undefined; - - const networkResponse: NetworkResponse = { - id: `res-${Date.now()}-${Math.random()}`, - requestId, - timestamp: timestamp * 1000, - status: response.status, - statusText: response.statusText, - url: response.url, - headers: response.headers || {}, - mimeType: response.mimeType, - duration - }; - - monitor.networkResponses.push(networkResponse); - if (monitor.networkResponses.length > 500) { - monitor.networkResponses.shift(); - } - } - - private handleNetworkFailure(monitor: CDPMonitor, params: { - requestId: string; - timestamp: number; - errorText: string; - }): void { - const { requestId, timestamp, errorText } = params; - - const networkResponse: NetworkResponse = { - id: `fail-${Date.now()}-${Math.random()}`, - requestId, - timestamp: timestamp * 1000, - status: 0, - statusText: errorText, - url: '', - headers: {} - }; - - monitor.networkResponses.push(networkResponse); - if (monitor.networkResponses.length > 500) { - monitor.networkResponses.shift(); - } - } -} diff --git a/src/vs/workbench/contrib/roopik/electron-main/mcp/executor/canvasExecutor.ts b/src/vs/workbench/contrib/roopik/electron-main/mcp/executor/canvasExecutor.ts deleted file mode 100644 index 83de77a84..000000000 --- a/src/vs/workbench/contrib/roopik/electron-main/mcp/executor/canvasExecutor.ts +++ /dev/null @@ -1,363 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Roopik. All rights reserved. - * Licensed under the MIT License. - *--------------------------------------------------------------------------------------------*/ - -/** - * Canvas & Component Executor - * - * Unified execution logic for canvas and component tools. - * Called by both HTTP MCP transport and WebSocket transport. - */ - -import { resolve, normalize } from '../../../../../../base/common/path.js'; -import type { ICanvasService } from '../../../common/canvas/canvasService.js'; -import type { ComponentService } from '../../component/componentService.js'; -import type { ToolResult, CanvasListResult, ComponentListResult } from './types.js'; - -// ============================================================================ -// Helper Functions -// ============================================================================ - -/** - * Resolve a path that may be relative or absolute. - */ -function resolvePath(inputPath: string, workspacePath: string): string { - let sanitizedInput = inputPath; - if (process.platform !== 'win32') { - sanitizedInput = inputPath.replace(/\\/g, '/'); - } - const normalizedInput = normalize(sanitizedInput); - return resolve(workspacePath, normalizedInput); -} - -// ============================================================================ -// Canvas Executor Class -// ============================================================================ - -export class CanvasExecutor { - constructor( - private readonly canvasService: ICanvasService, - private readonly componentService: ComponentService - ) {} - - // ========================================================================== - // Canvas List - // ========================================================================== - - async listCanvases(params?: { - nameFilter?: string; - sortBy?: 'name' | 'updatedAt' | 'createdAt' | 'componentCount'; - sortDirection?: 'asc' | 'desc'; - }): Promise> { - try { - const canvases = await this.canvasService.listCanvasesAsync(params); - - return { - success: true, - data: { - canvases: canvases.map(c => ({ - id: c.id, - name: c.name, - componentCount: c.componentCount, - description: c.description - })), - count: canvases.length - } - }; - } catch (error) { - return { - success: false, - error: error instanceof Error ? error.message : 'Unknown error' - }; - } - } - - // ========================================================================== - // Canvas Get Active - // ========================================================================== - - async getActiveCanvas(): Promise> { - try { - const focusedCanvasId = await this.canvasService.getFocusedCanvasIdAsync(); - - if (!focusedCanvasId) { - return { - success: true, - data: { - activeCanvas: null, - message: 'No canvas is currently focused' - } - }; - } - - const canvas = await this.canvasService.getCanvasAsync(focusedCanvasId); - if (!canvas) { - return { - success: true, - data: { - activeCanvas: null, - message: 'Focused canvas not found' - } - }; - } - - return { - success: true, - data: { - activeCanvas: { - id: canvas.id, - name: canvas.name, - componentCount: canvas.componentCount, - description: canvas.description, - icon: canvas.icon, - color: canvas.color, - createdAt: canvas.createdAt, - updatedAt: canvas.updatedAt, - isOpen: canvas.isOpen, - isFocused: canvas.isFocused - } - } - }; - } catch (error) { - return { - success: false, - error: error instanceof Error ? error.message : 'Unknown error' - }; - } - } - - // ========================================================================== - // Canvas Create - // ========================================================================== - - async createCanvas(name: string): Promise> { - try { - const result = await this.canvasService.createCanvas(name); - - return { - success: true, - data: { - canvasId: result.canvasId, - isNew: result.isNew, - canvas: { - id: result.canvas.id, - name: result.canvas.name, - componentCount: result.canvas.componentCount, - description: result.canvas.description, - icon: result.canvas.icon, - color: result.canvas.color, - createdAt: result.canvas.createdAt, - updatedAt: result.canvas.updatedAt - } - } - }; - } catch (error) { - return { - success: false, - error: error instanceof Error ? error.message : 'Unknown error' - }; - } - } - - // ========================================================================== - // Component Add - // ========================================================================== - - async addComponent(params: { - canvasId?: string; - folderPath: string; - name?: string; - entryFile?: string; - framework?: 'react' | 'vue' | 'svelte' | 'solid' | 'preact' | 'html'; - }): Promise> { - try { - const workspacePath = this.canvasService.getWorkspacePath(); - if (!workspacePath) { - return { - success: false, - error: 'No workspace is open. Please open a folder/workspace first.' - }; - } - - const resolvedPath = resolvePath(params.folderPath, workspacePath); - - const component = await this.componentService.addComponent({ - folderPath: resolvedPath, - canvasId: params.canvasId, - componentName: params.name, - entryFile: params.entryFile, - framework: params.framework, - origin: 'ai' - }); - - return { - success: true, - data: { - component: { - id: component.id, - canvasId: component.canvasId, - folderPath: component.folderPath, - entryFile: component.entryFile, - framework: component.framework, - buildState: component.buildState, - componentName: component.componentName, - origin: component.origin, - createdAt: component.createdAt, - updatedAt: component.updatedAt - } - } - }; - } catch (error) { - return { - success: false, - error: error instanceof Error ? error.message : 'Unknown error' - }; - } - } - - // ========================================================================== - // Component Add Batch - // ========================================================================== - - async addComponentBatch(components: Array<{ - canvasId?: string; - folderPath: string; - name?: string; - entryFile?: string; - framework?: 'react' | 'vue' | 'svelte' | 'solid' | 'preact' | 'html'; - }>): Promise> { - try { - const workspacePath = this.canvasService.getWorkspacePath(); - - const requests = components.map(c => ({ - folderPath: resolvePath(c.folderPath, workspacePath), - canvasId: c.canvasId, - componentName: c.name, - entryFile: c.entryFile, - framework: c.framework, - origin: 'ai' as const - })); - - const created = await this.componentService.addComponents(requests); - - return { - success: true, - data: { - count: created.length, - components: created.map(c => ({ - id: c.id, - canvasId: c.canvasId, - componentName: c.componentName, - folderPath: c.folderPath, - framework: c.framework, - buildState: c.buildState - })) - } - }; - } catch (error) { - return { - success: false, - error: error instanceof Error ? error.message : 'Unknown error' - }; - } - } - - // ========================================================================== - // Component Remove - // ========================================================================== - - async removeComponent(componentId: string, deleteSourceCode?: boolean): Promise> { - try { - await this.componentService.deleteComponent(componentId, deleteSourceCode); - - return { - success: true, - data: { - componentId, - deletedSourceCode: deleteSourceCode ?? false - } - }; - } catch (error) { - return { - success: false, - error: error instanceof Error ? error.message : 'Unknown error' - }; - } - } - - // ========================================================================== - // Component Get Info - // ========================================================================== - - async getComponentInfo(componentId: string): Promise> { - try { - const info = await this.componentService.getComponentInfo(componentId); - - return { - success: true, - data: { - component: info - } - }; - } catch (error) { - return { - success: false, - error: error instanceof Error ? error.message : 'Unknown error' - }; - } - } - - // ========================================================================== - // Component List - // ========================================================================== - - async listComponents(canvasId: string): Promise> { - try { - const components = this.componentService.getComponentsForCanvas(canvasId); - - return { - success: true, - data: { - components: components.map(c => ({ - id: c.id, - name: c.componentName, - path: c.folderPath, - canvasId: c.canvasId, - // buildState can be string or { status: string, error: string } - status: (typeof c.buildState === 'string' ? c.buildState : c.buildState.status) as 'pending' | 'building' | 'ready' | 'error' - })), - count: components.length - } - }; - } catch (error) { - return { - success: false, - error: error instanceof Error ? error.message : 'Unknown error' - }; - } - } - - // ========================================================================== - // Component Rebuild - // ========================================================================== - - async rebuildComponent(componentId: string): Promise> { - try { - await this.componentService.rebuildComponent(componentId); - - return { - success: true, - data: { - componentId, - message: 'Rebuild queued - use getComponentInfo to check build state' - } - }; - } catch (error) { - return { - success: false, - error: error instanceof Error ? error.message : 'Unknown error' - }; - } - } -} diff --git a/src/vs/workbench/contrib/roopik/electron-main/mcp/executor/index.ts b/src/vs/workbench/contrib/roopik/electron-main/mcp/executor/index.ts index 89d116fa6..9c9e195ef 100644 --- a/src/vs/workbench/contrib/roopik/electron-main/mcp/executor/index.ts +++ b/src/vs/workbench/contrib/roopik/electron-main/mcp/executor/index.ts @@ -6,8 +6,11 @@ /** * Tool Executor Module * - * Unified tool execution layer for all MCP transports. - * Both HTTP MCP and WebSocket MCP call these executors. + * Central dispatcher for MCP tool execution. + * Routes tool calls to unified tool services. + * + * NOTE: Old executor classes (BrowserExecutor, CanvasExecutor, ProjectExecutor) + * have been removed. Tool implementation now lives in electron-main/tools/*. */ // Types @@ -36,9 +39,6 @@ export type { ProjectStopResult, } from './types.js'; -// Executors +// Main Executor export { ToolExecutor } from './toolExecutor.js'; export type { ToolCall, ToolCallResult } from './toolExecutor.js'; -export { BrowserExecutor } from './browserExecutor.js'; -export { CanvasExecutor } from './canvasExecutor.js'; -export { ProjectExecutor } from './projectExecutor.js'; diff --git a/src/vs/workbench/contrib/roopik/electron-main/mcp/executor/projectExecutor.ts b/src/vs/workbench/contrib/roopik/electron-main/mcp/executor/projectExecutor.ts deleted file mode 100644 index 22c466aa0..000000000 --- a/src/vs/workbench/contrib/roopik/electron-main/mcp/executor/projectExecutor.ts +++ /dev/null @@ -1,157 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Roopik. All rights reserved. - * Licensed under the MIT License. - *--------------------------------------------------------------------------------------------*/ - -/** - * Project Executor - * - * Unified execution logic for project/dev server tools. - * Called by both HTTP MCP transport and WebSocket transport. - */ - -import { resolve, normalize } from '../../../../../../base/common/path.js'; -import type { DevServerService } from '../../projectMode/devServer/devServerService.js'; -import type { BrowserViewService } from '../../projectMode/browserViewService.js'; -import type { IRoopikStorageService } from '../../../common/storage/storageService.js'; -import type { ToolResult, ProjectStartResult, ProjectStopResult, ProjectServerInfo } from './types.js'; - -// ============================================================================ -// Helper Functions -// ============================================================================ - -/** - * Resolve a path that may be relative or absolute. - */ -function resolvePath(inputPath: string, workspacePath: string): string { - let sanitizedInput = inputPath; - if (process.platform !== 'win32') { - sanitizedInput = inputPath.replace(/\\/g, '/'); - } - const normalizedInput = normalize(sanitizedInput); - return resolve(workspacePath, normalizedInput); -} - -// ============================================================================ -// Project Executor Class -// ============================================================================ - -export class ProjectExecutor { - constructor( - private readonly devServerService: DevServerService, - private readonly browserViewService: BrowserViewService, - private readonly storageService: IRoopikStorageService - ) {} - - // ========================================================================== - // Project Get Active - // ========================================================================== - - async getActiveProject(): Promise> { - try { - const runningServer = await this.devServerService.getRunningServer(); - - if (!runningServer) { - return { - success: true, - data: { - hasActiveProject: false, - message: 'No project is currently running. Use project_start to start one.' - } - }; - } - - return { - success: true, - data: { - hasActiveProject: true, - project: { - url: runningServer.url, - projectRoot: runningServer.projectRoot, - framework: runningServer.framework, - status: runningServer.state as 'starting' | 'running' | 'stopping' | 'stopped' - } - } - }; - } catch (error) { - return { - success: false, - error: error instanceof Error ? error.message : 'Unknown error' - }; - } - } - - // ========================================================================== - // Project Start - // ========================================================================== - - async startProject(projectPath: string, port?: number): Promise> { - try { - // Use getWorkspaceRootPath() - NOT getWorkspacePath() which returns .roopik/ folder - const workspacePath = this.storageService.getWorkspaceRootPath(); - const resolvedPath = resolvePath(projectPath, workspacePath); - - const url = await this.devServerService.startServer({ - projectRoot: resolvedPath, - port - }); - - // Get framework info - const runningServer = await this.devServerService.getRunningServer(); - - return { - success: true, - data: { - url, - projectRoot: resolvedPath, - framework: runningServer?.framework, - message: `Dev server started at ${url}. Browser is now showing the project.` - } - }; - } catch (error) { - return { - success: false, - error: error instanceof Error ? error.message : 'Unknown error' - }; - } - } - - // ========================================================================== - // Project Stop - // ========================================================================== - - async stopProject(): Promise> { - try { - const runningServer = await this.devServerService.getRunningServer(); - - if (!runningServer) { - return { - success: true, - data: { - stopped: false, - message: 'No project is currently running' - } - }; - } - - const projectPath = runningServer.projectRoot; - await this.devServerService.stopServer(projectPath); - - // Close browser editor UI (fires event to renderer process) - this.browserViewService.requestBrowserClose(); - - return { - success: true, - data: { - stopped: true, - message: 'Dev server stopped and browser closed' - } - }; - } catch (error) { - return { - success: false, - error: error instanceof Error ? error.message : 'Unknown error' - }; - } - } -} diff --git a/src/vs/workbench/contrib/roopik/electron-main/mcp/executor/toolExecutor.ts b/src/vs/workbench/contrib/roopik/electron-main/mcp/executor/toolExecutor.ts index a59905cc5..d2f3da100 100644 --- a/src/vs/workbench/contrib/roopik/electron-main/mcp/executor/toolExecutor.ts +++ b/src/vs/workbench/contrib/roopik/electron-main/mcp/executor/toolExecutor.ts @@ -55,14 +55,17 @@ export class ToolExecutor { private readonly canvasToolService: CanvasToolService; private readonly componentToolService: ComponentToolService; private readonly projectToolService: ProjectToolService; + private readonly storageService: IRoopikStorageService; constructor( browserViewService: BrowserViewService, - _storageService: IRoopikStorageService, // Kept for backward compatibility, no longer used + storageService: IRoopikStorageService, canvasService: ICanvasService, componentService: ComponentService, devServerService: DevServerService ) { + this.storageService = storageService; + // Create CDPMonitorService first (used by BrowserToolService) this.cdpMonitorService = new CDPMonitorService(browserViewService); @@ -185,7 +188,8 @@ export class ToolExecutor { case 'browser_inspect_element': return this.browserToolService.inspectElement( params.selector as string, - params.includeInherited as boolean | undefined + params.includeInherited as boolean | undefined, + this.storageService.getWorkspacePath() ); case 'browser_get_errors': diff --git a/src/vs/workbench/contrib/roopik/electron-main/mcp/executor/types.ts b/src/vs/workbench/contrib/roopik/electron-main/mcp/executor/types.ts index 2492dd324..4ab7386ab 100644 --- a/src/vs/workbench/contrib/roopik/electron-main/mcp/executor/types.ts +++ b/src/vs/workbench/contrib/roopik/electron-main/mcp/executor/types.ts @@ -35,9 +35,10 @@ export interface ImageResult { /** * Browser open result + * Note: browserViewId deliberately excluded - internal implementation detail + * that agents never use (they call tools by name, not by ID) */ export interface BrowserOpenResult { - browserViewId?: number; url?: string; message: string; } @@ -57,7 +58,6 @@ export interface BrowserScreenshotResult extends ImageResult { * Browser navigation result */ export interface BrowserNavigateResult { - browserViewId: number; url: string; message: string; } @@ -66,9 +66,7 @@ export interface BrowserNavigateResult { * Browser action result */ export interface BrowserActionResult { - browserViewId: number; action: string; - success: boolean; message: string; } @@ -136,11 +134,9 @@ export interface BrowserPerformanceResult { /** * Browser state result - returns current browser status - * Note: Use tools/list for available tools, not this result */ export interface BrowserStateResult { browserOpen: boolean; - browserViewId?: number; currentUrl?: string; title?: string; isLoading?: boolean; @@ -224,12 +220,16 @@ export interface BrowserNetworkRequestsResult { /** * Canvas info + * Note: icon/color deliberately excluded - they're UI-only fields + * that provide no value to AI agents (context pollution) */ export interface CanvasInfo { id: string; name: string; description?: string; componentCount: number; + createdAt?: number; + updatedAt?: number; } /** @@ -265,11 +265,14 @@ export interface ComponentListResult { /** * Project server info + * Note: frameworkDisplayName deliberately excluded - it's just + * UI-friendly text (e.g., "Next.js" vs "next") that agents don't need */ export interface ProjectServerInfo { url?: string; // May be undefined during 'starting' state projectRoot: string; framework?: string; + port?: number; status: 'starting' | 'running' | 'stopping' | 'stopped'; } diff --git a/src/vs/workbench/contrib/roopik/electron-main/projectMode/browserViewService.ts b/src/vs/workbench/contrib/roopik/electron-main/projectMode/browserViewService.ts index fe1de49d2..65f85ebb2 100644 --- a/src/vs/workbench/contrib/roopik/electron-main/projectMode/browserViewService.ts +++ b/src/vs/workbench/contrib/roopik/electron-main/projectMode/browserViewService.ts @@ -15,7 +15,7 @@ import { Disposable } from '../../../../../base/common/lifecycle.js'; import { CDPCssService } from './cssResolvers/cdpCssService.js'; import { StyleSourceOrchestrator } from './cssResolvers/styleSourceOrchestrator.js'; import contextMenu from 'electron-context-menu'; -import { cleanupCDPMonitoring } from '../mcp/executor/browserExecutor.js'; +import { cleanupCDPMonitoring } from '../tools/cdpMonitorService.js'; import { ILoggerService } from '../../../../../platform/log/common/log.js'; import { getRoopikLogger } from '../../common/roopikLogger.js'; diff --git a/src/vs/workbench/contrib/roopik/electron-main/tools/browserToolService.ts b/src/vs/workbench/contrib/roopik/electron-main/tools/browserToolService.ts index 1de1e7743..c37da78d7 100644 --- a/src/vs/workbench/contrib/roopik/electron-main/tools/browserToolService.ts +++ b/src/vs/workbench/contrib/roopik/electron-main/tools/browserToolService.ts @@ -67,7 +67,6 @@ export class BrowserToolService { return { success: true, data: { - browserViewId, url, message: `Navigated to ${url}` } @@ -77,7 +76,6 @@ export class BrowserToolService { return { success: true, data: { - browserViewId, message: 'Browser is already open' } }; @@ -176,7 +174,6 @@ export class BrowserToolService { return { success: true, data: { - browserViewId, url, message: `Navigated to ${url}` } @@ -209,7 +206,6 @@ export class BrowserToolService { return { success: true, data: { - browserViewId, url: currentUrl, message: `Reloaded page${ignoreCache ? ' (cache ignored)' : ''}` } @@ -294,9 +290,7 @@ export class BrowserToolService { return { success: true, data: { - browserViewId, action, - success: true, message: `Executed ${action} action` } }; @@ -341,7 +335,7 @@ export class BrowserToolService { // Element Inspection // ========================================================================== - async inspectElement(selector: string, includeInherited?: boolean): Promise> { + async inspectElement(selector: string, includeInherited?: boolean, projectRoot?: string): Promise> { try { const browserViewId = this.browserViewService.getActiveBrowserViewId(); @@ -349,14 +343,10 @@ export class BrowserToolService { return { success: false, error: 'No browser is open. Use browser_open first.' }; } - // Get project root - for now use empty string (CSS source resolution still works) - // TODO: Add DevServerService dependency to get actual project root - const projectRoot = ''; - const result = await this.browserViewService.getElementStyles({ browserViewId, target: selector, - projectRoot, + projectRoot: projectRoot ?? '', includeInherited: includeInherited ?? false, includeUserAgent: false }); @@ -603,7 +593,6 @@ export class BrowserToolService { success: true, data: { browserOpen: true, - browserViewId, currentUrl: navState.url, title: navState.title, isLoading: navState.isLoading, diff --git a/src/vs/workbench/contrib/roopik/electron-main/tools/canvasToolService.ts b/src/vs/workbench/contrib/roopik/electron-main/tools/canvasToolService.ts index 2b74be384..24716cb8e 100644 --- a/src/vs/workbench/contrib/roopik/electron-main/tools/canvasToolService.ts +++ b/src/vs/workbench/contrib/roopik/electron-main/tools/canvasToolService.ts @@ -67,7 +67,9 @@ export class CanvasToolService { id: c.id, name: c.name, description: c.description, - componentCount: c.componentCount || 0 + componentCount: c.componentCount || 0, + createdAt: c.createdAt, + updatedAt: c.updatedAt })); return { @@ -117,7 +119,9 @@ export class CanvasToolService { id: canvas.id, name: canvas.name, description: canvas.description, - componentCount: canvas.componentCount || 0 + componentCount: canvas.componentCount || 0, + createdAt: canvas.createdAt, + updatedAt: canvas.updatedAt } }; } catch (error) { @@ -145,7 +149,9 @@ export class CanvasToolService { id: result.canvas.id, name: result.canvas.name, description: result.canvas.description, - componentCount: result.canvas.componentCount || 0 + componentCount: result.canvas.componentCount || 0, + createdAt: result.canvas.createdAt, + updatedAt: result.canvas.updatedAt } }; } @@ -159,7 +165,9 @@ export class CanvasToolService { id: result.canvasId, name: canvas?.name || name, description: canvas?.description, - componentCount: 0 + componentCount: 0, + createdAt: canvas?.createdAt, + updatedAt: canvas?.updatedAt } }; } catch (error) { diff --git a/src/vs/workbench/contrib/roopik/electron-main/tools/cdpMonitorService.ts b/src/vs/workbench/contrib/roopik/electron-main/tools/cdpMonitorService.ts index 43471495d..263ba8ddb 100644 --- a/src/vs/workbench/contrib/roopik/electron-main/tools/cdpMonitorService.ts +++ b/src/vs/workbench/contrib/roopik/electron-main/tools/cdpMonitorService.ts @@ -10,10 +10,23 @@ * Single source of truth for console logs, errors, and network requests. * * Used by both WebSocket MCP (external agents) and Native IPC (Dio agent). + * + * IMPORTANT: Uses module-level state so ALL instances share the same data. + * This ensures both Claude Code and Dio agent see the same console logs/errors. */ import type { BrowserViewService } from '../projectMode/browserViewService.js'; +// ============================================================================ +// Module-Level Shared State +// ============================================================================ + +/** + * Shared monitors map - all CDPMonitorService instances operate on this. + * This ensures console logs captured by one agent are visible to all agents. + */ +const sharedMonitors = new Map(); + // ============================================================================ // CDP Monitor Types // ============================================================================ @@ -59,13 +72,36 @@ interface CDPMonitor { cleanupFunctions: Array<() => void>; } +// ============================================================================ +// Exported Cleanup Function +// ============================================================================ + +/** + * Cleanup CDP monitoring for a specific browser view. + * Exported for use by BrowserViewService when destroying browser views. + * + * This is a module-level function that operates on the shared state, + * so it works regardless of which CDPMonitorService instance was used. + */ +export function cleanupCDPMonitoring(browserViewId: number): void { + const monitor = sharedMonitors.get(browserViewId); + if (monitor) { + for (const cleanup of monitor.cleanupFunctions) { + try { + cleanup(); + } catch { + // Ignore cleanup errors + } + } + sharedMonitors.delete(browserViewId); + } +} + // ============================================================================ // CDP Monitor Service // ============================================================================ export class CDPMonitorService { - private readonly monitors = new Map(); - constructor( private readonly browserViewService: BrowserViewService ) {} @@ -80,13 +116,13 @@ export class CDPMonitorService { */ async ensureMonitoring(browserViewId: number): Promise { // Cleanup stale monitors from other browser views - for (const [monitoredId] of this.monitors) { + for (const [monitoredId] of sharedMonitors) { if (monitoredId !== browserViewId) { this.cleanup(monitoredId); } } - if (this.monitors.has(browserViewId)) { + if (sharedMonitors.has(browserViewId)) { return; } @@ -131,7 +167,7 @@ export class CDPMonitorService { // Enable Network domain for requests await this.browserViewService.sendCDPCommand(browserViewId, 'Network.enable', {}); - this.monitors.set(browserViewId, monitor); + sharedMonitors.set(browserViewId, monitor); } /** @@ -143,7 +179,7 @@ export class CDPMonitorService { limit?: number; clear?: boolean; }): ConsoleLog[] { - const monitor = this.monitors.get(browserViewId); + const monitor = sharedMonitors.get(browserViewId); if (!monitor) { return []; } @@ -186,7 +222,7 @@ export class CDPMonitorService { url?: string; method?: string; }> { - const monitor = this.monitors.get(browserViewId); + const monitor = sharedMonitors.get(browserViewId); if (!monitor) { return []; } @@ -263,7 +299,7 @@ export class CDPMonitorService { requestHeaders?: Record; responseHeaders?: Record; }> { - const monitor = this.monitors.get(browserViewId); + const monitor = sharedMonitors.get(browserViewId); if (!monitor) { return []; } @@ -313,26 +349,17 @@ export class CDPMonitorService { /** * Cleanup CDP monitoring for a browser view. + * Delegates to the module-level function for consistency. */ cleanup(browserViewId: number): void { - const monitor = this.monitors.get(browserViewId); - if (monitor) { - for (const cleanup of monitor.cleanupFunctions) { - try { - cleanup(); - } catch { - // Ignore cleanup errors - } - } - this.monitors.delete(browserViewId); - } + cleanupCDPMonitoring(browserViewId); } /** * Check if monitoring is active for a browser view. */ isMonitoring(browserViewId: number): boolean { - return this.monitors.has(browserViewId); + return sharedMonitors.has(browserViewId); } // ========================================================================== diff --git a/src/vs/workbench/contrib/roopik/electron-main/tools/index.ts b/src/vs/workbench/contrib/roopik/electron-main/tools/index.ts index 2f321a6af..ae4aa64d3 100644 --- a/src/vs/workbench/contrib/roopik/electron-main/tools/index.ts +++ b/src/vs/workbench/contrib/roopik/electron-main/tools/index.ts @@ -17,7 +17,7 @@ * - ComponentToolService: Component management tools (6 tools) */ -export { CDPMonitorService } from './cdpMonitorService.js'; +export { CDPMonitorService, cleanupCDPMonitoring } from './cdpMonitorService.js'; export type { ConsoleLog, NetworkRequest, NetworkResponse } from './cdpMonitorService.js'; export { BrowserToolService } from './browserToolService.js'; diff --git a/src/vs/workbench/contrib/roopik/electron-main/tools/projectToolService.ts b/src/vs/workbench/contrib/roopik/electron-main/tools/projectToolService.ts index 7d9399ecd..2eabad2dc 100644 --- a/src/vs/workbench/contrib/roopik/electron-main/tools/projectToolService.ts +++ b/src/vs/workbench/contrib/roopik/electron-main/tools/projectToolService.ts @@ -53,6 +53,7 @@ export class ProjectToolService { url: serverInfo.url, projectRoot: serverInfo.projectRoot, framework: serverInfo.framework, + port: serverInfo.port, status } }; From 3949f8c0925a0e24bddd78f4c4bb1e51aa5d6ba9 Mon Sep 17 00:00:00 2001 From: Dinesh Yadav <13635627+HumbleBee14@users.noreply.github.com> Date: Fri, 30 Jan 2026 22:17:02 -0500 Subject: [PATCH 5/8] Add browser viewport & network tools; enhance CDP monitor Rename browser_get_cdp_info to browser_get_state and add two new browser tools: browser_set_viewport and browser_get_network_requests. Wire up handlers and client methods for the new tools and adjust browserOpen return type. Enhance CDPMonitorService with noise filtering, static-vs-api classification, auto-clear on page reload, network filtering/limit handling, and a compressed network summary API; add internal helpers. Update tool schemas and descriptions to expose includeStaticAssets and clarify error behavior. Include runtime error data in ComponentToolService responses and make runtime errors take precedence when computing component status. Also remove frameworkDisplayName mapping in the channel. These changes improve browser inspection, reduce noise, and provide richer component and network diagnostics for agents. --- .../core/prompts/tools/native-tools/roopik.ts | 85 ++++++- .../core/prompts/tools/roopik/roopik-tools.ts | 10 +- .../core/tools/roopik/RoopikToolHandler.ts | 33 ++- .../src/services/roopik/RoopikToolClient.ts | 48 +++- .../channel/roopikToolsChannel.ts | 3 +- .../roopik/electron-main/mcp/toolSchemas.ts | 5 +- .../electron-main/tools/cdpMonitorService.ts | 215 +++++++++++++++++- .../tools/componentToolService.ts | 60 +++-- tools/roopik/mcp-stdio-server/src/tools.ts | 5 +- 9 files changed, 408 insertions(+), 56 deletions(-) diff --git a/extensions/roopik-roo/src/core/prompts/tools/native-tools/roopik.ts b/extensions/roopik-roo/src/core/prompts/tools/native-tools/roopik.ts index 10d330664..aed7e4491 100644 --- a/extensions/roopik-roo/src/core/prompts/tools/native-tools/roopik.ts +++ b/extensions/roopik-roo/src/core/prompts/tools/native-tools/roopik.ts @@ -1,7 +1,7 @@ import type OpenAI from "openai" // ============================================================================ -// Browser Tools (12) +// Browser Tools (14) // ============================================================================ export const browser_open: OpenAI.Chat.ChatCompletionTool = { @@ -266,12 +266,12 @@ export const browser_get_performance: OpenAI.Chat.ChatCompletionTool = { }, } -export const browser_get_cdp_info: OpenAI.Chat.ChatCompletionTool = { +export const browser_get_state: OpenAI.Chat.ChatCompletionTool = { type: "function", function: { - name: "browser_get_cdp_info", + name: "browser_get_state", description: - "[Roopik IDE] Get information about browser state and available Roopik tools for browser automation. Returns current URL, dev server status, and list of available browser tools.", + "[Roopik IDE] Get browser state information (open/closed, current URL, title).", strict: true, parameters: { type: "object", @@ -282,6 +282,77 @@ export const browser_get_cdp_info: OpenAI.Chat.ChatCompletionTool = { }, } +export const browser_set_viewport: OpenAI.Chat.ChatCompletionTool = { + type: "function", + function: { + name: "browser_set_viewport", + description: + "[Roopik IDE] Set or clear browser viewport override. Provide width/height to set a specific size (e.g., mobile 375x812). Call with NO parameters to clear override and restore natural browser size.", + strict: true, + parameters: { + type: "object", + properties: { + width: { + type: "number", + description: "Viewport width in pixels. Omit to clear override.", + }, + height: { + type: "number", + description: "Viewport height in pixels. Omit to clear override.", + }, + deviceScaleFactor: { + type: "number", + description: "Device scale factor (default: 1)", + }, + mobile: { + type: "boolean", + description: "Emulate mobile device (default: false)", + }, + }, + required: [], + additionalProperties: false, + }, + }, +} + +export const browser_get_network_requests: OpenAI.Chat.ChatCompletionTool = { + type: "function", + function: { + name: "browser_get_network_requests", + description: + "[Roopik IDE] Get network requests. Use includeStaticAssets parameter to show all assets.", + strict: false, + parameters: { + type: "object", + properties: { + includeStaticAssets: { + type: "boolean", + description: "Include static assets (JS/CSS/images). Default: false (only API calls shown)", + }, + urlFilter: { + type: "string", + description: "Filter requests by URL substring", + }, + method: { + type: "string", + description: "Filter by HTTP method (GET, POST, etc.)", + }, + statusFilter: { + type: "string", + description: "Filter by status: success (2xx-3xx), error (4xx-5xx or failed), all", + enum: ["success", "error", "all"], + }, + limit: { + type: "number", + description: "Maximum number of requests to return (default: 100, max: 500)", + }, + }, + required: [], + additionalProperties: false, + }, + }, +} + // ============================================================================ // Project Tools (3) // ============================================================================ @@ -584,7 +655,7 @@ export const component_rebuild: OpenAI.Chat.ChatCompletionTool = { // ============================================================================ export const roopikNativeTools: OpenAI.Chat.ChatCompletionTool[] = [ - // Browser (12 tools) + // Browser (14 tools) browser_open, browser_close, browser_action_input, @@ -596,7 +667,9 @@ export const roopikNativeTools: OpenAI.Chat.ChatCompletionTool[] = [ browser_get_errors, browser_get_console_logs, browser_get_performance, - browser_get_cdp_info, + browser_get_state, + browser_set_viewport, + browser_get_network_requests, // Project (3 tools) project_get_active, project_start, diff --git a/extensions/roopik-roo/src/core/prompts/tools/roopik/roopik-tools.ts b/extensions/roopik-roo/src/core/prompts/tools/roopik/roopik-tools.ts index 67ef6971f..d3ce22b84 100644 --- a/extensions/roopik-roo/src/core/prompts/tools/roopik/roopik-tools.ts +++ b/extensions/roopik-roo/src/core/prompts/tools/roopik/roopik-tools.ts @@ -5,8 +5,8 @@ * used for tool validation and routing. * * Tool Categories: - * - Browser (12): open, close, action, navigate, reload, screenshot, execute_script, inspect_element, - * get_errors, get_console_logs, get_performance, get_cdp_info + * - Browser (14): open, close, action, navigate, reload, screenshot, execute_script, inspect_element, + * get_errors, get_console_logs, get_performance, get_state, set_viewport, get_network_requests * - Project (3): get_active, start, stop * - Canvas (3): list, get_active, create * - Component (6): add, add_batch, remove, get_info, list, rebuild @@ -451,7 +451,7 @@ ${descriptions.join("\n\n")}` * Used for tool validation and routing. */ export const ROOPIK_TOOL_NAMES = [ - // Browser (12 tools) + // Browser (14 tools) "browser_open", "browser_close", "browser_action_input", @@ -463,7 +463,9 @@ export const ROOPIK_TOOL_NAMES = [ "browser_get_errors", "browser_get_console_logs", "browser_get_performance", - "browser_get_cdp_info", + "browser_get_state", + "browser_set_viewport", + "browser_get_network_requests", // Project (3 tools) "project_get_active", "project_start", diff --git a/extensions/roopik-roo/src/core/tools/roopik/RoopikToolHandler.ts b/extensions/roopik-roo/src/core/tools/roopik/RoopikToolHandler.ts index 2eddb50ff..afecb1247 100644 --- a/extensions/roopik-roo/src/core/tools/roopik/RoopikToolHandler.ts +++ b/extensions/roopik-roo/src/core/tools/roopik/RoopikToolHandler.ts @@ -70,7 +70,7 @@ export async function handleRoopikTool( let result: RoopikToolResult switch (toolName) { - // Browser Tools (12) + // Browser Tools (14) case "browser_open": result = await handleBrowserOpen(task, block, callbacks) break @@ -104,8 +104,14 @@ export async function handleRoopikTool( case "browser_get_performance": result = await handleBrowserGetPerformance(task, block, callbacks) break - case "browser_get_cdp_info": - result = await handleBrowserGetCdpInfo(task, block, callbacks) + case "browser_get_state": + result = await handleBrowserGetState(task, block, callbacks) + break + case "browser_set_viewport": + result = await handleBrowserSetViewport(task, block, callbacks) + break + case "browser_get_network_requests": + result = await handleBrowserGetNetworkRequests(task, block, callbacks) break // Project Tools (3) @@ -230,8 +236,25 @@ async function handleBrowserGetPerformance(task: Task, block: ToolUse, callbacks return roopikClient.browserGetPerformance() } -async function handleBrowserGetCdpInfo(task: Task, block: ToolUse, callbacks: ToolCallbacks): Promise { - return roopikClient.browserGetCdpInfo() +async function handleBrowserGetState(task: Task, block: ToolUse, callbacks: ToolCallbacks): Promise { + return roopikClient.browserGetState() +} + +async function handleBrowserSetViewport(task: Task, block: ToolUse, callbacks: ToolCallbacks): Promise { + const width = block.params.width ? parseInt(block.params.width, 10) : undefined + const height = block.params.height ? parseInt(block.params.height, 10) : undefined + const deviceScaleFactor = block.params.deviceScaleFactor ? parseFloat(block.params.deviceScaleFactor) : undefined + const mobile = block.params.mobile === "true" || block.params.mobile === true + return roopikClient.browserSetViewport(width, height, deviceScaleFactor, mobile) +} + +async function handleBrowserGetNetworkRequests(task: Task, block: ToolUse, callbacks: ToolCallbacks): Promise { + const includeStaticAssets = block.params.includeStaticAssets === "true" || block.params.includeStaticAssets === true + const urlFilter = block.params.urlFilter + const method = block.params.method + const statusFilter = block.params.statusFilter as "success" | "error" | "all" | undefined + const limit = block.params.limit ? parseInt(block.params.limit, 10) : undefined + return roopikClient.browserGetNetworkRequests({ includeStaticAssets, urlFilter, method, statusFilter, limit }) } async function handleNavigate(task: Task, block: ToolUse, callbacks: ToolCallbacks): Promise { diff --git a/extensions/roopik-roo/src/services/roopik/RoopikToolClient.ts b/extensions/roopik-roo/src/services/roopik/RoopikToolClient.ts index 2ce973b75..7ae1ddcd9 100644 --- a/extensions/roopik-roo/src/services/roopik/RoopikToolClient.ts +++ b/extensions/roopik-roo/src/services/roopik/RoopikToolClient.ts @@ -166,7 +166,6 @@ export interface ActiveProjectData { port?: number state?: string framework?: string - frameworkDisplayName?: string message?: string } @@ -195,8 +194,6 @@ export interface CanvasSummary { name: string componentCount: number description?: string - icon?: string - color?: string createdAt?: string updatedAt?: string } @@ -359,8 +356,8 @@ export class RoopikToolClient { * Open the browser preview * Optionally navigate to a URL after opening */ - async browserOpen(url?: string): Promise> { - return this.executeCommand<{ browserViewId: number; url?: string; message: string }>("roopik.tools.browserOpen", { url }) + async browserOpen(url?: string): Promise> { + return this.executeCommand<{ url?: string; message: string }>("roopik.tools.browserOpen", { url }) } /** @@ -402,10 +399,45 @@ export class RoopikToolClient { } /** - * Get CDP connection info for external agents + * Get browser state information (open/closed, current URL, title) */ - async browserGetCdpInfo(): Promise> { - return this.executeCommand("roopik.tools.browserGetCdpInfo") + async browserGetState(): Promise> { + return this.executeCommand("roopik.tools.browserGetState") + } + + /** + * Set or clear browser viewport override + * @param width Viewport width in pixels (omit to clear override) + * @param height Viewport height in pixels (omit to clear override) + * @param deviceScaleFactor Device scale factor (default: 1) + * @param mobile Emulate mobile device (default: false) + */ + async browserSetViewport( + width?: number, + height?: number, + deviceScaleFactor?: number, + mobile?: boolean + ): Promise> { + return this.executeCommand("roopik.tools.browserSetViewport", { + width, + height, + deviceScaleFactor, + mobile, + }) + } + + /** + * Get network requests + * @param options Filter options for network requests + */ + async browserGetNetworkRequests(options?: { + includeStaticAssets?: boolean + urlFilter?: string + method?: string + statusFilter?: "success" | "error" | "all" + limit?: number + }): Promise> { + return this.executeCommand("roopik.tools.browserGetNetworkRequests", options) } /** diff --git a/src/vs/workbench/contrib/roopik/electron-main/channel/roopikToolsChannel.ts b/src/vs/workbench/contrib/roopik/electron-main/channel/roopikToolsChannel.ts index 091a0bedf..4335494c4 100644 --- a/src/vs/workbench/contrib/roopik/electron-main/channel/roopikToolsChannel.ts +++ b/src/vs/workbench/contrib/roopik/electron-main/channel/roopikToolsChannel.ts @@ -600,8 +600,7 @@ export class RoopikToolsChannel implements IServerChannel { url: result.data.url, port: result.data.port, state: result.data.status, - framework: result.data.framework, - frameworkDisplayName: result.data.frameworkDisplayName + framework: result.data.framework } }; } diff --git a/src/vs/workbench/contrib/roopik/electron-main/mcp/toolSchemas.ts b/src/vs/workbench/contrib/roopik/electron-main/mcp/toolSchemas.ts index 3c33ebae5..e9e78371b 100644 --- a/src/vs/workbench/contrib/roopik/electron-main/mcp/toolSchemas.ts +++ b/src/vs/workbench/contrib/roopik/electron-main/mcp/toolSchemas.ts @@ -72,6 +72,7 @@ export const browserSetViewportSchema = z.object({ }); export const browserGetNetworkRequestsSchema = z.object({ + includeStaticAssets: z.boolean().optional().describe('Include static assets (JS/CSS/images). Default: false (only API calls shown)'), urlFilter: z.string().optional().describe('Filter requests by URL substring'), method: z.string().optional().describe('Filter by HTTP method (GET, POST, etc.)'), statusFilter: z.enum(['success', 'error', 'all']).optional() @@ -203,7 +204,7 @@ export const TOOL_DEFINITIONS: ToolDefinition[] = [ }, { name: 'browser_get_errors', - description: 'Get combined console errors and network failures.', + description: 'Get console errors and network failures. Auto-clears on page reload.', schema: browserGetErrorsSchema }, { @@ -228,7 +229,7 @@ export const TOOL_DEFINITIONS: ToolDefinition[] = [ }, { name: 'browser_get_network_requests', - description: 'Get captured network requests and responses. Requires CDP monitoring.', + description: 'Get network requests. Use includeStaticAssets parameter to show all assets.', schema: browserGetNetworkRequestsSchema }, diff --git a/src/vs/workbench/contrib/roopik/electron-main/tools/cdpMonitorService.ts b/src/vs/workbench/contrib/roopik/electron-main/tools/cdpMonitorService.ts index 263ba8ddb..8fbec303e 100644 --- a/src/vs/workbench/contrib/roopik/electron-main/tools/cdpMonitorService.ts +++ b/src/vs/workbench/contrib/roopik/electron-main/tools/cdpMonitorService.ts @@ -13,10 +13,41 @@ * * IMPORTANT: Uses module-level state so ALL instances share the same data. * This ensures both Claude Code and Dio agent see the same console logs/errors. + * + * SELF-HEALING ARCHITECTURE: + * - Auto-clears on Page.loadEventFired (page reload/HMR) to prevent "ghost" errors + * - Filters HMR/Vite/WDS noise at capture time to save tokens + * - Classifies network requests as 'static' vs 'api' for compression */ import type { BrowserViewService } from '../projectMode/browserViewService.js'; +// ============================================================================ +// Noise Filtering Patterns +// ============================================================================ + +/** + * Console messages matching these patterns are dev-tool noise with no debugging value. + * Filtering at capture time saves tokens and prevents agent confusion. + */ +const NOISE_PATTERNS = [ + /^\[HMR\]/, // Webpack Hot Module Replacement + /^\[WDS\]/, // Webpack Dev Server + /^\[vite\]/, // Vite dev server + /React DevTools/, // React DevTools promotion + /Download the React/, // React DevTools banner + /Fast Refresh/, // React Fast Refresh + /hot updated/i, // Generic HMR messages + /Compiled successfully/, // CRA/Next.js compile messages + /webpack.*compiled/i, // Webpack compiled messages +]; + +/** + * Static asset file extensions - these requests are noise for debugging. + * API calls and failures are signal; static assets are noise. + */ +const STATIC_ASSET_REGEX = /\.(js|mjs|cjs|jsx|ts|tsx|css|scss|sass|less|png|jpg|jpeg|gif|webp|ico|svg|woff|woff2|ttf|eot|otf|map|json)(\?.*)?$/i; + // ============================================================================ // Module-Level Shared State // ============================================================================ @@ -49,6 +80,8 @@ export interface NetworkRequest { url: string; headers: Record; postData?: string; + /** Classification: 'static' for JS/CSS/images, 'api' for API calls */ + type: 'static' | 'api'; } export interface NetworkResponse { @@ -104,7 +137,7 @@ export function cleanupCDPMonitoring(browserViewId: number): void { export class CDPMonitorService { constructor( private readonly browserViewService: BrowserViewService - ) {} + ) { } // ========================================================================== // Public API @@ -142,8 +175,12 @@ export class CDPMonitorService { return; } - // Enable Runtime domain for console - await this.browserViewService.sendCDPCommand(browserViewId, 'Runtime.enable', {}); + // Enable CDP domains in parallel for efficiency + await Promise.all([ + this.browserViewService.sendCDPCommand(browserViewId, 'Runtime.enable', {}), + this.browserViewService.sendCDPCommand(browserViewId, 'Network.enable', {}), + this.browserViewService.sendCDPCommand(browserViewId, 'Page.enable', {}), // NEW: Lifecycle events + ]); // Register CDP event listener - single listener handles all events const cdpCleanup = this.browserViewService.onCDPEvent(browserViewId, (method, params) => { @@ -160,13 +197,15 @@ export class CDPMonitorService { case 'Network.loadingFailed': this.handleNetworkFailed(monitor, params as Parameters[1]); break; + case 'Page.loadEventFired': + // AUTO-CLEAR: Page reloaded (manual refresh or HMR) + // Previous errors are now stale - clear for fresh state + this.clearMonitorData(monitor); + break; } }); monitor.cleanupFunctions.push(cdpCleanup); - // Enable Network domain for requests - await this.browserViewService.sendCDPCommand(browserViewId, 'Network.enable', {}); - sharedMonitors.set(browserViewId, monitor); } @@ -261,7 +300,7 @@ export class CDPMonitorService { timestamp: response.timestamp, source: 'network', type: response.status === 0 ? 'failed' : `${response.status}`, - message: response.status === 0 ? 'Request failed' : response.statusText, + message: response.status === 0 ? 'Request failed' : (response.statusText || `HTTP ${response.status}`), url: response.url, method: request?.method }); @@ -283,6 +322,7 @@ export class CDPMonitorService { * Get network requests for a browser view. */ getNetworkRequests(browserViewId: number, options?: { + includeStaticAssets?: boolean; urlFilter?: string; method?: string; statusFilter?: 'success' | 'error' | 'all'; @@ -304,7 +344,7 @@ export class CDPMonitorService { return []; } - // Join requests with responses + // Join requests with responses, including request type for filtering let requests = monitor.networkRequests.map(req => { const response = monitor.networkResponses.find(r => r.requestId === req.requestId); return { @@ -317,10 +357,27 @@ export class CDPMonitorService { duration: response?.duration, timestamp: req.timestamp, requestHeaders: req.headers, - responseHeaders: response?.headers + responseHeaders: response?.headers, + _type: req.type // Internal: used for filtering }; }); + // Filter static assets unless includeStaticAssets is true + if (!options?.includeStaticAssets) { + requests = requests.filter(r => { + // Always show API requests + if (r._type === 'api') { + return true; + } + // Always show localhost/127.0.0.1 (project assets) + if (this.isLocalhost(r.url)) { + return true; + } + // Hide external static assets + return false; + }); + } + // Filter by URL if (options?.urlFilter) { const filter = options.urlFilter.toLowerCase(); @@ -344,7 +401,10 @@ export class CDPMonitorService { // Apply limit (max 500) const limit = Math.min(options?.limit || 100, 500); - return requests.slice(-limit); + const limitedRequests = requests.slice(-limit); + + // Remove internal _type field before returning + return limitedRequests.map(({ _type, ...rest }) => rest); } /** @@ -362,6 +422,124 @@ export class CDPMonitorService { return sharedMonitors.has(browserViewId); } + /** + * Manually clear all CDP data for a browser view. + * Useful for agent to reset state before specific actions. + */ + clearAllData(browserViewId: number): void { + const monitor = sharedMonitors.get(browserViewId); + if (monitor) { + this.clearMonitorData(monitor); + } + } + + /** + * Get compressed network summary optimized for self-healing agents. + * API requests shown individually, static assets summarized. + */ + getNetworkSummary(browserViewId: number): { + apiRequests: Array<{ method: string; url: string; status?: number; statusText?: string; duration?: number }>; + failures: Array<{ method: string; url: string; status: number; statusText: string; error?: string }>; + staticAssetsSummary: { loaded: number; failed: number }; + } { + const monitor = sharedMonitors.get(browserViewId); + if (!monitor) { + return { apiRequests: [], failures: [], staticAssetsSummary: { loaded: 0, failed: 0 } }; + } + + const apiRequests: Array<{ method: string; url: string; status?: number; statusText?: string; duration?: number }> = []; + const failures: Array<{ method: string; url: string; status: number; statusText: string; error?: string }> = []; + let staticLoaded = 0; + let staticFailed = 0; + + for (const req of monitor.networkRequests) { + const response = monitor.networkResponses.find(r => r.requestId === req.requestId); + const status = response?.status ?? 0; + + if (req.type === 'static') { + // Static assets: just count them + if (status >= 200 && status < 400) { + staticLoaded++; + } else if (status >= 400 || status === 0) { + staticFailed++; + } + } else { + // API request: include full details + if (status >= 400 || status === 0) { + failures.push({ + method: req.method, + url: req.url, + status, + statusText: response?.statusText || 'Failed', + error: status === 0 ? 'Request failed' : undefined + }); + } else if (response) { + apiRequests.push({ + method: req.method, + url: req.url, + status: response.status, + statusText: response.statusText, + duration: response.duration + }); + } + } + } + + return { + apiRequests, + failures, + staticAssetsSummary: { loaded: staticLoaded, failed: staticFailed } + }; + } + + // ========================================================================== + // Internal Helpers + // ========================================================================== + + /** + * Clear all data for a monitor (called on Page.loadEventFired). + * Gives agents a fresh state after page reload/HMR. + */ + private clearMonitorData(monitor: CDPMonitor): void { + monitor.consoleLogs = []; + monitor.networkRequests = []; + monitor.networkResponses = []; + monitor.requestStartTimes.clear(); + } + + /** + * Check if a console message is HMR/dev-tool noise. + */ + private isNoisyLog(message: string): boolean { + return NOISE_PATTERNS.some(pattern => pattern.test(message)); + } + + /** + * Check if a URL is for a static asset. + */ + private isStaticAsset(url: string): boolean { + try { + const pathname = new URL(url).pathname; + return STATIC_ASSET_REGEX.test(pathname); + } catch { + return STATIC_ASSET_REGEX.test(url); + } + } + + /** + * Check if a URL is from localhost/127.0.0.1 (project assets). + * Project assets should always be visible for debugging. + */ + private isLocalhost(url: string): boolean { + try { + const urlObj = new URL(url); + const hostname = urlObj.hostname.toLowerCase(); + return hostname === 'localhost' || hostname === '127.0.0.1' || hostname === '[::1]'; + } catch { + return false; + } + } + // ========================================================================== // Internal Handlers // ========================================================================== @@ -387,11 +565,19 @@ export class CDPMonitorService { return '[object]'; }); + const fullMessage = messages.join(' '); + + // NOISE FILTER: Drop HMR/Vite/WDS messages at capture time + // These waste tokens and have no debugging value for agents + if (this.isNoisyLog(fullMessage)) { + return; + } + const log: ConsoleLog = { id: `console-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`, timestamp: timestamp * 1000, type: type as ConsoleLog['type'], - message: messages.join(' '), + message: fullMessage, args: args.map(a => a.value), stackTrace }; @@ -418,6 +604,10 @@ export class CDPMonitorService { monitor.requestStartTimes.set(requestId, timestamp); + // CLASSIFY: static assets vs API calls + // Static assets will be summarized, API calls shown in full + const requestType = this.isStaticAsset(request.url) ? 'static' : 'api'; + const networkRequest: NetworkRequest = { id: `req-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`, requestId, @@ -425,7 +615,8 @@ export class CDPMonitorService { method: request.method, url: request.url, headers: request.headers || {}, - postData: request.postData + postData: request.postData, + type: requestType }; monitor.networkRequests.push(networkRequest); diff --git a/src/vs/workbench/contrib/roopik/electron-main/tools/componentToolService.ts b/src/vs/workbench/contrib/roopik/electron-main/tools/componentToolService.ts index ef3b8e5a7..8cca73e11 100644 --- a/src/vs/workbench/contrib/roopik/electron-main/tools/componentToolService.ts +++ b/src/vs/workbench/contrib/roopik/electron-main/tools/componentToolService.ts @@ -57,7 +57,7 @@ export class ComponentToolService { constructor( private readonly componentService: ComponentService, private readonly canvasService: ICanvasService - ) {} + ) { } // ========================================================================== // Add Component @@ -160,7 +160,11 @@ export class ComponentToolService { // Get Component Info // ========================================================================== - async getInfo(componentId: string): Promise> { + async getInfo(componentId: string): Promise> { try { // getComponent is sync - returns Component | undefined const component = this.componentService.getComponent(componentId); @@ -175,11 +179,20 @@ export class ComponentToolService { // Get comprehensive info via async getComponentInfo for build state const componentInfo = await this.componentService.getComponentInfo(componentId); - // Map buildStatus from ComponentInfo to the status field expected by MCP - // ComponentInfo.buildStatus is 'building' | 'ready' | 'error', MCP adds 'pending' - const status = componentInfo.buildStatus === 'building' ? 'building' : - componentInfo.buildStatus === 'ready' ? 'ready' : - componentInfo.buildStatus === 'error' ? 'error' : 'pending'; + // Determine status: runtime error takes precedence over build success + // A component can build successfully but crash at runtime + let status: 'pending' | 'building' | 'ready' | 'error'; + if (componentInfo.runtimeError) { + status = 'error'; // Runtime error = error state + } else if (componentInfo.buildStatus === 'building') { + status = 'building'; + } else if (componentInfo.buildStatus === 'ready') { + status = 'ready'; + } else if (componentInfo.buildStatus === 'error') { + status = 'error'; + } else { + status = 'pending'; + } return { success: true, @@ -190,7 +203,13 @@ export class ComponentToolService { canvasId: component.canvasId, status, buildStatus: componentInfo.buildStatus, - buildErrors: componentInfo.buildErrorInfo?.message ? [componentInfo.buildErrorInfo.message] : undefined + buildErrors: componentInfo.buildErrorInfo?.message ? [componentInfo.buildErrorInfo.message] : undefined, + // Include runtime error so agents can see crashes that happened after build + runtimeError: componentInfo.runtimeError ? { + message: componentInfo.runtimeError.message, + type: componentInfo.runtimeError.type, + stack: componentInfo.runtimeError.stack + } : undefined } }; } catch (error) { @@ -210,13 +229,24 @@ export class ComponentToolService { // getComponentsForCanvas is sync const components = this.componentService.getComponentsForCanvas(canvasId); - const result: ComponentInfo[] = components.map(c => ({ - id: c.id, - name: c.componentName, - path: c.folderPath, - canvasId: c.canvasId, - status: c.buildState.status - })); + // Map components, accounting for runtime errors in status + const result: ComponentInfo[] = components.map(c => { + // Runtime error takes precedence over build status + let status: 'pending' | 'building' | 'ready' | 'error'; + if (c.runtimeError) { + status = 'error'; // Component crashed at runtime + } else { + status = c.buildState.status; + } + + return { + id: c.id, + name: c.componentName, + path: c.folderPath, + canvasId: c.canvasId, + status + }; + }); return { success: true, diff --git a/tools/roopik/mcp-stdio-server/src/tools.ts b/tools/roopik/mcp-stdio-server/src/tools.ts index b47161f40..d0fc0a626 100644 --- a/tools/roopik/mcp-stdio-server/src/tools.ts +++ b/tools/roopik/mcp-stdio-server/src/tools.ts @@ -76,6 +76,7 @@ export const browserSetViewportSchema = z.object({ }); export const browserGetNetworkRequestsSchema = z.object({ + includeStaticAssets: z.boolean().optional().describe('Include static assets (JS/CSS/images). Default: false (only API calls shown)'), urlFilter: z.string().optional().describe('Filter requests by URL substring'), method: z.string().optional().describe('Filter by HTTP method (GET, POST, etc.)'), statusFilter: z.enum(['success', 'error', 'all']).optional() @@ -207,7 +208,7 @@ export const TOOL_DEFINITIONS: ToolDefinition[] = [ }, { name: 'browser_get_errors', - description: 'Get combined console errors and network failures.', + description: 'Get console errors and network failures. Auto-clears on page reload.', schema: browserGetErrorsSchema }, { @@ -232,7 +233,7 @@ export const TOOL_DEFINITIONS: ToolDefinition[] = [ }, { name: 'browser_get_network_requests', - description: 'Get captured network requests and responses. Requires CDP monitoring.', + description: 'Get network requests. Use includeStaticAssets parameter to show all assets.', schema: browserGetNetworkRequestsSchema }, From f60aa48b8116b4c67cfa35ed4ffdbe72b7fe31da Mon Sep 17 00:00:00 2001 From: Dinesh Yadav <13635627+HumbleBee14@users.noreply.github.com> Date: Fri, 30 Jan 2026 22:59:16 -0500 Subject: [PATCH 6/8] vue-devtool --- .../_metadata/verified_contents.json | 0 ....vue_vue_type_script_setup_true_lang-CzNAudTz.js | 0 ....vue_vue_type_script_setup_true_lang-N7sQGPjM.js | 0 .../client/SectionBlock-Baq-oFWF.js | 0 .../client/SectionBlock-Dp0NIF_b.js | 0 .../client/assets-Cenvjhy6.js | 0 .../client/assets-DtK-7ema.js | 0 .../client/color-scheme.js | 0 .../client/components-BgyXz8w_.js | 0 .../client/components-C1pVLxno.js | 0 .../client/css-BnL1064W.js | 0 .../client/custom-inspector-tab-view-CIZhovSF.js | 0 .../client/custom-inspector-tab-view-dZSnsHNr.js | 0 .../client/custom-tab-view-DB2FZ6eN.js | 0 .../client/custom-tab-view-DnTFKhiI.js | 0 .../client/devtools-panel.css | 0 .../client/devtools-panel.js | 0 .../client/diff-DOdUfPSR.js | 0 .../client/graph-CHZkohji.js | 0 .../client/graph-LINFOiRk.js | 0 .../client/html-Iy9EJBkL.js | 0 .../client/index-B-XIiefs.js | 0 .../client/index-C7XPmmLS.js | 0 .../client/javascript-CXHlxgtu.js | 0 .../client/json-D4cXEhM0.js | 0 .../client/logo.svg | 0 .../client/overview-B6qofkEm.js | 0 .../client/overview-NOHiLwdq.js | 0 .../client/pages-Bqu7i9_W.js | 0 .../client/pages-C5V9CoPY.js | 0 .../client/pinia-BPran779.js | 0 .../client/pinia-VeJqP-Jl.js | 0 .../client/router-BW0WfY9R.js | 0 .../client/router-BbI-eMq9.js | 0 .../client/settings-DTrnopyF.js | 0 .../client/settings-DnVNSbhN.js | 0 .../client/shellscript-DmhSIIKI.js | 0 .../client/timeline-CVQgEdyi.js | 0 .../client/timeline-Cwu3caRC.js | 0 .../client/typescript-DKfvlBCw.js | 0 .../client/vitesse-dark-Cym-eLtO.js | 0 .../client/vitesse-light-CcmG315c.js | 0 .../client/vue-ChaKAHzo.js | 0 .../client/vue-apis-C8_kYYSp.js | 0 .../client/vue-html-Hg1VsDox.js | 0 .../client/yaml-DcQhbMrL.js | 0 .../dist/background.js | 0 .../dist/detector.js | 0 .../dist/devtools-background.js | 0 .../dist/devtools-overlay.js | 0 .../dist/devtools-panel.js | 0 .../dist/prepare.js | 0 .../dist/proxy.js | 0 .../dist/user-app.js | 0 .../icons/128-beta.png | Bin .../icons/128-gray.png | Bin .../icons/128.nuxt.png | Bin .../icons/128.png | Bin .../icons/128.vitepress.png | Bin .../icons/16-beta.png | Bin .../icons/16-gray.png | Bin .../icons/16.nuxt.png | Bin .../icons/16.png | Bin .../icons/16.vitepress.png | Bin .../icons/48-beta.png | Bin .../icons/48-gray.png | Bin .../icons/48.nuxt.png | Bin .../icons/48.png | Bin .../icons/48.vitepress.png | Bin .../manifest.json | 0 .../overlay/devtools-overlay.css | 0 .../overlay/devtools-overlay.js | 0 .../package.json | 0 .../pages/devtools-background.html | 0 .../pages/devtools-panel.html | 0 .../popups/devtools-screenshot.png | Bin .../popups/disabled.html | 0 .../popups/disabled.nuxt.html | 0 .../popups/disabled.vitepress.html | 0 .../popups/enabled.html | 0 .../popups/enabled.nuxt.html | 0 .../popups/enabled.vitepress.html | 0 .../popups/not-found.html | 0 .../popups/popup.css | 0 .../popups/vue2-migration-guide.html | 0 .../electron-main/channel/roopikToolsChannel.ts | 2 +- .../electron-main/mcp/executor/toolExecutor.ts | 2 +- 87 files changed, 2 insertions(+), 2 deletions(-) rename resources/devtools-extensions/vue-devtools/{Vue-js-devtools-Chrome-Web-Store => }/_metadata/verified_contents.json (100%) rename resources/devtools-extensions/vue-devtools/{Vue-js-devtools-Chrome-Web-Store => }/client/IconTitle.vue_vue_type_script_setup_true_lang-CzNAudTz.js (100%) rename resources/devtools-extensions/vue-devtools/{Vue-js-devtools-Chrome-Web-Store => }/client/IconTitle.vue_vue_type_script_setup_true_lang-N7sQGPjM.js (100%) rename resources/devtools-extensions/vue-devtools/{Vue-js-devtools-Chrome-Web-Store => }/client/SectionBlock-Baq-oFWF.js (100%) rename resources/devtools-extensions/vue-devtools/{Vue-js-devtools-Chrome-Web-Store => }/client/SectionBlock-Dp0NIF_b.js (100%) rename resources/devtools-extensions/vue-devtools/{Vue-js-devtools-Chrome-Web-Store => }/client/assets-Cenvjhy6.js (100%) rename resources/devtools-extensions/vue-devtools/{Vue-js-devtools-Chrome-Web-Store => }/client/assets-DtK-7ema.js (100%) rename resources/devtools-extensions/vue-devtools/{Vue-js-devtools-Chrome-Web-Store => }/client/color-scheme.js (100%) rename resources/devtools-extensions/vue-devtools/{Vue-js-devtools-Chrome-Web-Store => }/client/components-BgyXz8w_.js (100%) rename resources/devtools-extensions/vue-devtools/{Vue-js-devtools-Chrome-Web-Store => }/client/components-C1pVLxno.js (100%) rename resources/devtools-extensions/vue-devtools/{Vue-js-devtools-Chrome-Web-Store => }/client/css-BnL1064W.js (100%) rename resources/devtools-extensions/vue-devtools/{Vue-js-devtools-Chrome-Web-Store => }/client/custom-inspector-tab-view-CIZhovSF.js (100%) rename resources/devtools-extensions/vue-devtools/{Vue-js-devtools-Chrome-Web-Store => }/client/custom-inspector-tab-view-dZSnsHNr.js (100%) rename resources/devtools-extensions/vue-devtools/{Vue-js-devtools-Chrome-Web-Store => }/client/custom-tab-view-DB2FZ6eN.js (100%) rename resources/devtools-extensions/vue-devtools/{Vue-js-devtools-Chrome-Web-Store => }/client/custom-tab-view-DnTFKhiI.js (100%) rename resources/devtools-extensions/vue-devtools/{Vue-js-devtools-Chrome-Web-Store => }/client/devtools-panel.css (100%) rename resources/devtools-extensions/vue-devtools/{Vue-js-devtools-Chrome-Web-Store => }/client/devtools-panel.js (100%) rename resources/devtools-extensions/vue-devtools/{Vue-js-devtools-Chrome-Web-Store => }/client/diff-DOdUfPSR.js (100%) rename resources/devtools-extensions/vue-devtools/{Vue-js-devtools-Chrome-Web-Store => }/client/graph-CHZkohji.js (100%) rename resources/devtools-extensions/vue-devtools/{Vue-js-devtools-Chrome-Web-Store => }/client/graph-LINFOiRk.js (100%) rename resources/devtools-extensions/vue-devtools/{Vue-js-devtools-Chrome-Web-Store => }/client/html-Iy9EJBkL.js (100%) rename resources/devtools-extensions/vue-devtools/{Vue-js-devtools-Chrome-Web-Store => }/client/index-B-XIiefs.js (100%) rename resources/devtools-extensions/vue-devtools/{Vue-js-devtools-Chrome-Web-Store => }/client/index-C7XPmmLS.js (100%) rename resources/devtools-extensions/vue-devtools/{Vue-js-devtools-Chrome-Web-Store => }/client/javascript-CXHlxgtu.js (100%) rename resources/devtools-extensions/vue-devtools/{Vue-js-devtools-Chrome-Web-Store => }/client/json-D4cXEhM0.js (100%) rename resources/devtools-extensions/vue-devtools/{Vue-js-devtools-Chrome-Web-Store => }/client/logo.svg (100%) rename resources/devtools-extensions/vue-devtools/{Vue-js-devtools-Chrome-Web-Store => }/client/overview-B6qofkEm.js (100%) rename resources/devtools-extensions/vue-devtools/{Vue-js-devtools-Chrome-Web-Store => }/client/overview-NOHiLwdq.js (100%) rename resources/devtools-extensions/vue-devtools/{Vue-js-devtools-Chrome-Web-Store => }/client/pages-Bqu7i9_W.js (100%) rename resources/devtools-extensions/vue-devtools/{Vue-js-devtools-Chrome-Web-Store => }/client/pages-C5V9CoPY.js (100%) rename resources/devtools-extensions/vue-devtools/{Vue-js-devtools-Chrome-Web-Store => }/client/pinia-BPran779.js (100%) rename resources/devtools-extensions/vue-devtools/{Vue-js-devtools-Chrome-Web-Store => }/client/pinia-VeJqP-Jl.js (100%) rename resources/devtools-extensions/vue-devtools/{Vue-js-devtools-Chrome-Web-Store => }/client/router-BW0WfY9R.js (100%) rename resources/devtools-extensions/vue-devtools/{Vue-js-devtools-Chrome-Web-Store => }/client/router-BbI-eMq9.js (100%) rename resources/devtools-extensions/vue-devtools/{Vue-js-devtools-Chrome-Web-Store => }/client/settings-DTrnopyF.js (100%) rename resources/devtools-extensions/vue-devtools/{Vue-js-devtools-Chrome-Web-Store => }/client/settings-DnVNSbhN.js (100%) rename resources/devtools-extensions/vue-devtools/{Vue-js-devtools-Chrome-Web-Store => }/client/shellscript-DmhSIIKI.js (100%) rename resources/devtools-extensions/vue-devtools/{Vue-js-devtools-Chrome-Web-Store => }/client/timeline-CVQgEdyi.js (100%) rename resources/devtools-extensions/vue-devtools/{Vue-js-devtools-Chrome-Web-Store => }/client/timeline-Cwu3caRC.js (100%) rename resources/devtools-extensions/vue-devtools/{Vue-js-devtools-Chrome-Web-Store => }/client/typescript-DKfvlBCw.js (100%) rename resources/devtools-extensions/vue-devtools/{Vue-js-devtools-Chrome-Web-Store => }/client/vitesse-dark-Cym-eLtO.js (100%) rename resources/devtools-extensions/vue-devtools/{Vue-js-devtools-Chrome-Web-Store => }/client/vitesse-light-CcmG315c.js (100%) rename resources/devtools-extensions/vue-devtools/{Vue-js-devtools-Chrome-Web-Store => }/client/vue-ChaKAHzo.js (100%) rename resources/devtools-extensions/vue-devtools/{Vue-js-devtools-Chrome-Web-Store => }/client/vue-apis-C8_kYYSp.js (100%) rename resources/devtools-extensions/vue-devtools/{Vue-js-devtools-Chrome-Web-Store => }/client/vue-html-Hg1VsDox.js (100%) rename resources/devtools-extensions/vue-devtools/{Vue-js-devtools-Chrome-Web-Store => }/client/yaml-DcQhbMrL.js (100%) rename resources/devtools-extensions/vue-devtools/{Vue-js-devtools-Chrome-Web-Store => }/dist/background.js (100%) rename resources/devtools-extensions/vue-devtools/{Vue-js-devtools-Chrome-Web-Store => }/dist/detector.js (100%) rename resources/devtools-extensions/vue-devtools/{Vue-js-devtools-Chrome-Web-Store => }/dist/devtools-background.js (100%) rename resources/devtools-extensions/vue-devtools/{Vue-js-devtools-Chrome-Web-Store => }/dist/devtools-overlay.js (100%) rename resources/devtools-extensions/vue-devtools/{Vue-js-devtools-Chrome-Web-Store => }/dist/devtools-panel.js (100%) rename resources/devtools-extensions/vue-devtools/{Vue-js-devtools-Chrome-Web-Store => }/dist/prepare.js (100%) rename resources/devtools-extensions/vue-devtools/{Vue-js-devtools-Chrome-Web-Store => }/dist/proxy.js (100%) rename resources/devtools-extensions/vue-devtools/{Vue-js-devtools-Chrome-Web-Store => }/dist/user-app.js (100%) rename resources/devtools-extensions/vue-devtools/{Vue-js-devtools-Chrome-Web-Store => }/icons/128-beta.png (100%) rename resources/devtools-extensions/vue-devtools/{Vue-js-devtools-Chrome-Web-Store => }/icons/128-gray.png (100%) rename resources/devtools-extensions/vue-devtools/{Vue-js-devtools-Chrome-Web-Store => }/icons/128.nuxt.png (100%) rename resources/devtools-extensions/vue-devtools/{Vue-js-devtools-Chrome-Web-Store => }/icons/128.png (100%) rename resources/devtools-extensions/vue-devtools/{Vue-js-devtools-Chrome-Web-Store => }/icons/128.vitepress.png (100%) rename resources/devtools-extensions/vue-devtools/{Vue-js-devtools-Chrome-Web-Store => }/icons/16-beta.png (100%) rename resources/devtools-extensions/vue-devtools/{Vue-js-devtools-Chrome-Web-Store => }/icons/16-gray.png (100%) rename resources/devtools-extensions/vue-devtools/{Vue-js-devtools-Chrome-Web-Store => }/icons/16.nuxt.png (100%) rename resources/devtools-extensions/vue-devtools/{Vue-js-devtools-Chrome-Web-Store => }/icons/16.png (100%) rename resources/devtools-extensions/vue-devtools/{Vue-js-devtools-Chrome-Web-Store => }/icons/16.vitepress.png (100%) rename resources/devtools-extensions/vue-devtools/{Vue-js-devtools-Chrome-Web-Store => }/icons/48-beta.png (100%) rename resources/devtools-extensions/vue-devtools/{Vue-js-devtools-Chrome-Web-Store => }/icons/48-gray.png (100%) rename resources/devtools-extensions/vue-devtools/{Vue-js-devtools-Chrome-Web-Store => }/icons/48.nuxt.png (100%) rename resources/devtools-extensions/vue-devtools/{Vue-js-devtools-Chrome-Web-Store => }/icons/48.png (100%) rename resources/devtools-extensions/vue-devtools/{Vue-js-devtools-Chrome-Web-Store => }/icons/48.vitepress.png (100%) rename resources/devtools-extensions/vue-devtools/{Vue-js-devtools-Chrome-Web-Store => }/manifest.json (100%) rename resources/devtools-extensions/vue-devtools/{Vue-js-devtools-Chrome-Web-Store => }/overlay/devtools-overlay.css (100%) rename resources/devtools-extensions/vue-devtools/{Vue-js-devtools-Chrome-Web-Store => }/overlay/devtools-overlay.js (100%) rename resources/devtools-extensions/vue-devtools/{Vue-js-devtools-Chrome-Web-Store => }/package.json (100%) rename resources/devtools-extensions/vue-devtools/{Vue-js-devtools-Chrome-Web-Store => }/pages/devtools-background.html (100%) rename resources/devtools-extensions/vue-devtools/{Vue-js-devtools-Chrome-Web-Store => }/pages/devtools-panel.html (100%) rename resources/devtools-extensions/vue-devtools/{Vue-js-devtools-Chrome-Web-Store => }/popups/devtools-screenshot.png (100%) rename resources/devtools-extensions/vue-devtools/{Vue-js-devtools-Chrome-Web-Store => }/popups/disabled.html (100%) rename resources/devtools-extensions/vue-devtools/{Vue-js-devtools-Chrome-Web-Store => }/popups/disabled.nuxt.html (100%) rename resources/devtools-extensions/vue-devtools/{Vue-js-devtools-Chrome-Web-Store => }/popups/disabled.vitepress.html (100%) rename resources/devtools-extensions/vue-devtools/{Vue-js-devtools-Chrome-Web-Store => }/popups/enabled.html (100%) rename resources/devtools-extensions/vue-devtools/{Vue-js-devtools-Chrome-Web-Store => }/popups/enabled.nuxt.html (100%) rename resources/devtools-extensions/vue-devtools/{Vue-js-devtools-Chrome-Web-Store => }/popups/enabled.vitepress.html (100%) rename resources/devtools-extensions/vue-devtools/{Vue-js-devtools-Chrome-Web-Store => }/popups/not-found.html (100%) rename resources/devtools-extensions/vue-devtools/{Vue-js-devtools-Chrome-Web-Store => }/popups/popup.css (100%) rename resources/devtools-extensions/vue-devtools/{Vue-js-devtools-Chrome-Web-Store => }/popups/vue2-migration-guide.html (100%) diff --git a/resources/devtools-extensions/vue-devtools/Vue-js-devtools-Chrome-Web-Store/_metadata/verified_contents.json b/resources/devtools-extensions/vue-devtools/_metadata/verified_contents.json similarity index 100% rename from resources/devtools-extensions/vue-devtools/Vue-js-devtools-Chrome-Web-Store/_metadata/verified_contents.json rename to resources/devtools-extensions/vue-devtools/_metadata/verified_contents.json diff --git a/resources/devtools-extensions/vue-devtools/Vue-js-devtools-Chrome-Web-Store/client/IconTitle.vue_vue_type_script_setup_true_lang-CzNAudTz.js b/resources/devtools-extensions/vue-devtools/client/IconTitle.vue_vue_type_script_setup_true_lang-CzNAudTz.js similarity index 100% rename from resources/devtools-extensions/vue-devtools/Vue-js-devtools-Chrome-Web-Store/client/IconTitle.vue_vue_type_script_setup_true_lang-CzNAudTz.js rename to resources/devtools-extensions/vue-devtools/client/IconTitle.vue_vue_type_script_setup_true_lang-CzNAudTz.js diff --git a/resources/devtools-extensions/vue-devtools/Vue-js-devtools-Chrome-Web-Store/client/IconTitle.vue_vue_type_script_setup_true_lang-N7sQGPjM.js b/resources/devtools-extensions/vue-devtools/client/IconTitle.vue_vue_type_script_setup_true_lang-N7sQGPjM.js similarity index 100% rename from resources/devtools-extensions/vue-devtools/Vue-js-devtools-Chrome-Web-Store/client/IconTitle.vue_vue_type_script_setup_true_lang-N7sQGPjM.js rename to resources/devtools-extensions/vue-devtools/client/IconTitle.vue_vue_type_script_setup_true_lang-N7sQGPjM.js diff --git a/resources/devtools-extensions/vue-devtools/Vue-js-devtools-Chrome-Web-Store/client/SectionBlock-Baq-oFWF.js b/resources/devtools-extensions/vue-devtools/client/SectionBlock-Baq-oFWF.js similarity index 100% rename from resources/devtools-extensions/vue-devtools/Vue-js-devtools-Chrome-Web-Store/client/SectionBlock-Baq-oFWF.js rename to resources/devtools-extensions/vue-devtools/client/SectionBlock-Baq-oFWF.js diff --git a/resources/devtools-extensions/vue-devtools/Vue-js-devtools-Chrome-Web-Store/client/SectionBlock-Dp0NIF_b.js b/resources/devtools-extensions/vue-devtools/client/SectionBlock-Dp0NIF_b.js similarity index 100% rename from resources/devtools-extensions/vue-devtools/Vue-js-devtools-Chrome-Web-Store/client/SectionBlock-Dp0NIF_b.js rename to resources/devtools-extensions/vue-devtools/client/SectionBlock-Dp0NIF_b.js diff --git a/resources/devtools-extensions/vue-devtools/Vue-js-devtools-Chrome-Web-Store/client/assets-Cenvjhy6.js b/resources/devtools-extensions/vue-devtools/client/assets-Cenvjhy6.js similarity index 100% rename from resources/devtools-extensions/vue-devtools/Vue-js-devtools-Chrome-Web-Store/client/assets-Cenvjhy6.js rename to resources/devtools-extensions/vue-devtools/client/assets-Cenvjhy6.js diff --git a/resources/devtools-extensions/vue-devtools/Vue-js-devtools-Chrome-Web-Store/client/assets-DtK-7ema.js b/resources/devtools-extensions/vue-devtools/client/assets-DtK-7ema.js similarity index 100% rename from resources/devtools-extensions/vue-devtools/Vue-js-devtools-Chrome-Web-Store/client/assets-DtK-7ema.js rename to resources/devtools-extensions/vue-devtools/client/assets-DtK-7ema.js diff --git a/resources/devtools-extensions/vue-devtools/Vue-js-devtools-Chrome-Web-Store/client/color-scheme.js b/resources/devtools-extensions/vue-devtools/client/color-scheme.js similarity index 100% rename from resources/devtools-extensions/vue-devtools/Vue-js-devtools-Chrome-Web-Store/client/color-scheme.js rename to resources/devtools-extensions/vue-devtools/client/color-scheme.js diff --git a/resources/devtools-extensions/vue-devtools/Vue-js-devtools-Chrome-Web-Store/client/components-BgyXz8w_.js b/resources/devtools-extensions/vue-devtools/client/components-BgyXz8w_.js similarity index 100% rename from resources/devtools-extensions/vue-devtools/Vue-js-devtools-Chrome-Web-Store/client/components-BgyXz8w_.js rename to resources/devtools-extensions/vue-devtools/client/components-BgyXz8w_.js diff --git a/resources/devtools-extensions/vue-devtools/Vue-js-devtools-Chrome-Web-Store/client/components-C1pVLxno.js b/resources/devtools-extensions/vue-devtools/client/components-C1pVLxno.js similarity index 100% rename from resources/devtools-extensions/vue-devtools/Vue-js-devtools-Chrome-Web-Store/client/components-C1pVLxno.js rename to resources/devtools-extensions/vue-devtools/client/components-C1pVLxno.js diff --git a/resources/devtools-extensions/vue-devtools/Vue-js-devtools-Chrome-Web-Store/client/css-BnL1064W.js b/resources/devtools-extensions/vue-devtools/client/css-BnL1064W.js similarity index 100% rename from resources/devtools-extensions/vue-devtools/Vue-js-devtools-Chrome-Web-Store/client/css-BnL1064W.js rename to resources/devtools-extensions/vue-devtools/client/css-BnL1064W.js diff --git a/resources/devtools-extensions/vue-devtools/Vue-js-devtools-Chrome-Web-Store/client/custom-inspector-tab-view-CIZhovSF.js b/resources/devtools-extensions/vue-devtools/client/custom-inspector-tab-view-CIZhovSF.js similarity index 100% rename from resources/devtools-extensions/vue-devtools/Vue-js-devtools-Chrome-Web-Store/client/custom-inspector-tab-view-CIZhovSF.js rename to resources/devtools-extensions/vue-devtools/client/custom-inspector-tab-view-CIZhovSF.js diff --git a/resources/devtools-extensions/vue-devtools/Vue-js-devtools-Chrome-Web-Store/client/custom-inspector-tab-view-dZSnsHNr.js b/resources/devtools-extensions/vue-devtools/client/custom-inspector-tab-view-dZSnsHNr.js similarity index 100% rename from resources/devtools-extensions/vue-devtools/Vue-js-devtools-Chrome-Web-Store/client/custom-inspector-tab-view-dZSnsHNr.js rename to resources/devtools-extensions/vue-devtools/client/custom-inspector-tab-view-dZSnsHNr.js diff --git a/resources/devtools-extensions/vue-devtools/Vue-js-devtools-Chrome-Web-Store/client/custom-tab-view-DB2FZ6eN.js b/resources/devtools-extensions/vue-devtools/client/custom-tab-view-DB2FZ6eN.js similarity index 100% rename from resources/devtools-extensions/vue-devtools/Vue-js-devtools-Chrome-Web-Store/client/custom-tab-view-DB2FZ6eN.js rename to resources/devtools-extensions/vue-devtools/client/custom-tab-view-DB2FZ6eN.js diff --git a/resources/devtools-extensions/vue-devtools/Vue-js-devtools-Chrome-Web-Store/client/custom-tab-view-DnTFKhiI.js b/resources/devtools-extensions/vue-devtools/client/custom-tab-view-DnTFKhiI.js similarity index 100% rename from resources/devtools-extensions/vue-devtools/Vue-js-devtools-Chrome-Web-Store/client/custom-tab-view-DnTFKhiI.js rename to resources/devtools-extensions/vue-devtools/client/custom-tab-view-DnTFKhiI.js diff --git a/resources/devtools-extensions/vue-devtools/Vue-js-devtools-Chrome-Web-Store/client/devtools-panel.css b/resources/devtools-extensions/vue-devtools/client/devtools-panel.css similarity index 100% rename from resources/devtools-extensions/vue-devtools/Vue-js-devtools-Chrome-Web-Store/client/devtools-panel.css rename to resources/devtools-extensions/vue-devtools/client/devtools-panel.css diff --git a/resources/devtools-extensions/vue-devtools/Vue-js-devtools-Chrome-Web-Store/client/devtools-panel.js b/resources/devtools-extensions/vue-devtools/client/devtools-panel.js similarity index 100% rename from resources/devtools-extensions/vue-devtools/Vue-js-devtools-Chrome-Web-Store/client/devtools-panel.js rename to resources/devtools-extensions/vue-devtools/client/devtools-panel.js diff --git a/resources/devtools-extensions/vue-devtools/Vue-js-devtools-Chrome-Web-Store/client/diff-DOdUfPSR.js b/resources/devtools-extensions/vue-devtools/client/diff-DOdUfPSR.js similarity index 100% rename from resources/devtools-extensions/vue-devtools/Vue-js-devtools-Chrome-Web-Store/client/diff-DOdUfPSR.js rename to resources/devtools-extensions/vue-devtools/client/diff-DOdUfPSR.js diff --git a/resources/devtools-extensions/vue-devtools/Vue-js-devtools-Chrome-Web-Store/client/graph-CHZkohji.js b/resources/devtools-extensions/vue-devtools/client/graph-CHZkohji.js similarity index 100% rename from resources/devtools-extensions/vue-devtools/Vue-js-devtools-Chrome-Web-Store/client/graph-CHZkohji.js rename to resources/devtools-extensions/vue-devtools/client/graph-CHZkohji.js diff --git a/resources/devtools-extensions/vue-devtools/Vue-js-devtools-Chrome-Web-Store/client/graph-LINFOiRk.js b/resources/devtools-extensions/vue-devtools/client/graph-LINFOiRk.js similarity index 100% rename from resources/devtools-extensions/vue-devtools/Vue-js-devtools-Chrome-Web-Store/client/graph-LINFOiRk.js rename to resources/devtools-extensions/vue-devtools/client/graph-LINFOiRk.js diff --git a/resources/devtools-extensions/vue-devtools/Vue-js-devtools-Chrome-Web-Store/client/html-Iy9EJBkL.js b/resources/devtools-extensions/vue-devtools/client/html-Iy9EJBkL.js similarity index 100% rename from resources/devtools-extensions/vue-devtools/Vue-js-devtools-Chrome-Web-Store/client/html-Iy9EJBkL.js rename to resources/devtools-extensions/vue-devtools/client/html-Iy9EJBkL.js diff --git a/resources/devtools-extensions/vue-devtools/Vue-js-devtools-Chrome-Web-Store/client/index-B-XIiefs.js b/resources/devtools-extensions/vue-devtools/client/index-B-XIiefs.js similarity index 100% rename from resources/devtools-extensions/vue-devtools/Vue-js-devtools-Chrome-Web-Store/client/index-B-XIiefs.js rename to resources/devtools-extensions/vue-devtools/client/index-B-XIiefs.js diff --git a/resources/devtools-extensions/vue-devtools/Vue-js-devtools-Chrome-Web-Store/client/index-C7XPmmLS.js b/resources/devtools-extensions/vue-devtools/client/index-C7XPmmLS.js similarity index 100% rename from resources/devtools-extensions/vue-devtools/Vue-js-devtools-Chrome-Web-Store/client/index-C7XPmmLS.js rename to resources/devtools-extensions/vue-devtools/client/index-C7XPmmLS.js diff --git a/resources/devtools-extensions/vue-devtools/Vue-js-devtools-Chrome-Web-Store/client/javascript-CXHlxgtu.js b/resources/devtools-extensions/vue-devtools/client/javascript-CXHlxgtu.js similarity index 100% rename from resources/devtools-extensions/vue-devtools/Vue-js-devtools-Chrome-Web-Store/client/javascript-CXHlxgtu.js rename to resources/devtools-extensions/vue-devtools/client/javascript-CXHlxgtu.js diff --git a/resources/devtools-extensions/vue-devtools/Vue-js-devtools-Chrome-Web-Store/client/json-D4cXEhM0.js b/resources/devtools-extensions/vue-devtools/client/json-D4cXEhM0.js similarity index 100% rename from resources/devtools-extensions/vue-devtools/Vue-js-devtools-Chrome-Web-Store/client/json-D4cXEhM0.js rename to resources/devtools-extensions/vue-devtools/client/json-D4cXEhM0.js diff --git a/resources/devtools-extensions/vue-devtools/Vue-js-devtools-Chrome-Web-Store/client/logo.svg b/resources/devtools-extensions/vue-devtools/client/logo.svg similarity index 100% rename from resources/devtools-extensions/vue-devtools/Vue-js-devtools-Chrome-Web-Store/client/logo.svg rename to resources/devtools-extensions/vue-devtools/client/logo.svg diff --git a/resources/devtools-extensions/vue-devtools/Vue-js-devtools-Chrome-Web-Store/client/overview-B6qofkEm.js b/resources/devtools-extensions/vue-devtools/client/overview-B6qofkEm.js similarity index 100% rename from resources/devtools-extensions/vue-devtools/Vue-js-devtools-Chrome-Web-Store/client/overview-B6qofkEm.js rename to resources/devtools-extensions/vue-devtools/client/overview-B6qofkEm.js diff --git a/resources/devtools-extensions/vue-devtools/Vue-js-devtools-Chrome-Web-Store/client/overview-NOHiLwdq.js b/resources/devtools-extensions/vue-devtools/client/overview-NOHiLwdq.js similarity index 100% rename from resources/devtools-extensions/vue-devtools/Vue-js-devtools-Chrome-Web-Store/client/overview-NOHiLwdq.js rename to resources/devtools-extensions/vue-devtools/client/overview-NOHiLwdq.js diff --git a/resources/devtools-extensions/vue-devtools/Vue-js-devtools-Chrome-Web-Store/client/pages-Bqu7i9_W.js b/resources/devtools-extensions/vue-devtools/client/pages-Bqu7i9_W.js similarity index 100% rename from resources/devtools-extensions/vue-devtools/Vue-js-devtools-Chrome-Web-Store/client/pages-Bqu7i9_W.js rename to resources/devtools-extensions/vue-devtools/client/pages-Bqu7i9_W.js diff --git a/resources/devtools-extensions/vue-devtools/Vue-js-devtools-Chrome-Web-Store/client/pages-C5V9CoPY.js b/resources/devtools-extensions/vue-devtools/client/pages-C5V9CoPY.js similarity index 100% rename from resources/devtools-extensions/vue-devtools/Vue-js-devtools-Chrome-Web-Store/client/pages-C5V9CoPY.js rename to resources/devtools-extensions/vue-devtools/client/pages-C5V9CoPY.js diff --git a/resources/devtools-extensions/vue-devtools/Vue-js-devtools-Chrome-Web-Store/client/pinia-BPran779.js b/resources/devtools-extensions/vue-devtools/client/pinia-BPran779.js similarity index 100% rename from resources/devtools-extensions/vue-devtools/Vue-js-devtools-Chrome-Web-Store/client/pinia-BPran779.js rename to resources/devtools-extensions/vue-devtools/client/pinia-BPran779.js diff --git a/resources/devtools-extensions/vue-devtools/Vue-js-devtools-Chrome-Web-Store/client/pinia-VeJqP-Jl.js b/resources/devtools-extensions/vue-devtools/client/pinia-VeJqP-Jl.js similarity index 100% rename from resources/devtools-extensions/vue-devtools/Vue-js-devtools-Chrome-Web-Store/client/pinia-VeJqP-Jl.js rename to resources/devtools-extensions/vue-devtools/client/pinia-VeJqP-Jl.js diff --git a/resources/devtools-extensions/vue-devtools/Vue-js-devtools-Chrome-Web-Store/client/router-BW0WfY9R.js b/resources/devtools-extensions/vue-devtools/client/router-BW0WfY9R.js similarity index 100% rename from resources/devtools-extensions/vue-devtools/Vue-js-devtools-Chrome-Web-Store/client/router-BW0WfY9R.js rename to resources/devtools-extensions/vue-devtools/client/router-BW0WfY9R.js diff --git a/resources/devtools-extensions/vue-devtools/Vue-js-devtools-Chrome-Web-Store/client/router-BbI-eMq9.js b/resources/devtools-extensions/vue-devtools/client/router-BbI-eMq9.js similarity index 100% rename from resources/devtools-extensions/vue-devtools/Vue-js-devtools-Chrome-Web-Store/client/router-BbI-eMq9.js rename to resources/devtools-extensions/vue-devtools/client/router-BbI-eMq9.js diff --git a/resources/devtools-extensions/vue-devtools/Vue-js-devtools-Chrome-Web-Store/client/settings-DTrnopyF.js b/resources/devtools-extensions/vue-devtools/client/settings-DTrnopyF.js similarity index 100% rename from resources/devtools-extensions/vue-devtools/Vue-js-devtools-Chrome-Web-Store/client/settings-DTrnopyF.js rename to resources/devtools-extensions/vue-devtools/client/settings-DTrnopyF.js diff --git a/resources/devtools-extensions/vue-devtools/Vue-js-devtools-Chrome-Web-Store/client/settings-DnVNSbhN.js b/resources/devtools-extensions/vue-devtools/client/settings-DnVNSbhN.js similarity index 100% rename from resources/devtools-extensions/vue-devtools/Vue-js-devtools-Chrome-Web-Store/client/settings-DnVNSbhN.js rename to resources/devtools-extensions/vue-devtools/client/settings-DnVNSbhN.js diff --git a/resources/devtools-extensions/vue-devtools/Vue-js-devtools-Chrome-Web-Store/client/shellscript-DmhSIIKI.js b/resources/devtools-extensions/vue-devtools/client/shellscript-DmhSIIKI.js similarity index 100% rename from resources/devtools-extensions/vue-devtools/Vue-js-devtools-Chrome-Web-Store/client/shellscript-DmhSIIKI.js rename to resources/devtools-extensions/vue-devtools/client/shellscript-DmhSIIKI.js diff --git a/resources/devtools-extensions/vue-devtools/Vue-js-devtools-Chrome-Web-Store/client/timeline-CVQgEdyi.js b/resources/devtools-extensions/vue-devtools/client/timeline-CVQgEdyi.js similarity index 100% rename from resources/devtools-extensions/vue-devtools/Vue-js-devtools-Chrome-Web-Store/client/timeline-CVQgEdyi.js rename to resources/devtools-extensions/vue-devtools/client/timeline-CVQgEdyi.js diff --git a/resources/devtools-extensions/vue-devtools/Vue-js-devtools-Chrome-Web-Store/client/timeline-Cwu3caRC.js b/resources/devtools-extensions/vue-devtools/client/timeline-Cwu3caRC.js similarity index 100% rename from resources/devtools-extensions/vue-devtools/Vue-js-devtools-Chrome-Web-Store/client/timeline-Cwu3caRC.js rename to resources/devtools-extensions/vue-devtools/client/timeline-Cwu3caRC.js diff --git a/resources/devtools-extensions/vue-devtools/Vue-js-devtools-Chrome-Web-Store/client/typescript-DKfvlBCw.js b/resources/devtools-extensions/vue-devtools/client/typescript-DKfvlBCw.js similarity index 100% rename from resources/devtools-extensions/vue-devtools/Vue-js-devtools-Chrome-Web-Store/client/typescript-DKfvlBCw.js rename to resources/devtools-extensions/vue-devtools/client/typescript-DKfvlBCw.js diff --git a/resources/devtools-extensions/vue-devtools/Vue-js-devtools-Chrome-Web-Store/client/vitesse-dark-Cym-eLtO.js b/resources/devtools-extensions/vue-devtools/client/vitesse-dark-Cym-eLtO.js similarity index 100% rename from resources/devtools-extensions/vue-devtools/Vue-js-devtools-Chrome-Web-Store/client/vitesse-dark-Cym-eLtO.js rename to resources/devtools-extensions/vue-devtools/client/vitesse-dark-Cym-eLtO.js diff --git a/resources/devtools-extensions/vue-devtools/Vue-js-devtools-Chrome-Web-Store/client/vitesse-light-CcmG315c.js b/resources/devtools-extensions/vue-devtools/client/vitesse-light-CcmG315c.js similarity index 100% rename from resources/devtools-extensions/vue-devtools/Vue-js-devtools-Chrome-Web-Store/client/vitesse-light-CcmG315c.js rename to resources/devtools-extensions/vue-devtools/client/vitesse-light-CcmG315c.js diff --git a/resources/devtools-extensions/vue-devtools/Vue-js-devtools-Chrome-Web-Store/client/vue-ChaKAHzo.js b/resources/devtools-extensions/vue-devtools/client/vue-ChaKAHzo.js similarity index 100% rename from resources/devtools-extensions/vue-devtools/Vue-js-devtools-Chrome-Web-Store/client/vue-ChaKAHzo.js rename to resources/devtools-extensions/vue-devtools/client/vue-ChaKAHzo.js diff --git a/resources/devtools-extensions/vue-devtools/Vue-js-devtools-Chrome-Web-Store/client/vue-apis-C8_kYYSp.js b/resources/devtools-extensions/vue-devtools/client/vue-apis-C8_kYYSp.js similarity index 100% rename from resources/devtools-extensions/vue-devtools/Vue-js-devtools-Chrome-Web-Store/client/vue-apis-C8_kYYSp.js rename to resources/devtools-extensions/vue-devtools/client/vue-apis-C8_kYYSp.js diff --git a/resources/devtools-extensions/vue-devtools/Vue-js-devtools-Chrome-Web-Store/client/vue-html-Hg1VsDox.js b/resources/devtools-extensions/vue-devtools/client/vue-html-Hg1VsDox.js similarity index 100% rename from resources/devtools-extensions/vue-devtools/Vue-js-devtools-Chrome-Web-Store/client/vue-html-Hg1VsDox.js rename to resources/devtools-extensions/vue-devtools/client/vue-html-Hg1VsDox.js diff --git a/resources/devtools-extensions/vue-devtools/Vue-js-devtools-Chrome-Web-Store/client/yaml-DcQhbMrL.js b/resources/devtools-extensions/vue-devtools/client/yaml-DcQhbMrL.js similarity index 100% rename from resources/devtools-extensions/vue-devtools/Vue-js-devtools-Chrome-Web-Store/client/yaml-DcQhbMrL.js rename to resources/devtools-extensions/vue-devtools/client/yaml-DcQhbMrL.js diff --git a/resources/devtools-extensions/vue-devtools/Vue-js-devtools-Chrome-Web-Store/dist/background.js b/resources/devtools-extensions/vue-devtools/dist/background.js similarity index 100% rename from resources/devtools-extensions/vue-devtools/Vue-js-devtools-Chrome-Web-Store/dist/background.js rename to resources/devtools-extensions/vue-devtools/dist/background.js diff --git a/resources/devtools-extensions/vue-devtools/Vue-js-devtools-Chrome-Web-Store/dist/detector.js b/resources/devtools-extensions/vue-devtools/dist/detector.js similarity index 100% rename from resources/devtools-extensions/vue-devtools/Vue-js-devtools-Chrome-Web-Store/dist/detector.js rename to resources/devtools-extensions/vue-devtools/dist/detector.js diff --git a/resources/devtools-extensions/vue-devtools/Vue-js-devtools-Chrome-Web-Store/dist/devtools-background.js b/resources/devtools-extensions/vue-devtools/dist/devtools-background.js similarity index 100% rename from resources/devtools-extensions/vue-devtools/Vue-js-devtools-Chrome-Web-Store/dist/devtools-background.js rename to resources/devtools-extensions/vue-devtools/dist/devtools-background.js diff --git a/resources/devtools-extensions/vue-devtools/Vue-js-devtools-Chrome-Web-Store/dist/devtools-overlay.js b/resources/devtools-extensions/vue-devtools/dist/devtools-overlay.js similarity index 100% rename from resources/devtools-extensions/vue-devtools/Vue-js-devtools-Chrome-Web-Store/dist/devtools-overlay.js rename to resources/devtools-extensions/vue-devtools/dist/devtools-overlay.js diff --git a/resources/devtools-extensions/vue-devtools/Vue-js-devtools-Chrome-Web-Store/dist/devtools-panel.js b/resources/devtools-extensions/vue-devtools/dist/devtools-panel.js similarity index 100% rename from resources/devtools-extensions/vue-devtools/Vue-js-devtools-Chrome-Web-Store/dist/devtools-panel.js rename to resources/devtools-extensions/vue-devtools/dist/devtools-panel.js diff --git a/resources/devtools-extensions/vue-devtools/Vue-js-devtools-Chrome-Web-Store/dist/prepare.js b/resources/devtools-extensions/vue-devtools/dist/prepare.js similarity index 100% rename from resources/devtools-extensions/vue-devtools/Vue-js-devtools-Chrome-Web-Store/dist/prepare.js rename to resources/devtools-extensions/vue-devtools/dist/prepare.js diff --git a/resources/devtools-extensions/vue-devtools/Vue-js-devtools-Chrome-Web-Store/dist/proxy.js b/resources/devtools-extensions/vue-devtools/dist/proxy.js similarity index 100% rename from resources/devtools-extensions/vue-devtools/Vue-js-devtools-Chrome-Web-Store/dist/proxy.js rename to resources/devtools-extensions/vue-devtools/dist/proxy.js diff --git a/resources/devtools-extensions/vue-devtools/Vue-js-devtools-Chrome-Web-Store/dist/user-app.js b/resources/devtools-extensions/vue-devtools/dist/user-app.js similarity index 100% rename from resources/devtools-extensions/vue-devtools/Vue-js-devtools-Chrome-Web-Store/dist/user-app.js rename to resources/devtools-extensions/vue-devtools/dist/user-app.js diff --git a/resources/devtools-extensions/vue-devtools/Vue-js-devtools-Chrome-Web-Store/icons/128-beta.png b/resources/devtools-extensions/vue-devtools/icons/128-beta.png similarity index 100% rename from resources/devtools-extensions/vue-devtools/Vue-js-devtools-Chrome-Web-Store/icons/128-beta.png rename to resources/devtools-extensions/vue-devtools/icons/128-beta.png diff --git a/resources/devtools-extensions/vue-devtools/Vue-js-devtools-Chrome-Web-Store/icons/128-gray.png b/resources/devtools-extensions/vue-devtools/icons/128-gray.png similarity index 100% rename from resources/devtools-extensions/vue-devtools/Vue-js-devtools-Chrome-Web-Store/icons/128-gray.png rename to resources/devtools-extensions/vue-devtools/icons/128-gray.png diff --git a/resources/devtools-extensions/vue-devtools/Vue-js-devtools-Chrome-Web-Store/icons/128.nuxt.png b/resources/devtools-extensions/vue-devtools/icons/128.nuxt.png similarity index 100% rename from resources/devtools-extensions/vue-devtools/Vue-js-devtools-Chrome-Web-Store/icons/128.nuxt.png rename to resources/devtools-extensions/vue-devtools/icons/128.nuxt.png diff --git a/resources/devtools-extensions/vue-devtools/Vue-js-devtools-Chrome-Web-Store/icons/128.png b/resources/devtools-extensions/vue-devtools/icons/128.png similarity index 100% rename from resources/devtools-extensions/vue-devtools/Vue-js-devtools-Chrome-Web-Store/icons/128.png rename to resources/devtools-extensions/vue-devtools/icons/128.png diff --git a/resources/devtools-extensions/vue-devtools/Vue-js-devtools-Chrome-Web-Store/icons/128.vitepress.png b/resources/devtools-extensions/vue-devtools/icons/128.vitepress.png similarity index 100% rename from resources/devtools-extensions/vue-devtools/Vue-js-devtools-Chrome-Web-Store/icons/128.vitepress.png rename to resources/devtools-extensions/vue-devtools/icons/128.vitepress.png diff --git a/resources/devtools-extensions/vue-devtools/Vue-js-devtools-Chrome-Web-Store/icons/16-beta.png b/resources/devtools-extensions/vue-devtools/icons/16-beta.png similarity index 100% rename from resources/devtools-extensions/vue-devtools/Vue-js-devtools-Chrome-Web-Store/icons/16-beta.png rename to resources/devtools-extensions/vue-devtools/icons/16-beta.png diff --git a/resources/devtools-extensions/vue-devtools/Vue-js-devtools-Chrome-Web-Store/icons/16-gray.png b/resources/devtools-extensions/vue-devtools/icons/16-gray.png similarity index 100% rename from resources/devtools-extensions/vue-devtools/Vue-js-devtools-Chrome-Web-Store/icons/16-gray.png rename to resources/devtools-extensions/vue-devtools/icons/16-gray.png diff --git a/resources/devtools-extensions/vue-devtools/Vue-js-devtools-Chrome-Web-Store/icons/16.nuxt.png b/resources/devtools-extensions/vue-devtools/icons/16.nuxt.png similarity index 100% rename from resources/devtools-extensions/vue-devtools/Vue-js-devtools-Chrome-Web-Store/icons/16.nuxt.png rename to resources/devtools-extensions/vue-devtools/icons/16.nuxt.png diff --git a/resources/devtools-extensions/vue-devtools/Vue-js-devtools-Chrome-Web-Store/icons/16.png b/resources/devtools-extensions/vue-devtools/icons/16.png similarity index 100% rename from resources/devtools-extensions/vue-devtools/Vue-js-devtools-Chrome-Web-Store/icons/16.png rename to resources/devtools-extensions/vue-devtools/icons/16.png diff --git a/resources/devtools-extensions/vue-devtools/Vue-js-devtools-Chrome-Web-Store/icons/16.vitepress.png b/resources/devtools-extensions/vue-devtools/icons/16.vitepress.png similarity index 100% rename from resources/devtools-extensions/vue-devtools/Vue-js-devtools-Chrome-Web-Store/icons/16.vitepress.png rename to resources/devtools-extensions/vue-devtools/icons/16.vitepress.png diff --git a/resources/devtools-extensions/vue-devtools/Vue-js-devtools-Chrome-Web-Store/icons/48-beta.png b/resources/devtools-extensions/vue-devtools/icons/48-beta.png similarity index 100% rename from resources/devtools-extensions/vue-devtools/Vue-js-devtools-Chrome-Web-Store/icons/48-beta.png rename to resources/devtools-extensions/vue-devtools/icons/48-beta.png diff --git a/resources/devtools-extensions/vue-devtools/Vue-js-devtools-Chrome-Web-Store/icons/48-gray.png b/resources/devtools-extensions/vue-devtools/icons/48-gray.png similarity index 100% rename from resources/devtools-extensions/vue-devtools/Vue-js-devtools-Chrome-Web-Store/icons/48-gray.png rename to resources/devtools-extensions/vue-devtools/icons/48-gray.png diff --git a/resources/devtools-extensions/vue-devtools/Vue-js-devtools-Chrome-Web-Store/icons/48.nuxt.png b/resources/devtools-extensions/vue-devtools/icons/48.nuxt.png similarity index 100% rename from resources/devtools-extensions/vue-devtools/Vue-js-devtools-Chrome-Web-Store/icons/48.nuxt.png rename to resources/devtools-extensions/vue-devtools/icons/48.nuxt.png diff --git a/resources/devtools-extensions/vue-devtools/Vue-js-devtools-Chrome-Web-Store/icons/48.png b/resources/devtools-extensions/vue-devtools/icons/48.png similarity index 100% rename from resources/devtools-extensions/vue-devtools/Vue-js-devtools-Chrome-Web-Store/icons/48.png rename to resources/devtools-extensions/vue-devtools/icons/48.png diff --git a/resources/devtools-extensions/vue-devtools/Vue-js-devtools-Chrome-Web-Store/icons/48.vitepress.png b/resources/devtools-extensions/vue-devtools/icons/48.vitepress.png similarity index 100% rename from resources/devtools-extensions/vue-devtools/Vue-js-devtools-Chrome-Web-Store/icons/48.vitepress.png rename to resources/devtools-extensions/vue-devtools/icons/48.vitepress.png diff --git a/resources/devtools-extensions/vue-devtools/Vue-js-devtools-Chrome-Web-Store/manifest.json b/resources/devtools-extensions/vue-devtools/manifest.json similarity index 100% rename from resources/devtools-extensions/vue-devtools/Vue-js-devtools-Chrome-Web-Store/manifest.json rename to resources/devtools-extensions/vue-devtools/manifest.json diff --git a/resources/devtools-extensions/vue-devtools/Vue-js-devtools-Chrome-Web-Store/overlay/devtools-overlay.css b/resources/devtools-extensions/vue-devtools/overlay/devtools-overlay.css similarity index 100% rename from resources/devtools-extensions/vue-devtools/Vue-js-devtools-Chrome-Web-Store/overlay/devtools-overlay.css rename to resources/devtools-extensions/vue-devtools/overlay/devtools-overlay.css diff --git a/resources/devtools-extensions/vue-devtools/Vue-js-devtools-Chrome-Web-Store/overlay/devtools-overlay.js b/resources/devtools-extensions/vue-devtools/overlay/devtools-overlay.js similarity index 100% rename from resources/devtools-extensions/vue-devtools/Vue-js-devtools-Chrome-Web-Store/overlay/devtools-overlay.js rename to resources/devtools-extensions/vue-devtools/overlay/devtools-overlay.js diff --git a/resources/devtools-extensions/vue-devtools/Vue-js-devtools-Chrome-Web-Store/package.json b/resources/devtools-extensions/vue-devtools/package.json similarity index 100% rename from resources/devtools-extensions/vue-devtools/Vue-js-devtools-Chrome-Web-Store/package.json rename to resources/devtools-extensions/vue-devtools/package.json diff --git a/resources/devtools-extensions/vue-devtools/Vue-js-devtools-Chrome-Web-Store/pages/devtools-background.html b/resources/devtools-extensions/vue-devtools/pages/devtools-background.html similarity index 100% rename from resources/devtools-extensions/vue-devtools/Vue-js-devtools-Chrome-Web-Store/pages/devtools-background.html rename to resources/devtools-extensions/vue-devtools/pages/devtools-background.html diff --git a/resources/devtools-extensions/vue-devtools/Vue-js-devtools-Chrome-Web-Store/pages/devtools-panel.html b/resources/devtools-extensions/vue-devtools/pages/devtools-panel.html similarity index 100% rename from resources/devtools-extensions/vue-devtools/Vue-js-devtools-Chrome-Web-Store/pages/devtools-panel.html rename to resources/devtools-extensions/vue-devtools/pages/devtools-panel.html diff --git a/resources/devtools-extensions/vue-devtools/Vue-js-devtools-Chrome-Web-Store/popups/devtools-screenshot.png b/resources/devtools-extensions/vue-devtools/popups/devtools-screenshot.png similarity index 100% rename from resources/devtools-extensions/vue-devtools/Vue-js-devtools-Chrome-Web-Store/popups/devtools-screenshot.png rename to resources/devtools-extensions/vue-devtools/popups/devtools-screenshot.png diff --git a/resources/devtools-extensions/vue-devtools/Vue-js-devtools-Chrome-Web-Store/popups/disabled.html b/resources/devtools-extensions/vue-devtools/popups/disabled.html similarity index 100% rename from resources/devtools-extensions/vue-devtools/Vue-js-devtools-Chrome-Web-Store/popups/disabled.html rename to resources/devtools-extensions/vue-devtools/popups/disabled.html diff --git a/resources/devtools-extensions/vue-devtools/Vue-js-devtools-Chrome-Web-Store/popups/disabled.nuxt.html b/resources/devtools-extensions/vue-devtools/popups/disabled.nuxt.html similarity index 100% rename from resources/devtools-extensions/vue-devtools/Vue-js-devtools-Chrome-Web-Store/popups/disabled.nuxt.html rename to resources/devtools-extensions/vue-devtools/popups/disabled.nuxt.html diff --git a/resources/devtools-extensions/vue-devtools/Vue-js-devtools-Chrome-Web-Store/popups/disabled.vitepress.html b/resources/devtools-extensions/vue-devtools/popups/disabled.vitepress.html similarity index 100% rename from resources/devtools-extensions/vue-devtools/Vue-js-devtools-Chrome-Web-Store/popups/disabled.vitepress.html rename to resources/devtools-extensions/vue-devtools/popups/disabled.vitepress.html diff --git a/resources/devtools-extensions/vue-devtools/Vue-js-devtools-Chrome-Web-Store/popups/enabled.html b/resources/devtools-extensions/vue-devtools/popups/enabled.html similarity index 100% rename from resources/devtools-extensions/vue-devtools/Vue-js-devtools-Chrome-Web-Store/popups/enabled.html rename to resources/devtools-extensions/vue-devtools/popups/enabled.html diff --git a/resources/devtools-extensions/vue-devtools/Vue-js-devtools-Chrome-Web-Store/popups/enabled.nuxt.html b/resources/devtools-extensions/vue-devtools/popups/enabled.nuxt.html similarity index 100% rename from resources/devtools-extensions/vue-devtools/Vue-js-devtools-Chrome-Web-Store/popups/enabled.nuxt.html rename to resources/devtools-extensions/vue-devtools/popups/enabled.nuxt.html diff --git a/resources/devtools-extensions/vue-devtools/Vue-js-devtools-Chrome-Web-Store/popups/enabled.vitepress.html b/resources/devtools-extensions/vue-devtools/popups/enabled.vitepress.html similarity index 100% rename from resources/devtools-extensions/vue-devtools/Vue-js-devtools-Chrome-Web-Store/popups/enabled.vitepress.html rename to resources/devtools-extensions/vue-devtools/popups/enabled.vitepress.html diff --git a/resources/devtools-extensions/vue-devtools/Vue-js-devtools-Chrome-Web-Store/popups/not-found.html b/resources/devtools-extensions/vue-devtools/popups/not-found.html similarity index 100% rename from resources/devtools-extensions/vue-devtools/Vue-js-devtools-Chrome-Web-Store/popups/not-found.html rename to resources/devtools-extensions/vue-devtools/popups/not-found.html diff --git a/resources/devtools-extensions/vue-devtools/Vue-js-devtools-Chrome-Web-Store/popups/popup.css b/resources/devtools-extensions/vue-devtools/popups/popup.css similarity index 100% rename from resources/devtools-extensions/vue-devtools/Vue-js-devtools-Chrome-Web-Store/popups/popup.css rename to resources/devtools-extensions/vue-devtools/popups/popup.css diff --git a/resources/devtools-extensions/vue-devtools/Vue-js-devtools-Chrome-Web-Store/popups/vue2-migration-guide.html b/resources/devtools-extensions/vue-devtools/popups/vue2-migration-guide.html similarity index 100% rename from resources/devtools-extensions/vue-devtools/Vue-js-devtools-Chrome-Web-Store/popups/vue2-migration-guide.html rename to resources/devtools-extensions/vue-devtools/popups/vue2-migration-guide.html diff --git a/src/vs/workbench/contrib/roopik/electron-main/channel/roopikToolsChannel.ts b/src/vs/workbench/contrib/roopik/electron-main/channel/roopikToolsChannel.ts index 4335494c4..2f704fd9f 100644 --- a/src/vs/workbench/contrib/roopik/electron-main/channel/roopikToolsChannel.ts +++ b/src/vs/workbench/contrib/roopik/electron-main/channel/roopikToolsChannel.ts @@ -83,7 +83,7 @@ export class RoopikToolsChannel implements IServerChannel { this.browserToolService = new BrowserToolService(browserViewService, this.cdpMonitorService); this.canvasToolService = new CanvasToolService(canvasService); this.componentToolService = new ComponentToolService(componentService, canvasService); - this.projectToolService = new ProjectToolService(devServerService); + this.projectToolService = new ProjectToolService(devServerService, storageService); } /** diff --git a/src/vs/workbench/contrib/roopik/electron-main/mcp/executor/toolExecutor.ts b/src/vs/workbench/contrib/roopik/electron-main/mcp/executor/toolExecutor.ts index d2f3da100..f5eb55f8d 100644 --- a/src/vs/workbench/contrib/roopik/electron-main/mcp/executor/toolExecutor.ts +++ b/src/vs/workbench/contrib/roopik/electron-main/mcp/executor/toolExecutor.ts @@ -73,7 +73,7 @@ export class ToolExecutor { this.browserToolService = new BrowserToolService(browserViewService, this.cdpMonitorService); this.canvasToolService = new CanvasToolService(canvasService); this.componentToolService = new ComponentToolService(componentService, canvasService); - this.projectToolService = new ProjectToolService(devServerService); + this.projectToolService = new ProjectToolService(devServerService, storageService); } /** From 30134673532244f0ef700e77e645aa57d158b5c6 Mon Sep 17 00:00:00 2001 From: Dinesh Yadav <13635627+HumbleBee14@users.noreply.github.com> Date: Sat, 31 Jan 2026 16:57:49 -0500 Subject: [PATCH 7/8] Add browser tools, canvas validation & screenshots --- .../roopik-roo/packages/types/src/mode.ts | 60 +++-- .../roopik-roo/packages/types/src/tool.ts | 11 +- .../assistant-message/NativeToolCallParser.ts | 58 ++++- .../presentAssistantMessage.ts | 18 +- .../src/core/prompts/sections/capabilities.ts | 4 +- .../core/prompts/tools/native-tools/roopik.ts | 24 +- .../core/prompts/tools/roopik/roopik-tools.ts | 5 +- .../core/tools/roopik/RoopikToolHandler.ts | 10 +- .../src/services/roopik/RoopikToolClient.ts | 10 + extensions/roopik-roo/src/shared/tools.ts | 33 ++- .../src/components/chat/ChatRow.tsx | 46 +++- .../webview-ui/src/i18n/locales/en/chat.json | 12 +- extensions/roopik/src/canvasPanel.ts | 36 +++ extensions/roopik/src/extension.ts | 32 ++- .../roopik/src/roopikExtensionManager.ts | 22 ++ .../src/componentView/ComponentView.tsx | 30 ++- .../_metadata/verified_contents.json | 1 - .../browser/commands/browserCommands.ts | 4 +- .../roopik/browser/commands/mcpCommands.ts | 2 +- .../browser/commands/roopikToolsCommands.ts | 46 +++- .../roopik/browser/componentServiceClient.ts | 26 +- .../contributions/componentContribution.ts | 38 +++ .../common/component/componentService.ts | 38 +++ .../electron-main/channel/componentChannel.ts | 13 + .../channel/roopikToolsChannel.ts | 25 +- .../component/componentService.ts | 60 ++++- .../mcp/executor/toolExecutor.ts | 32 ++- .../electron-main/mcp/mcpServerService.ts | 12 +- .../roopik/electron-main/mcp/toolSchemas.ts | 45 +++- .../projectMode/browserViewService.ts | 32 ++- .../electron-main/tools/browserToolService.ts | 14 +- .../electron-main/tools/canvasToolService.ts | 90 ++++++- .../electron-main/tools/cdpMonitorService.ts | 32 ++- .../tools/componentToolService.ts | 173 +++++++++++++ .../electron-main/tools/projectToolService.ts | 72 +++++- tools/roopik/mcp-stdio-server/src/tools.ts | 230 +++++++++++++++++- 36 files changed, 1287 insertions(+), 109 deletions(-) delete mode 100644 resources/devtools-extensions/vue-devtools/_metadata/verified_contents.json diff --git a/extensions/roopik-roo/packages/types/src/mode.ts b/extensions/roopik-roo/packages/types/src/mode.ts index 2f35e975b..368264954 100644 --- a/extensions/roopik-roo/packages/types/src/mode.ts +++ b/extensions/roopik-roo/packages/types/src/mode.ts @@ -140,7 +140,7 @@ export const DEFAULT_MODES: readonly ModeConfig[] = [ roleDefinition: "You are Dio, a highly skilled software engineer with extensive knowledge in many programming languages, frameworks, design patterns, and best practices.", whenToUse: - "Use this mode when you need to write, modify, or refactor code. This includes backend, data processing, algorithms, AND frontend/UI work. Roopik tools (canvas, components, browser preview) are available in this mode - you can create and preview UI components without switching modes.", + "Use this mode when you need to write, modify, or refactor code. This includes backend, data processing, algorithms, AND frontend/UI work. Roopik tools (canvas, components, browser preview) are available in this mode - but use browser and project tools only in code mode, else for any canvas related task ask the user if theyw ant to swithc to Design mode.", description: "Write, modify, and refactor code", groups: ["read", "edit", "browser", "command", "mcp", "roopik"], }, @@ -152,7 +152,7 @@ export const DEFAULT_MODES: readonly ModeConfig[] = [ whenToUse: "Use this mode when you need explanations, documentation, or answers to technical questions. Best for understanding concepts, analyzing existing code, getting recommendations, or learning about technologies without making changes.", description: "Get answers and explanations", - groups: ["read", "browser", "mcp", "roopik"], + groups: ["read", "browser", "mcp"], customInstructions: "You can analyze code, explain concepts, and access external resources. Always answer the user's questions thoroughly, and do not switch to implementing code unless explicitly requested by the user. Include Mermaid diagrams when they clarify your response.", }, @@ -171,15 +171,46 @@ export const DEFAULT_MODES: readonly ModeConfig[] = [ - Project mode is used to build, run and preview full projects using the embedded chromium browser. It can also be used to preview existing vite based projects. NOTE: If the project is not vite based, you can run them without project_start using terminal and other native tools and directly show them in the browser using navigate, else if its vite based supported, then you can let the Roopik IDE project mode start it internally by project tools. -**DESIGN CREATIVITY (Be Professional, Not Generic):** -- **Ask once** at the start of a design task: What's the vibe/aesthetic? Suggest 2-3 directions based on context. Don't repeat this question - remember their preference. -- **Be creative** - Avoid the same safe patterns. Each component should feel intentional and polished: - - Fresh color palettes, not default blues - - Modern animations with framer-motion (hover states, transitions, micro-interactions) - - Interesting layouts (asymmetry, layering, creative spacing) - - Contemporary trends when appropriate (glassmorphism, gradients, mesh backgrounds) -- **Use modern libraries freely**: framer-motion, lucide-react, @headlessui/react, @radix-ui/* - all auto-resolved -- **Think like a designer**: Consider visual hierarchy, whitespace, typography pairing, subtle shadows, and polish +**DESIGN AESTHETICS (Production-Grade, Not Generic AI Slop):** + +Before coding, commit to a BOLD aesthetic direction. Pick a tone: brutally minimal, maximalist chaos, retro-futuristic, organic/natural, luxury/refined, playful/toy-like, editorial/magazine, brutalist/raw, art deco/geometric, soft/pastel, industrial/utilitarian. Execute with precision. + +- **Ask once** at the start: What's the vibe/aesthetic? Suggest 2-3 directions based on context. Remember their preference. + +**Typography (CRITICAL - Avoid Generic):** +- NEVER use: Inter, Roboto, Arial, system fonts, or overused choices like Space Grotesk +- DO use: Distinctive, characterful fonts that elevate the design +- Pair a display font (headings) with a refined body font +- Google Fonts examples: Playfair Display, Cormorant, Bricolage Grotesque, Outfit, Syne, Cabinet Grotesk, Satoshi, General Sans + +**Color & Theme:** +- Commit to a cohesive palette - use CSS variables for consistency +- Dominant colors with sharp accents outperform timid, evenly-distributed palettes +- AVOID: Cliched purple gradients on white, generic blue CTAs +- Consider: Earthy tones, monochromatic schemes, unexpected color pairings, dark themes with vibrant accents + +**Motion & Animation:** +- Use framer-motion for React components (auto-resolved via CDN) +- Focus on high-impact moments: staggered page load reveals (animation-delay), orchestrated entrances +- Scroll-triggered animations and hover states that surprise +- Micro-interactions on buttons, cards, inputs - subtle but delightful + +**Spatial Composition:** +- Unexpected layouts: asymmetry, overlap, diagonal flow, grid-breaking elements +- Generous negative space OR controlled density - commit to one +- Break the grid intentionally for visual interest + +**Backgrounds & Visual Details:** +- Create atmosphere - never default to plain solid colors +- Use: gradient meshes, noise/grain textures, geometric patterns, layered transparencies +- Dramatic shadows, decorative borders, custom cursors when appropriate +- Glassmorphism, mesh backgrounds, aurora effects - use trends tastefully + +**Libraries (All Auto-Resolved):** +- framer-motion (animations), lucide-react (icons), @headlessui/react, @radix-ui/* +- tailwindcss (utility-first CSS), clsx (conditional classes) + +REMEMBER: Bold maximalism and refined minimalism both work - the key is intentionality, not intensity. Each design should feel genuinely crafted for its context, never cookie-cutter. **CANVAS → PROJECT (When user has refined components):** - After building several related components in Canvas, briefly suggest: "Would you like to see these assembled in a running project?" @@ -281,8 +312,8 @@ export default function LoginScreen() { - When in doubt → Default to component and ask: "I'll create this as a canvas component. Let me know if you need a full project with routing instead." **GRACEFUL MODE HANDLING:** -- You can use Roopik tools (canvas, component, project, browser) in ANY mode if available, but ask the user first if they want to switch mode or stay in current mode for the same. -- If user is in Code mode and asks to preview a component → Use component_add directly, no need to switch modes. But always ask once if they want to switch to Designer mode for extended design work or continue in same or relavent mode. +- You can use Roopik tools (canvas, component, project, browser) in ANY mode if available, but always ask the user first if they want to switch mode to this designer mode or stay in current mode for the same. +- If user is in Code mode and asks to preview a component → Use component_add directly, you have flexibility but if user insist on design relate things switch to this Design mode. But always ask once if they want to switch to Designer mode for extended design work or continue in same or relavent mode. - If user asks for UI work in Ask mode → You can still use roopik tools to demonstrate (preview related only), no mode switch required unless code changes are needed - Only suggest mode switch if user would benefit from the full Designer workflow (extended design session) @@ -300,6 +331,7 @@ export default function LoginScreen() { - **NEVER** use browser_open, browser_screenshot, or any browser_* tools for canvas components - There is NO URL like /canvas/{id} to navigate to - the Canvas is a built-in IDE feature, not a web page - When user asks "show me the preview" of canvas components → Inform them: "The components are now visible in the Canvas view in your IDE" + - After component_add_batch or adding multiple components, ALWAYS use canvas_validate_components to verify all components are ready and built successfully or not to catch errors early or health status. For single component_add, you should rather use component_get_info to verify build status. - **Projects - Browser Preview**: - Use browser_open, browser_screenshot, browser_inspect_element ONLY after project_start (when dev server is running) @@ -322,7 +354,7 @@ export default function LoginScreen() { whenToUse: "Use this mode when you need to plan, design, or strategize before implementation. Perfect for breaking down complex problems, creating technical specifications, designing system architecture, or brainstorming solutions before coding.", description: "Plan and design before implementation", - groups: ["read", ["edit", { fileRegex: "\\.md$", description: "Markdown files only" }], "browser", "mcp", "roopik"], + groups: ["read", ["edit", { fileRegex: "\\.md$", description: "Markdown files only" }], "browser", "mcp"], customInstructions: "1. Do some information gathering (using provided tools) to get more context about the task.\n\n2. You should also ask the user clarifying questions to get a better understanding of the task.\n\n3. Once you've gained more context about the user's request, break down the task into clear, actionable steps and create a todo list using the `update_todo_list` tool. Each todo item should be:\n - Specific and actionable\n - Listed in logical execution order\n - Focused on a single, well-defined outcome\n - Clear enough that another mode could execute it independently\n\n **Note:** If the `update_todo_list` tool is not available, write the plan to a markdown file (e.g., `plan.md` or `todo.md`) instead.\n\n4. As you gather more information or discover new requirements, update the todo list to reflect the current understanding of what needs to be accomplished.\n\n5. Ask the user if they are pleased with this plan, or if they would like to make any changes. Think of this as a brainstorming session where you can discuss the task and refine the todo list.\n\n6. Include Mermaid diagrams if they help clarify complex workflows or system architecture. Please avoid using double quotes (\"\") and parentheses () inside square brackets ([]) in Mermaid diagrams, as this can cause parsing errors.\n\n7. Use the switch_mode tool to request that the user switch to another mode to implement the solution.\n\n**IMPORTANT: Focus on creating clear, actionable todo lists rather than lengthy markdown documents. Use the todo list as your primary planning tool to track and organize the work that needs to be done.**\n\n**CRITICAL: Never provide level of effort time estimates (e.g., hours, days, weeks) for tasks. Focus solely on breaking down the work into clear, actionable steps without estimating how long they will take.**\n\nUnless told otherwise, if you want to save a plan file, put it in the /plans directory", }, diff --git a/extensions/roopik-roo/packages/types/src/tool.ts b/extensions/roopik-roo/packages/types/src/tool.ts index ea6e0333e..6b3d6860a 100644 --- a/extensions/roopik-roo/packages/types/src/tool.ts +++ b/extensions/roopik-roo/packages/types/src/tool.ts @@ -39,7 +39,7 @@ export const toolNames = [ "run_slash_command", "generate_image", "custom_tool", - // Roopik IDE Tools - Browser (12) + // Roopik IDE Tools - Browser (14) "browser_open", "browser_close", "browser_action_input", @@ -51,16 +51,19 @@ export const toolNames = [ "browser_get_errors", "browser_get_console_logs", "browser_get_performance", - "browser_get_cdp_info", + "browser_get_state", + "browser_set_viewport", + "browser_get_network_requests", // Roopik IDE Tools - Project (3) "project_get_active", "project_start", "project_stop", - // Roopik IDE Tools - Canvas (3) + // Roopik IDE Tools - Canvas (4) "canvas_list", "canvas_get_active", "canvas_create", - // Roopik IDE Tools - Component (6) + "canvas_validate_components", + // Roopik IDE Tools - Component (7) "component_add", "component_add_batch", "component_remove", diff --git a/extensions/roopik-roo/src/core/assistant-message/NativeToolCallParser.ts b/extensions/roopik-roo/src/core/assistant-message/NativeToolCallParser.ts index 7823c0bdc..be440e768 100644 --- a/extensions/roopik-roo/src/core/assistant-message/NativeToolCallParser.ts +++ b/extensions/roopik-roo/src/core/assistant-message/NativeToolCallParser.ts @@ -630,6 +630,29 @@ export class NativeToolCallParser { case "browser_get_cdp_info": nativeArgs = {} break + // @ts-expect-error - Roopik-specific tool not in base ToolName type + case "browser_get_state": + nativeArgs = {} + break + // @ts-expect-error - Roopik-specific tool not in base ToolName type + case "browser_set_viewport": + nativeArgs = { + width: partialArgs.width, + height: partialArgs.height, + deviceScaleFactor: partialArgs.deviceScaleFactor, + mobile: partialArgs.mobile, + } + break + // @ts-expect-error - Roopik-specific tool not in base ToolName type + case "browser_get_network_requests": + nativeArgs = { + includeStaticAssets: partialArgs.includeStaticAssets, + urlFilter: partialArgs.urlFilter, + method: partialArgs.method, + statusFilter: partialArgs.statusFilter, + limit: partialArgs.limit, + } + break // Project Tools (3) case "project_get_active": @@ -659,8 +682,12 @@ export class NativeToolCallParser { case "canvas_create": nativeArgs = { name: partialArgs.name } break + // @ts-expect-error - Roopik-specific tool not in base ToolName type + case "canvas_validate_components": + nativeArgs = { canvasId: partialArgs.canvasId } + break - // Component Tools (6) + // Component Tools (8) case "component_add": nativeArgs = { folderPath: partialArgs.folderPath, @@ -1057,6 +1084,29 @@ export class NativeToolCallParser { case "browser_get_cdp_info": nativeArgs = {} as NativeArgsFor break + // @ts-expect-error - Roopik-specific tool not in base ToolName type + case "browser_get_state": + nativeArgs = {} as NativeArgsFor + break + // @ts-expect-error - Roopik-specific tool not in base ToolName type + case "browser_set_viewport": + nativeArgs = { + width: args.width, + height: args.height, + deviceScaleFactor: args.deviceScaleFactor, + mobile: args.mobile, + } as unknown as NativeArgsFor + break + // @ts-expect-error - Roopik-specific tool not in base ToolName type + case "browser_get_network_requests": + nativeArgs = { + includeStaticAssets: args.includeStaticAssets, + urlFilter: args.urlFilter, + method: args.method, + statusFilter: args.statusFilter, + limit: args.limit, + } as unknown as NativeArgsFor + break // Project Tools (3) case "project_get_active": @@ -1090,8 +1140,12 @@ export class NativeToolCallParser { nativeArgs = { name: args.name } as NativeArgsFor } break + // @ts-expect-error - Roopik-specific tool not in base ToolName type + case "canvas_validate_components": + nativeArgs = { canvasId: args.canvasId } as unknown as NativeArgsFor + break - // Component Tools (6) + // Component Tools (8) case "component_add": if (args.folderPath !== undefined) { nativeArgs = { diff --git a/extensions/roopik-roo/src/core/assistant-message/presentAssistantMessage.ts b/extensions/roopik-roo/src/core/assistant-message/presentAssistantMessage.ts index 6e540412e..3bcdd7fee 100644 --- a/extensions/roopik-roo/src/core/assistant-message/presentAssistantMessage.ts +++ b/extensions/roopik-roo/src/core/assistant-message/presentAssistantMessage.ts @@ -445,6 +445,12 @@ export async function presentAssistantMessage(cline: Task) { return `[browser_get_performance]` case "browser_get_cdp_info": return `[browser_get_cdp_info]` + case "browser_get_state": + return `[browser_get_state]` + case "browser_set_viewport": + return `[browser_set_viewport${block.params.width && block.params.height ? ` ${block.params.width}x${block.params.height}` : ""}]` + case "browser_get_network_requests": + return `[browser_get_network_requests]` // Roopik IDE Tools - Project (3) case "project_get_active": return `[project_get_active]` @@ -459,7 +465,9 @@ export async function presentAssistantMessage(cline: Task) { return `[canvas_get_active]` case "canvas_create": return `[canvas_create '${block.params.name}']` - // Roopik IDE Tools - Component (6) + case "canvas_validate_components": + return `[canvas_validate_components${block.params.canvasId ? ` '${block.params.canvasId}'` : ""}]` + // Roopik IDE Tools - Component (8) case "component_add": return `[component_add '${block.params.folderPath || block.params.path}']` case "component_add_batch": @@ -976,8 +984,8 @@ export async function presentAssistantMessage(cline: Task) { pushToolResult, }) break - // Roopik IDE Tools (24 tools) - // Browser (12)-// Project (3)-// Canvas (3)-// Component (6) + // Roopik IDE Tools (28 tools) + // Browser (14)-// Project (3)-// Canvas (4)-// Component (8) case "browser_open": case "browser_close": case "browser_action_input": @@ -990,12 +998,16 @@ export async function presentAssistantMessage(cline: Task) { case "browser_get_console_logs": case "browser_get_performance": case "browser_get_cdp_info": + case "browser_get_state": + case "browser_set_viewport": + case "browser_get_network_requests": case "project_get_active": case "project_start": case "project_stop": case "canvas_list": case "canvas_get_active": case "canvas_create": + case "canvas_validate_components": case "component_add": case "component_add_batch": case "component_remove": diff --git a/extensions/roopik-roo/src/core/prompts/sections/capabilities.ts b/extensions/roopik-roo/src/core/prompts/sections/capabilities.ts index 42203af93..e35cb20af 100644 --- a/extensions/roopik-roo/src/core/prompts/sections/capabilities.ts +++ b/extensions/roopik-roo/src/core/prompts/sections/capabilities.ts @@ -14,6 +14,8 @@ CAPABILITIES ` : "" } -- **Roopik IDE**: You are "Dio", a full-featured AI coding assistant in this IDE. You excel at all software engineering tasks. For UI/UX and frontend work, you have access to specialized \`roopik\` tools (when available) that provide visual context, live browser and project preview, and element inspection with computed styles and source maps. Use these tools to iterate faster on design tasks and give users a better experience by showing them live previews of their work. For all other tasks, use standard tools effectively. +- **Roopik IDE**: You are "Dio", a full-featured AI coding assistant in this IDE. You excel at all software engineering tasks. For UI/UX and frontend work, you have access to specialized \`roopik\` tools (when available) that provide visual context, live browser and project preview, and element inspection with computed styles and source maps. Use these tools to iterate faster on design tasks and give users a better experience by showing them live previews of their work. + - **Canvas Component Health Verification**: After using \`component_add_batch\` or adding multiple components sequentially, ALWAYS call \`canvas_validate_components\` to verify all components are 'ready' and catch build/runtime errors early. This is significantly more efficient than polling individual \`component_get_info\` calls. If errors are found, fix the code and use \`component_rebuild\` if necessary to ensure users see a working UI. + - **Tool Selection Logic**: For single \`component_add\`, use \`component_get_info\` (targeted). For batch operations or after major edits, use \`canvas_validate_components\` (comprehensive health check across all components). ` } diff --git a/extensions/roopik-roo/src/core/prompts/tools/native-tools/roopik.ts b/extensions/roopik-roo/src/core/prompts/tools/native-tools/roopik.ts index aed7e4491..d5bed871f 100644 --- a/extensions/roopik-roo/src/core/prompts/tools/native-tools/roopik.ts +++ b/extensions/roopik-roo/src/core/prompts/tools/native-tools/roopik.ts @@ -650,6 +650,27 @@ export const component_rebuild: OpenAI.Chat.ChatCompletionTool = { }, } +export const canvas_validate_components: OpenAI.Chat.ChatCompletionTool = { + type: "function", + function: { + name: "canvas_validate_components", + description: + "[Roopik IDE] Validate all components in a canvas in a single efficient call. **IMPORTANT: Use this tool immediately after calling `component_add_batch` or when adding multiple components sequentially to verify their build and runtime health.** Returns a summary (total, success, failed, building counts) plus detailed error information for any failed components, including syntax errors and runtime failures. This is significantly more efficient than polling individual `component_get_info` calls. Use this to catch errors early and ensure users see a working UI.", + strict: true, + parameters: { + type: "object", + properties: { + canvasId: { + type: "string", + description: "Canvas ID to validate. Omit to use the currently active canvas.", + }, + }, + required: [], + additionalProperties: false, + }, + }, +} + // ============================================================================ // Export all Roopik tools // ============================================================================ @@ -674,10 +695,11 @@ export const roopikNativeTools: OpenAI.Chat.ChatCompletionTool[] = [ project_get_active, project_start, project_stop, - // Canvas (3 tools) + // Canvas (4 tools) canvas_list, canvas_get_active, canvas_create, + canvas_validate_components, // Component (6 tools) component_add, component_add_batch, diff --git a/extensions/roopik-roo/src/core/prompts/tools/roopik/roopik-tools.ts b/extensions/roopik-roo/src/core/prompts/tools/roopik/roopik-tools.ts index d3ce22b84..966a2e33c 100644 --- a/extensions/roopik-roo/src/core/prompts/tools/roopik/roopik-tools.ts +++ b/extensions/roopik-roo/src/core/prompts/tools/roopik/roopik-tools.ts @@ -9,7 +9,7 @@ * get_errors, get_console_logs, get_performance, get_state, set_viewport, get_network_requests * - Project (3): get_active, start, stop * - Canvas (3): list, get_active, create - * - Component (6): add, add_batch, remove, get_info, list, rebuild + * - Component (8): add, add_batch, remove, get_info, list, rebuild, validate_components, screenshot */ // ============================================================================= @@ -470,10 +470,11 @@ export const ROOPIK_TOOL_NAMES = [ "project_get_active", "project_start", "project_stop", - // Canvas (3 tools) + // Canvas (4 tools) "canvas_list", "canvas_get_active", "canvas_create", + "canvas_validate_components", // Component (6 tools) "component_add", "component_add_batch", diff --git a/extensions/roopik-roo/src/core/tools/roopik/RoopikToolHandler.ts b/extensions/roopik-roo/src/core/tools/roopik/RoopikToolHandler.ts index afecb1247..64fdacb0d 100644 --- a/extensions/roopik-roo/src/core/tools/roopik/RoopikToolHandler.ts +++ b/extensions/roopik-roo/src/core/tools/roopik/RoopikToolHandler.ts @@ -136,7 +136,7 @@ export async function handleRoopikTool( result = await handleCreateCanvas(task, block, callbacks) break - // Component Tools (6) + // Component Tools (8) case "component_add": result = await handleAddComponent(task, block, callbacks) break @@ -155,6 +155,9 @@ export async function handleRoopikTool( case "component_rebuild": result = await handleRebuildComponent(task, block, callbacks) break + case "canvas_validate_components": + result = await handleValidateComponents(task, block, callbacks) + break default: result = { success: false, error: `Unknown Roopik tool: ${toolName}` } @@ -457,6 +460,11 @@ async function handleRebuildComponent(task: Task, block: ToolUse, callbacks: Too return roopikClient.rebuildComponent(componentId) } +async function handleValidateComponents(task: Task, block: ToolUse, callbacks: ToolCallbacks): Promise { + const canvasId = block.params.canvasId + return roopikClient.validateComponents(canvasId) +} + // ============================================================================ // Result Formatting // ============================================================================ diff --git a/extensions/roopik-roo/src/services/roopik/RoopikToolClient.ts b/extensions/roopik-roo/src/services/roopik/RoopikToolClient.ts index 7ae1ddcd9..4f074cdb4 100644 --- a/extensions/roopik-roo/src/services/roopik/RoopikToolClient.ts +++ b/extensions/roopik-roo/src/services/roopik/RoopikToolClient.ts @@ -626,6 +626,16 @@ export class RoopikToolClient { }) } + /** + * Validate all components in a canvas + * Returns summary (total, success, failed, building) + detailed errors for failed components + */ + async validateComponents(canvasId?: string): Promise> { + return this.executeCommand("roopik.tools.validateComponents", { + canvasId, + }) + } + // ======================================================================== // Internal Helpers // ======================================================================== diff --git a/extensions/roopik-roo/src/shared/tools.ts b/extensions/roopik-roo/src/shared/tools.ts index a86f824ad..306c65800 100644 --- a/extensions/roopik-roo/src/shared/tools.ts +++ b/extensions/roopik-roo/src/shared/tools.ts @@ -96,6 +96,16 @@ export const toolParamNames = [ "componentId", // component_remove, component_get_info, component_rebuild "deleteSourceCode", // component_remove "components", // component_add_batch + // browser_set_viewport parameters + "width", // browser_set_viewport + "height", // browser_set_viewport + "deviceScaleFactor", // browser_set_viewport + "mobile", // browser_set_viewport + // browser_get_network_requests parameters + "includeStaticAssets", // browser_get_network_requests + "urlFilter", // browser_get_network_requests + "method", // browser_get_network_requests + "statusFilter", // browser_get_network_requests // browser_action parameters "key", // browser_action (press) "modifiers", // browser_action (press) @@ -269,7 +279,7 @@ export type ToolGroupConfig = { customTools?: readonly string[] // Opt-in only tools - only available when explicitly included via model's includedTools } -export const TOOL_DISPLAY_NAMES: Record = { +export const TOOL_DISPLAY_NAMES: Record = { execute_command: "run commands", read_file: "read files", read_command_output: "read command output", @@ -294,7 +304,7 @@ export const TOOL_DISPLAY_NAMES: Record = { run_slash_command: "run slash command", generate_image: "generate images", custom_tool: "use custom tools", - // Roopik IDE Tools - Browser + // Roopik IDE Tools - Browser (14 tools) browser_open: "open browser", browser_close: "close browser", browser_action_input: "perform browser input action", @@ -306,16 +316,19 @@ export const TOOL_DISPLAY_NAMES: Record = { browser_get_errors: "get browser errors", browser_get_console_logs: "get console logs", browser_get_performance: "get browser performance metrics", - browser_get_cdp_info: "get browser CDP info", - // Roopik IDE Tools - Project + browser_get_state: "get browser state", + browser_set_viewport: "set browser viewport", + browser_get_network_requests: "get network requests", + // Roopik IDE Tools - Project (3 tools) project_get_active: "get active project", project_start: "start project", project_stop: "stop project", - // Roopik IDE Tools - Canvas + // Roopik IDE Tools - Canvas (4 tools) canvas_list: "list canvases", canvas_get_active: "get active canvas", canvas_create: "create canvas", - // Roopik IDE Tools - Component + canvas_validate_components: "validate canvas components", + // Roopik IDE Tools - Component (7 tools) component_add: "add component", component_add_batch: "add multiple components", component_remove: "remove component", @@ -348,7 +361,7 @@ export const TOOL_GROUPS: Record = { }, roopik: { tools: [ - // Browser (12 tools) + // Browser (14 tools) "browser_open", "browser_close", "browser_action_input", @@ -361,14 +374,18 @@ export const TOOL_GROUPS: Record = { "browser_get_console_logs", "browser_get_performance", "browser_get_cdp_info", + "browser_get_state", + "browser_set_viewport", + "browser_get_network_requests", // Project (3 tools) "project_get_active", "project_start", "project_stop", - // Canvas (3 tools) + // Canvas (4 tools) "canvas_list", "canvas_get_active", "canvas_create", + "canvas_validate_components", // Component (6 tools) "component_add", "component_add_batch", diff --git a/extensions/roopik-roo/webview-ui/src/components/chat/ChatRow.tsx b/extensions/roopik-roo/webview-ui/src/components/chat/ChatRow.tsx index 4af31134b..37f73a9d4 100644 --- a/extensions/roopik-roo/webview-ui/src/components/chat/ChatRow.tsx +++ b/extensions/roopik-roo/webview-ui/src/components/chat/ChatRow.tsx @@ -1151,6 +1151,39 @@ export const ChatRowContent = ({ ) + case "browser_get_state": + return ( +
+ {toolIcon("info")} + + {message.type === "ask" + ? t("chat:roopik.browser.wantsToGetState") + : t("chat:roopik.browser.didGetState")} + +
+ ) + case "browser_set_viewport": + return ( +
+ {toolIcon("device-mobile")} + + {message.type === "ask" + ? t("chat:roopik.browser.wantsToSetViewport") + : t("chat:roopik.browser.didSetViewport")} + +
+ ) + case "browser_get_network_requests": + return ( +
+ {toolIcon("globe")} + + {message.type === "ask" + ? t("chat:roopik.browser.wantsToGetNetworkRequests") + : t("chat:roopik.browser.didGetNetworkRequests")} + +
+ ) // Project Tools (3) case "project_get_active": @@ -1231,8 +1264,19 @@ export const ChatRowContent = ({ )} ) + case "canvas_validate_components": + return ( +
+ {toolIcon("checklist")} + + {message.type === "ask" + ? t("chat:roopik.canvas.wantsToValidateComponents") + : t("chat:roopik.canvas.didValidateComponents")} + +
+ ) - // Component Tools (6) + // Component Tools (8) case "component_add": return (
diff --git a/extensions/roopik-roo/webview-ui/src/i18n/locales/en/chat.json b/extensions/roopik-roo/webview-ui/src/i18n/locales/en/chat.json index 4e1f1f44c..849cf7fb3 100644 --- a/extensions/roopik-roo/webview-ui/src/i18n/locales/en/chat.json +++ b/extensions/roopik-roo/webview-ui/src/i18n/locales/en/chat.json @@ -521,7 +521,13 @@ "wantsToGetPerformance": "Dio wants to get performance metrics", "didGetPerformance": "Dio retrieved performance metrics", "wantsToGetCdpInfo": "Dio wants to get browser info", - "didGetCdpInfo": "Dio retrieved browser info" + "didGetCdpInfo": "Dio retrieved browser info", + "wantsToGetState": "Dio wants to get browser state", + "didGetState": "Dio retrieved browser state", + "wantsToSetViewport": "Dio wants to set viewport size", + "didSetViewport": "Dio set viewport size", + "wantsToGetNetworkRequests": "Dio wants to get network requests", + "didGetNetworkRequests": "Dio retrieved network requests" }, "project": { "wantsToGetActive": "Dio wants to get the active project", @@ -537,7 +543,9 @@ "wantsToGetActive": "Dio wants to get the active canvas", "didGetActive": "Dio retrieved the active canvas", "wantsToCreate": "Dio wants to create a canvas", - "didCreate": "Dio created a canvas" + "didCreate": "Dio created a canvas", + "wantsToValidateComponents": "Dio wants to validate components", + "didValidateComponents": "Dio validated components" }, "component": { "wantsToAdd": "Dio wants to add a component to the canvas", diff --git a/extensions/roopik/src/canvasPanel.ts b/extensions/roopik/src/canvasPanel.ts index 7c5a5f2da..38346a9f5 100644 --- a/extensions/roopik/src/canvasPanel.ts +++ b/extensions/roopik/src/canvasPanel.ts @@ -1256,6 +1256,42 @@ export class CanvasPanel implements vscode.Disposable { this.logger.debug('Panel disposed'); } + // ======================================================================== + // Screenshot + // ======================================================================== + + /** + * Capture a screenshot of a component (bidirectional IPC) + * Called when Core requests a screenshot via MCP tools + */ + public async captureComponentScreenshot(componentId: string): Promise { + return new Promise((resolve, reject) => { + const requestId = `screenshot-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; + + const timeout = setTimeout(() => { + reject(new Error(`Webview did not respond within 10 seconds for component ${componentId}`)); + }, 10000); + + const messageHandler = this.panel.webview.onDidReceiveMessage((message: any) => { + if (message.type === 'screenshotResponse' && message.requestId === requestId) { + clearTimeout(timeout); + messageHandler.dispose(); + + if (message.screenshot) { + resolve(message.screenshot); + } else { + reject(new Error(`Webview returned null - component may not be rendered`)); + } + } + }); + + this.postToWebview('captureComponentScreenshot', { + requestId, + componentId + }); + }); + } + /** * Dispose the panel */ diff --git a/extensions/roopik/src/extension.ts b/extensions/roopik/src/extension.ts index 4818deb0e..96ddcc19d 100644 --- a/extensions/roopik/src/extension.ts +++ b/extensions/roopik/src/extension.ts @@ -229,6 +229,35 @@ export async function activate(context: vscode.ExtensionContext) { } ); + // Handle screenshot request from Core (bidirectional IPC) + const captureComponentScreenshotCommand = vscode.commands.registerCommand( + 'roopik.canvas.captureComponentScreenshot', + async (componentId: string): Promise => { + try { + logger.debug('Extension', `📸 Screenshot requested for component: ${componentId}`); + + // Get the canvas panel for this component + const screenshot = await manager!.captureComponentScreenshot(componentId); + + if (screenshot) { + const preview = screenshot.substring(0, 100); + logger.debug('Extension', `✅ Screenshot captured, returning to Core`, { + componentId, + dataUrlLength: screenshot.length, + preview + }); + return screenshot; + } else { + logger.warn('Extension', `❌ Failed to capture screenshot (null returned) for ${componentId}`); + return null; + } + } catch (error) { + logger.error('Extension', `❌ Error capturing screenshot for ${componentId}`, error); + return null; + } + } + ); + // Register commands context.subscriptions.push( openCanvasCommand, @@ -238,7 +267,8 @@ export async function activate(context: vscode.ExtensionContext) { componentCreatedCommand, componentBuiltCommand, componentDeletedCommand, - componentUpdatedCommand + componentUpdatedCommand, + captureComponentScreenshotCommand ); // logger.info('Extension', 'Roopik Canvas extension activated'); diff --git a/extensions/roopik/src/roopikExtensionManager.ts b/extensions/roopik/src/roopikExtensionManager.ts index 21770a195..ba0f0a4d1 100644 --- a/extensions/roopik/src/roopikExtensionManager.ts +++ b/extensions/roopik/src/roopikExtensionManager.ts @@ -513,6 +513,28 @@ export class RoopikExtensionManager implements vscode.Disposable { vscode.commands.executeCommand('roopik.core.registerPanelFocused', canvasId); } + // ============================================================================ + // Screenshot + // ============================================================================ + + /** + * Capture a screenshot of a component (bidirectional IPC) + * Called by Core when screenshot is requested via MCP/tools + */ + public async captureComponentScreenshot(componentId: string): Promise { + for (const [, panel] of this.canvasPanels.entries()) { + try { + const screenshot = await panel.captureComponentScreenshot(componentId); + if (screenshot) { + return screenshot; + } + } catch (error) { + // Try next panel + } + } + return null; + } + // ============================================================================ // Dispose // ============================================================================ diff --git a/extensions/roopik/webview/src/componentView/ComponentView.tsx b/extensions/roopik/webview/src/componentView/ComponentView.tsx index 3316c7690..eca5cead9 100644 --- a/extensions/roopik/webview/src/componentView/ComponentView.tsx +++ b/extensions/roopik/webview/src/componentView/ComponentView.tsx @@ -498,6 +498,27 @@ function App() { } break; } + + case "captureComponentScreenshot": { + const { requestId, componentId } = msg.payload; + (async () => { + try { + const screenshot = await captureComponentScreenshot(componentId); + vscode.postMessage({ + type: "screenshotResponse", + requestId, + screenshot + }); + } catch (error) { + vscode.postMessage({ + type: "screenshotResponse", + requestId, + screenshot: null + }); + } + })(); + break; + } } }; @@ -625,10 +646,15 @@ function App() { const iframe = document.querySelector( `iframe[data-sandbox-id="${componentId}"]`, ) as HTMLIFrameElement | null; + + if (!iframe) { + return null; + } + const doc = iframe?.contentDocument; const target = doc?.documentElement; - if (!iframe || !doc || !target) { + if (!doc || !target) { return null; } @@ -636,6 +662,7 @@ function App() { const scrollWidth = Math.max(target.scrollWidth, target.clientWidth); const scrollHeight = Math.max(target.scrollHeight, target.clientHeight); const iframeWindow = iframe.contentWindow; + const canvas = await html2canvas(target, { backgroundColor: null, logging: false, @@ -650,7 +677,6 @@ function App() { return canvas.toDataURL("image/png"); } catch (error) { - logger.warn("[Canvas] Screenshot capture failed", { componentId, error }); return null; } }, []); diff --git a/resources/devtools-extensions/vue-devtools/_metadata/verified_contents.json b/resources/devtools-extensions/vue-devtools/_metadata/verified_contents.json deleted file mode 100644 index 87c63ca95..000000000 --- a/resources/devtools-extensions/vue-devtools/_metadata/verified_contents.json +++ /dev/null @@ -1 +0,0 @@ -[{"description":"treehash per file","signed_content":{"payload":"eyJjb250ZW50X2hhc2hlcyI6W3siYmxvY2tfc2l6ZSI6NDA5NiwiZGlnZXN0Ijoic2hhMjU2IiwiZmlsZXMiOlt7InBhdGgiOiJjbGllbnQvSWNvblRpdGxlLnZ1ZV92dWVfdHlwZV9zY3JpcHRfc2V0dXBfdHJ1ZV9sYW5nLUN6TkF1ZFR6LmpzIiwicm9vdF9oYXNoIjoibmtOZTlLT3RCd0JfTlVsWTVfTEdLOENKQlU2NzJoam1hUExSWUpKdkxOQSJ9LHsicGF0aCI6ImNsaWVudC9JY29uVGl0bGUudnVlX3Z1ZV90eXBlX3NjcmlwdF9zZXR1cF90cnVlX2xhbmctTjdzUUdQak0uanMiLCJyb290X2hhc2giOiJBNm9BZWoyUS1BbXpBdWx3YXlibkxLTUxQNllrM2hKSnRvaC15Uy1tOFlNIn0seyJwYXRoIjoiY2xpZW50L1NlY3Rpb25CbG9jay1CYXEtb0ZXRi5qcyIsInJvb3RfaGFzaCI6IkxIeU1lbzlGZVloQ3pTc3RuWHFXUGlVanM2a2VKQlZkYnhGMzR0TjhBVjgifSx7InBhdGgiOiJjbGllbnQvU2VjdGlvbkJsb2NrLURwME5JRl9iLmpzIiwicm9vdF9oYXNoIjoidUNMYUoxTU1vLVM1ZnliemxBR1dwdmd6VmpocHF0UDJpWDZCQV9uMXY3cyJ9LHsicGF0aCI6ImNsaWVudC9hc3NldHMtQ2VudmpoeTYuanMiLCJyb290X2hhc2giOiJQTWo0MHk0R25ha01ZUXZJdk9SUVRaYlpFQkl4Rkx4SnlWZWkyaS1VWmc0In0seyJwYXRoIjoiY2xpZW50L2Fzc2V0cy1EdEstN2VtYS5qcyIsInJvb3RfaGFzaCI6InpiX1dZUTI2bTlTeTU1eEIzZEZZdldGaVNQM1lYVklwaVZVb3ptekNzbkUifSx7InBhdGgiOiJjbGllbnQvY29sb3Itc2NoZW1lLmpzIiwicm9vdF9oYXNoIjoiMi1lUlVacnBQeVJkai1SNzJBTi02Vk1EUHRqS1drVUg1ei1fTVUzMTdmRSJ9LHsicGF0aCI6ImNsaWVudC9jb21wb25lbnRzLUJneVh6OHdfLmpzIiwicm9vdF9oYXNoIjoia2RnWGxwMzZHWHRsQl9RRnZvTmpxenM4S29QRmZQWHVzcGdvbXE5U2U3byJ9LHsicGF0aCI6ImNsaWVudC9jb21wb25lbnRzLUMxcFZMeG5vLmpzIiwicm9vdF9oYXNoIjoiX2g1OF9LbExnNC0xb09yS05MSDU4aGJPa1JtNW45S2tNUEZGNXhzRUFLNCJ9LHsicGF0aCI6ImNsaWVudC9jc3MtQm5MMTA2NFcuanMiLCJyb290X2hhc2giOiJkUFFTWHB1XzctVkNCaHBORUR0TGh0TmlybEFlaG9pYTFfT3FMQVQwUEUwIn0seyJwYXRoIjoiY2xpZW50L2N1c3RvbS1pbnNwZWN0b3ItdGFiLXZpZXctQ0laaG92U0YuanMiLCJyb290X2hhc2giOiJONzVvLWVjTnJLNGluc2Y0c3RnbHozZUthUU9KeXdJWVpCWEE0T0JhWWgwIn0seyJwYXRoIjoiY2xpZW50L2N1c3RvbS1pbnNwZWN0b3ItdGFiLXZpZXctZFpTbnNITnIuanMiLCJyb290X2hhc2giOiJabDQxVngyemtRT1dRN1hIamhPUU5aM0dJeXhWX3d6VHpZYm94V3djWWhBIn0seyJwYXRoIjoiY2xpZW50L2N1c3RvbS10YWItdmlldy1EQjJGWjZlTi5qcyIsInJvb3RfaGFzaCI6Ikw5cUJCNXM3dUZEU3h1eXRuR2ZpMTdDdGN3cGxkYS1DeG5ZSmlvMjdNTHMifSx7InBhdGgiOiJjbGllbnQvY3VzdG9tLXRhYi12aWV3LURuVEZLaGlJLmpzIiwicm9vdF9oYXNoIjoiTDQwZzh2TjdibDVCNFdkZDZpMW1abVBqMWs1VVY5SXJ4YW1UQW5wLU1NNCJ9LHsicGF0aCI6ImNsaWVudC9kZXZ0b29scy1wYW5lbC5jc3MiLCJyb290X2hhc2giOiItZGhlcUlNd2ppdTVsMDZYLTlhcFhENEdDeWloa1BsRlFlZFd4b1FaTEo0In0seyJwYXRoIjoiY2xpZW50L2RldnRvb2xzLXBhbmVsLmpzIiwicm9vdF9oYXNoIjoibUFHWDc3b1MwWlh4Q3FxTlc0UG1pRDR2cEcyMG1NVHFJZTBGU3VQcTBtayJ9LHsicGF0aCI6ImNsaWVudC9kaWZmLURPZFVmUFNSLmpzIiwicm9vdF9oYXNoIjoiOUtMVzRDRXRoT0wxQm1IWklNSmdPTndhaE0xWjlRTExrZGlVMTF5ZUdDQSJ9LHsicGF0aCI6ImNsaWVudC9ncmFwaC1DSFprb2hqaS5qcyIsInJvb3RfaGFzaCI6InQ0WmJCRWJET2MyQ1YtdG9BTWxzamhGMTZ2YXhVNVlPVDdnNWIwTlJGWnMifSx7InBhdGgiOiJjbGllbnQvZ3JhcGgtTElORk9pUmsuanMiLCJyb290X2hhc2giOiIyaVJjMmM1OGdDdzNSWVdFdTFPN29aMUl6RzBCX09nYWttYWlZU2hlOGlJIn0seyJwYXRoIjoiY2xpZW50L2h0bWwtSXk5RUpCa0wuanMiLCJyb290X2hhc2giOiJ6MHFFNXk5ZlFja0ZxalZqQlY4dnBVZ1M1THlZRFA5UV91VjJpbXF0bGJrIn0seyJwYXRoIjoiY2xpZW50L2luZGV4LUItWElpZWZzLmpzIiwicm9vdF9oYXNoIjoiN3FwVWh6dU56Vm5hazBRbEJKUnNXVDJSSGJDQ0Z5ZDcxbU0xTmpqX2hTcyJ9LHsicGF0aCI6ImNsaWVudC9pbmRleC1DN1hQbW1MUy5qcyIsInJvb3RfaGFzaCI6IjJGeVI1Z3BqaXlGdkZhTUFBRGs4U1dnS3pwNnd6NTR2bVNfQkx2cUlGaUEifSx7InBhdGgiOiJjbGllbnQvamF2YXNjcmlwdC1DWEhseGd0dS5qcyIsInJvb3RfaGFzaCI6Iko2QThhZXpxT3ZYSDRIQTJOc18xSUxDdmRpNWJOVkNNYUFZUjVBOTU5ZnMifSx7InBhdGgiOiJjbGllbnQvanNvbi1ENGNYRWhNMC5qcyIsInJvb3RfaGFzaCI6IlMxUUR2cXh4WC1LaHRNZzNWcllKdmxNTm15cTBpemxKTGF2NU1ocVItYXcifSx7InBhdGgiOiJjbGllbnQvbG9nby5zdmciLCJyb290X2hhc2giOiJHeElSX2d6TEtOWjN6YmdGRF9QOVBlVV9QMmNsWU54WkFCbzZRSmtfSHlrIn0seyJwYXRoIjoiY2xpZW50L292ZXJ2aWV3LUI2cW9ma0VtLmpzIiwicm9vdF9oYXNoIjoiWEh6MkZXUDhnb2FjV2lmTFJqb1A0ajZSMlMwR1RlTTRvMDJ4Q2dVNlNfNCJ9LHsicGF0aCI6ImNsaWVudC9vdmVydmlldy1OT0hpTHdkcS5qcyIsInJvb3RfaGFzaCI6Im1ZLUtOODhIeVFIaFdYNzVGbnNlUFRYcFRRYzdMWlB5dERBSGNNZ25QZkEifSx7InBhdGgiOiJjbGllbnQvcGFnZXMtQnF1N2k5X1cuanMiLCJyb290X2hhc2giOiI2Qjk3cGVqbkxzZ0lZTUdXUW01bmthNjNKQXE0OWZjTmxGWXZ2U25jOFB3In0seyJwYXRoIjoiY2xpZW50L3BhZ2VzLUM1VjlDb1BZLmpzIiwicm9vdF9oYXNoIjoiUlZzZ1RBRkV1SU9XbFhSY1gxVUppTlpOS3ZKWC04MklFYVBzbEZoYWFuVSJ9LHsicGF0aCI6ImNsaWVudC9waW5pYS1CUHJhbjc3OS5qcyIsInJvb3RfaGFzaCI6ImxVUE5ZRnoxNzJQWUkzX3ZjbzB3c2R3TVV0Ukc4R2tELW1jYllHdnpYaWsifSx7InBhdGgiOiJjbGllbnQvcGluaWEtVmVKcVAtSmwuanMiLCJyb290X2hhc2giOiJUNFp1UUVRSS04d1JSZWloUkdtQUozTzNWbGhhd1d4TTQ3c2pCY3BROTlvIn0seyJwYXRoIjoiY2xpZW50L3JvdXRlci1CVzBXZlk5Ui5qcyIsInJvb3RfaGFzaCI6ImRJTWtfV2YzbExZYnpBWGpScHhyRTVtcGFoeDRaN2tQUG95azdlWFZRd0kifSx7InBhdGgiOiJjbGllbnQvcm91dGVyLUJiSS1lTXE5LmpzIiwicm9vdF9oYXNoIjoiMWVFdU1wSEpXZHRaY3doaXV0ZjRCRDFpeTh3WWlGUXZyOHB1RzNFMS1PSSJ9LHsicGF0aCI6ImNsaWVudC9zZXR0aW5ncy1EVHJub3B5Ri5qcyIsInJvb3RfaGFzaCI6Ino0ZF8wczBzZ1NGU0tNUkRBQTRCakt6NEloMi14ekJ6MHVYaFBqTUlrcVkifSx7InBhdGgiOiJjbGllbnQvc2V0dGluZ3MtRG5WTlNiaE4uanMiLCJyb290X2hhc2giOiJpTFBZQkJ1ZkRndjNBQjVqT0RkeGNxNzlJVDhZdVNkenJubFBDbTZaOGxvIn0seyJwYXRoIjoiY2xpZW50L3NoZWxsc2NyaXB0LURtaFNJSUtJLmpzIiwicm9vdF9oYXNoIjoiM2ZHY1o3OGxWTnhMSWZiU3A1WUFnQ3NoYUtrckYzX2xMSWFJQndWZjd5ZyJ9LHsicGF0aCI6ImNsaWVudC90aW1lbGluZS1DVlFnRWR5aS5qcyIsInJvb3RfaGFzaCI6Inl0Y296dkRTNWRwbld5ZV94VWdSeW1fQkxxZlZteUprRnlvSzk3aG9lMU0ifSx7InBhdGgiOiJjbGllbnQvdGltZWxpbmUtQ3d1M2NhUkMuanMiLCJyb290X2hhc2giOiJYTjh1NWVwYVFPamw5S0h4cHdNb0dzS1NtRlV1RnNXYVVnbkNLdXllMDkwIn0seyJwYXRoIjoiY2xpZW50L3R5cGVzY3JpcHQtREtmdmxCQ3cuanMiLCJyb290X2hhc2giOiJkdUlyenpHU05kS2tiM0Q5ZE9Tcmp4Q1VoOG9xV3hOa2Z6ZktDc0hrdkxZIn0seyJwYXRoIjoiY2xpZW50L3ZpdGVzc2UtZGFyay1DeW0tZUx0Ty5qcyIsInJvb3RfaGFzaCI6Im5yUE5XMlZpcGJGTFg1Q1Z5eUpWenN5NzVkM3hEUjdZYWZxZjVWeXQwTGsifSx7InBhdGgiOiJjbGllbnQvdml0ZXNzZS1saWdodC1DY21HMzE1Yy5qcyIsInJvb3RfaGFzaCI6IjdhYjc5cjhXWTc3TGVkRTQ5ZVlScm4wUGlBQ0lLalZKZ2JSdDJiTVZMckUifSx7InBhdGgiOiJjbGllbnQvdnVlLUNoYUtBSHpvLmpzIiwicm9vdF9oYXNoIjoieUZPdEY3eUU2WTB3WU8xQmZkUlFLcE9lTzdXQlFROThZUmZUd1dkSmNVdyJ9LHsicGF0aCI6ImNsaWVudC92dWUtYXBpcy1DOF9rWVlTcC5qcyIsInJvb3RfaGFzaCI6IlYxR0JabXpIN2dMdjZGZ1hzOE12U0JGM0RiYTNXU2o0SGlKZ0lrYVc4a1EifSx7InBhdGgiOiJjbGllbnQvdnVlLWh0bWwtSGcxVnNEb3guanMiLCJyb290X2hhc2giOiJ4RTh6VkE0OG1lbDczQ2ZYb0dfM1VDMVdZLVRtb19reTlRdjl4Wk5Mc1BzIn0seyJwYXRoIjoiY2xpZW50L3lhbWwtRGNRaGJNckwuanMiLCJyb290X2hhc2giOiJIMWp4dTM4elhqUkJmSXFyWi1YTXh3ZlhRVVlDckRnWkEwR05fYlFCYTh3In0seyJwYXRoIjoiZGlzdC9iYWNrZ3JvdW5kLmpzIiwicm9vdF9oYXNoIjoiN3k3M1hHeHAtSlBmX0NhVVA1TzJ0TzJXMGktdjVxNUpLaE1tcXdFVW01ayJ9LHsicGF0aCI6ImRpc3QvZGV0ZWN0b3IuanMiLCJyb290X2hhc2giOiI5WEZ6UVp5SnFibFQtdHliUjE4MTJUeU5nWUFad1lzT09YYm9zSzlrNURNIn0seyJwYXRoIjoiZGlzdC9kZXZ0b29scy1iYWNrZ3JvdW5kLmpzIiwicm9vdF9oYXNoIjoiM3p1TXdLdFMxa0Y2N1RUMl9sTVQ0VzZucnBtaEVEVkdNTnlPZnBKS2pzMCJ9LHsicGF0aCI6ImRpc3QvZGV2dG9vbHMtb3ZlcmxheS5qcyIsInJvb3RfaGFzaCI6IjFEMmtQb1hkeVJKRndEOVplaXU5R0xwbXhCTEJMTXNWbzB4QUZLZ3pFeXcifSx7InBhdGgiOiJkaXN0L2RldnRvb2xzLXBhbmVsLmpzIiwicm9vdF9oYXNoIjoiSVR5dmxhLUZScTBtaFpBMlF3bnlJdFhsSUZZQTZOdy15VkJ3akRaMTZ1TSJ9LHsicGF0aCI6ImRpc3QvcHJlcGFyZS5qcyIsInJvb3RfaGFzaCI6InpNNUF1ODZRaWJMY2ozbUN2b0NuODYxNTQtYm5DeWZJWWMzUVpiY3RVOW8ifSx7InBhdGgiOiJkaXN0L3Byb3h5LmpzIiwicm9vdF9oYXNoIjoiT1dtMEF0ODdWU0paWTFKelZUMlc4dXl0anBlam5VSkFKZGVBOU1nNmhVWSJ9LHsicGF0aCI6ImRpc3QvdXNlci1hcHAuanMiLCJyb290X2hhc2giOiJQOEpxVVBCNnRVNGdxcUJXVFlHT0VFVEtNZk5UUE1mR09Xa2ZVRWFRM1cwIn0seyJwYXRoIjoiaWNvbnMvMTI4LWJldGEucG5nIiwicm9vdF9oYXNoIjoiemoweWF4Qk5fMTlGY2Juci1qaGFnYWhuVk1CUW45TUJDZS1QUjRXelUyVSJ9LHsicGF0aCI6Imljb25zLzEyOC1ncmF5LnBuZyIsInJvb3RfaGFzaCI6ImdlMGw1d0Jjanl4OFZMRjVqZUVtSzV4SkpxWGYyclR1SlNDMzNuTV9PU00ifSx7InBhdGgiOiJpY29ucy8xMjgubnV4dC5wbmciLCJyb290X2hhc2giOiJRRUo2U1J2UG5YeDhSMHdBYTREMEhCWXh0eU1GQ0tLZnRwU0xuUno5RGpvIn0seyJwYXRoIjoiaWNvbnMvMTI4LnBuZyIsInJvb3RfaGFzaCI6IkhlNzBDVHZSS0ZhSVVmSDdNdGYwcTdKN0pGMnRFN3lsYi1scEptUW1aMFkifSx7InBhdGgiOiJpY29ucy8xMjgudml0ZXByZXNzLnBuZyIsInJvb3RfaGFzaCI6Imt0VUc3bjhLZUZhY0E5ZEctWktRMGxxd0JFdzVKaHJ2Ym13aEc0TGRqTE0ifSx7InBhdGgiOiJpY29ucy8xNi1iZXRhLnBuZyIsInJvb3RfaGFzaCI6IklOWFpEMTFoYnZqREJYWFREaVAtWWN0VkxwLTlnb1dkUWZJdU1wSWJ4TXMifSx7InBhdGgiOiJpY29ucy8xNi1ncmF5LnBuZyIsInJvb3RfaGFzaCI6IkFGUlFLVUZMZGZKeUZzTm1lcXRSN1hIal9NREVabWZjMVBySEJvQUdYZHMifSx7InBhdGgiOiJpY29ucy8xNi5udXh0LnBuZyIsInJvb3RfaGFzaCI6Il9VOHdkZ0RqbXB5YVRzY0dZN0JTUFdsclZMMVFsTDhPTDRGTmp3V3F1b3cifSx7InBhdGgiOiJpY29ucy8xNi5wbmciLCJyb290X2hhc2giOiJMRnBKZ3JMbThNMEtNMDVUcUZWS0dyWHAwZWROYXBERGtjQXI2UFJ1TF9nIn0seyJwYXRoIjoiaWNvbnMvMTYudml0ZXByZXNzLnBuZyIsInJvb3RfaGFzaCI6Il9Uam9kbTBlcHZHU0VkWWdoT2VsSENMaW9lUHNhZXhEd09Ed2VKWENObzAifSx7InBhdGgiOiJpY29ucy80OC1iZXRhLnBuZyIsInJvb3RfaGFzaCI6InZmaFI3UEFITS1zWkNUUXZqRXh4OUhJYk8yZ3JPRjJzYkpiWFVCd1JkazAifSx7InBhdGgiOiJpY29ucy80OC1ncmF5LnBuZyIsInJvb3RfaGFzaCI6InBEV29qVEY4TlBXUEtDbzkwakdBeTBQblVOT3I1ZkVuVEc2UEZXRVo4OUUifSx7InBhdGgiOiJpY29ucy80OC5udXh0LnBuZyIsInJvb3RfaGFzaCI6ImVvZmdKSW5NdS1pTlBFOG5FNWVmaFBySXhvTHl2SjNuUXctOUNtLVNHZE0ifSx7InBhdGgiOiJpY29ucy80OC5wbmciLCJyb290X2hhc2giOiJrUXJDbnhJMlphY3Q3SEhpOHFRcDJ4a0E3R2NSUUNCNnM1SGRPZnV5QVJrIn0seyJwYXRoIjoiaWNvbnMvNDgudml0ZXByZXNzLnBuZyIsInJvb3RfaGFzaCI6ImFQS3UwRThPdmhjNjVMejlMOWpDalM5dGxyck9jaUMxOFpNMmprb3lydG8ifSx7InBhdGgiOiJtYW5pZmVzdC5qc29uIiwicm9vdF9oYXNoIjoiVG1INVhQSXJ0SER3SXdfcm5KMTZiVFZ2TDdzS0lZbVVKYlFWMjdneGVnbyJ9LHsicGF0aCI6Im92ZXJsYXkvZGV2dG9vbHMtb3ZlcmxheS5jc3MiLCJyb290X2hhc2giOiJ2am85TTJnc2lJeVVqUTVUYXJXa1p0aHFvcVhoSjBJR2FqUzFNdHhCU0VVIn0seyJwYXRoIjoib3ZlcmxheS9kZXZ0b29scy1vdmVybGF5LmpzIiwicm9vdF9oYXNoIjoidWloUHpwemotSG1oa0pyZHpmTHBqSHVHdTNSMjh0V0szTGV1NUFkZGZJZyJ9LHsicGF0aCI6InBhY2thZ2UuanNvbiIsInJvb3RfaGFzaCI6ImlpZG4wRk85anFQc2ZKQVZXQkZ4cHprWXR4U1U2QVZab3RyamhWUzlRZGMifSx7InBhdGgiOiJwYWdlcy9kZXZ0b29scy1iYWNrZ3JvdW5kLmh0bWwiLCJyb290X2hhc2giOiJsWDNVbTA2MWF0UDh3ZENfbWg0aEdweE45aGpqWi1MdG9VWnFFV1Q4M0hRIn0seyJwYXRoIjoicGFnZXMvZGV2dG9vbHMtcGFuZWwuaHRtbCIsInJvb3RfaGFzaCI6IlptQVVQSW1JeVRLY2xxR3dpVS1aN3Z6ellhaXc2X1lvNlVWT0R2SS10U1EifSx7InBhdGgiOiJwb3B1cHMvZGV2dG9vbHMtc2NyZWVuc2hvdC5wbmciLCJyb290X2hhc2giOiJUZm1wZXc1cV9qME01Q1QzMHJ2RURaT2RIMk1sMWVvcTQwbS1YR3pIN3lFIn0seyJwYXRoIjoicG9wdXBzL2Rpc2FibGVkLmh0bWwiLCJyb290X2hhc2giOiJoRHZPNUZrQlBwZjM0MlFWQUYtYUl0dnJSOU1ySnRsbkN2Q0RDZDROc1cwIn0seyJwYXRoIjoicG9wdXBzL2Rpc2FibGVkLm51eHQuaHRtbCIsInJvb3RfaGFzaCI6Ii0zanpUcmQtNjJ3RWpyOElRVmcxYzVRbnM4WHVFWVBmMGF6eDYzSGlhRVkifSx7InBhdGgiOiJwb3B1cHMvZGlzYWJsZWQudml0ZXByZXNzLmh0bWwiLCJyb290X2hhc2giOiJxTXZndFRLTGFUVnhZM2h6N2lWVXNjT1pEOGw2ZElqUVhKMzh6WDNzUHN3In0seyJwYXRoIjoicG9wdXBzL2VuYWJsZWQuaHRtbCIsInJvb3RfaGFzaCI6Ik5Tb1NtNzFNS2JtWnJ5YmxTSm5wamgxa19WbmUwNnpMbkgwU2Z2cHl6VTgifSx7InBhdGgiOiJwb3B1cHMvZW5hYmxlZC5udXh0Lmh0bWwiLCJyb290X2hhc2giOiJ1WmpFNlBTY0pLWFNrc2pFZ3E4aGNtMlVOcnBYaElFX2xTaUNaYTJZQWZ3In0seyJwYXRoIjoicG9wdXBzL2VuYWJsZWQudml0ZXByZXNzLmh0bWwiLCJyb290X2hhc2giOiJUbnFXbDB2R2hTc2lHMzNlWFg5Q1A3NVh4UzdPYTdTU3kxOHJXMTVHbVcwIn0seyJwYXRoIjoicG9wdXBzL25vdC1mb3VuZC5odG1sIiwicm9vdF9oYXNoIjoieVdja3FtOGhqQlJONGg1em5vamJXTkt4TDJmVTJxYmZqTm5yVjc0TkdhUSJ9LHsicGF0aCI6InBvcHVwcy9wb3B1cC5jc3MiLCJyb290X2hhc2giOiJJOHRHYk5Takt5TTBUWDNLMkg1dzZDejh5SktXTVQ0eHphWHdRQUdBQUgwIn0seyJwYXRoIjoicG9wdXBzL3Z1ZTItbWlncmF0aW9uLWd1aWRlLmh0bWwiLCJyb290X2hhc2giOiI2a3d0djRvZ21fQ3ViSEF2aDRpYWRXTkFMaWFyb2xWTTZTU3JuNW03UVNVIn1dLCJmb3JtYXQiOiJ0cmVlaGFzaCIsImhhc2hfYmxvY2tfc2l6ZSI6NDA5Nn1dLCJpdGVtX2lkIjoibmhkb2dqbWVqaWdsaXBjY3Bubm5hbmhibGVkYWpicGQiLCJpdGVtX3ZlcnNpb24iOiI3LjcuNyIsInByb3RvY29sX3ZlcnNpb24iOjF9","signatures":[{"header":{"kid":"publisher"},"protected":"eyJhbGciOiJSUzI1NiJ9","signature":"AaJM8_u5YMqute66JhJoo08YkLOk0YMJxAWeCO1E3eZlv4XOu390crdToUgdy3HWJt-Zk-8vkfI7gLw6LaGR7P_A0j_Mm0oH0n6YHDZJr2fMWpToF9VsnvLr7a5z8durCaW2HOBwB0f4id5xEbLsjgXCttuIK1S153J59dCR57l4_7KORQDhbHUa7okZFLwqcf0ANJGw0mIXHqtMqXFzfXrUasTFC0tJx8Zz3NtvVwBQkvZ3V8kteo_LHM2katnfIjYhVSWG6x_lfKbpUeVvLjqBv2ctqAhm1wd1N9IhssaxCZeFN74bSp0Vi4JYFryZ7sZctzusFzo8qPRPzc_Hgg"},{"header":{"kid":"webstore"},"protected":"eyJhbGciOiJSUzI1NiJ9","signature":"SaLQAx-UrwO2kmq2EvG6f08wkfm8cVtb_Y0vBU3rTiOl7ZMqFKuDcO9cbdWWwlCS2khQztRlkPoCBHTronAvDII8szin72uA4Z7sY0eQGfeHRCH5Pho4UesC_FL9SJPT1-t32m-jxfQMp54qy_jawMYyf4AvOc2O-Ri3mAOIPI2Dt_CKL1nSkOc53mRJsBxhwulQMDgFvkojXEYKxJsIn7Xn-FqEs3NZDeOvABoP__MSi8weRGXtX04AI_2ylZKE29M573AeCrDosIVKa3o2PQJcnRdnx375RIjb3vgH8UbeUbc9djYnlPkc59a8s8IqyHhLc8rs5DbgsXhcp52Jdg"}]}}] \ No newline at end of file diff --git a/src/vs/workbench/contrib/roopik/browser/commands/browserCommands.ts b/src/vs/workbench/contrib/roopik/browser/commands/browserCommands.ts index fd96312ec..8dd6cf628 100644 --- a/src/vs/workbench/contrib/roopik/browser/commands/browserCommands.ts +++ b/src/vs/workbench/contrib/roopik/browser/commands/browserCommands.ts @@ -278,7 +278,7 @@ export function registerBrowserCommands(): void { try { // Get status before restart const statusBefore = await mcpServerService.getStatus(); - const portBefore = statusBefore.port; + const portBefore = statusBefore.wsPort; // Show notification that restart is in progress notificationService.info('Restarting MCP Server...'); @@ -288,7 +288,7 @@ export function registerBrowserCommands(): void { // Get status after restart const statusAfter = await mcpServerService.getStatus(); - const portAfter = statusAfter.port; + const portAfter = statusAfter.wsPort; // Show success message if (portBefore === portAfter) { diff --git a/src/vs/workbench/contrib/roopik/browser/commands/mcpCommands.ts b/src/vs/workbench/contrib/roopik/browser/commands/mcpCommands.ts index ed24470b4..1564c8dd6 100644 --- a/src/vs/workbench/contrib/roopik/browser/commands/mcpCommands.ts +++ b/src/vs/workbench/contrib/roopik/browser/commands/mcpCommands.ts @@ -88,7 +88,7 @@ export function registerMcpCommands(): void { let message = `MCP Server: ${serverStatus}`; if (server.running) { - message += ` (HTTP: ${server.port}, WebSocket: ${server.wsPort})`; + message += ` (WebSocket: ${server.wsPort})`; } message += `\n\nAgents: ${registeredAgents.length} registered, ${installedAgents.length} installed`; diff --git a/src/vs/workbench/contrib/roopik/browser/commands/roopikToolsCommands.ts b/src/vs/workbench/contrib/roopik/browser/commands/roopikToolsCommands.ts index 5b6b438a6..ae59dcb31 100644 --- a/src/vs/workbench/contrib/roopik/browser/commands/roopikToolsCommands.ts +++ b/src/vs/workbench/contrib/roopik/browser/commands/roopikToolsCommands.ts @@ -671,5 +671,49 @@ export function registerRoopikToolsCommands(): void { } }); - // console.log('[Roopik] Registered 24 tool bridge commands for agent roopik-dio'); + // ======================================================================== + // Validate Components Command + // ======================================================================== + + registerAction2(class extends Action2 { + constructor() { + super({ + id: 'roopik.tools.validateComponents', + title: { value: 'Validate Components', original: 'Validate Components' }, + category: { value: 'Roopik', original: 'Roopik' }, + f1: false + }); + } + + async run(accessor: ServicesAccessor, args?: { canvasId?: string }): Promise { + const mainProcessService = accessor.get(IMainProcessService); + const channel = getToolsChannel(mainProcessService); + return channel.call('canvas_validate_components', args); + } + }); + + // ======================================================================== + // Component Screenshot Command + // TODO: Feature pending - race condition with webview init + // ======================================================================== + + // registerAction2(class extends Action2 { + // constructor() { + // super({ + // id: 'roopik.tools.componentScreenshot', + // title: { value: 'Component Screenshot', original: 'Component Screenshot' }, + // category: { value: 'Roopik', original: 'Roopik' }, + // f1: false + // }); + // } + // + // async run(accessor: ServicesAccessor, args?: { componentId: string }): Promise { + // if (!args?.componentId) { + // return { success: false, error: 'Component ID is required' }; + // } + // const mainProcessService = accessor.get(IMainProcessService); + // const channel = getToolsChannel(mainProcessService); + // return channel.call('component_screenshot', args); + // } + // }); } diff --git a/src/vs/workbench/contrib/roopik/browser/componentServiceClient.ts b/src/vs/workbench/contrib/roopik/browser/componentServiceClient.ts index 2ecd2f571..a4607be1d 100644 --- a/src/vs/workbench/contrib/roopik/browser/componentServiceClient.ts +++ b/src/vs/workbench/contrib/roopik/browser/componentServiceClient.ts @@ -22,7 +22,8 @@ import { ComponentCreatedEvent, ComponentBuildEvent, ComponentDeletedEvent, - ComponentUpdatedEvent + ComponentUpdatedEvent, + ComponentScreenshotRequestEvent } from '../common/component/componentService.js'; import { Component, @@ -49,6 +50,7 @@ export class ComponentServiceClient implements IComponentService { readonly onComponentBuilt: Event; readonly onComponentDeleted: Event; readonly onComponentUpdated: Event; + readonly onScreenshotRequested: Event; // ======================================================================== // Constructor @@ -65,6 +67,7 @@ export class ComponentServiceClient implements IComponentService { this.onComponentBuilt = this.channel.listen('onComponentBuilt'); this.onComponentDeleted = this.channel.listen('onComponentDeleted'); this.onComponentUpdated = this.channel.listen('onComponentUpdated'); + this.onScreenshotRequested = this.channel.listen('onScreenshotRequested'); } // ======================================================================== @@ -256,4 +259,25 @@ export class ComponentServiceClient implements IComponentService { clearRuntimeError(componentId: string): void { this.channel.call('clearRuntimeError', componentId); } + + // ======================================================================== + // Screenshot (Bidirectional IPC) + // ======================================================================== + + /** + * Request a component screenshot (bidirectional IPC) + * This is called by tools in main process. + * The browser contribution will listen to onScreenshotRequested and handle it. + */ + async requestComponentScreenshot(componentId: string): Promise { + return this.channel.call('requestComponentScreenshot', componentId); + } + + /** + * Deliver screenshot result (called by browser contribution) + * This sends the screenshot back to main process to resolve the pending promise. + */ + deliverComponentScreenshot(requestId: string, screenshot: string | null, error?: string): void { + this.channel.call('deliverComponentScreenshot', { requestId, screenshot, error }); + } } diff --git a/src/vs/workbench/contrib/roopik/browser/contributions/componentContribution.ts b/src/vs/workbench/contrib/roopik/browser/contributions/componentContribution.ts index ae9b6fd75..4b52574a5 100644 --- a/src/vs/workbench/contrib/roopik/browser/contributions/componentContribution.ts +++ b/src/vs/workbench/contrib/roopik/browser/contributions/componentContribution.ts @@ -65,5 +65,43 @@ export class RoopikComponentContribution extends Disposable implements IWorkbenc changes: event.changes }); })); + + // Handle screenshot requests (bidirectional IPC) + this._register(this.componentService.onScreenshotRequested(async (event) => { + console.log(`[Browser] 📸 Screenshot requested from Core`, { requestId: event.requestId, componentId: event.componentId }); + try { + // Call extension command to capture screenshot + const screenshot = await this.commandService.executeCommand( + 'roopik.canvas.captureComponentScreenshot', + event.componentId + ); + + // Deliver result back to main process (coalesce undefined to null) + if (screenshot === null || screenshot === undefined) { + console.warn(`[Browser] ❌ Extension returned null/undefined for ${event.componentId}`); + this.componentService.deliverComponentScreenshot( + event.requestId, + null, + '[Browser] Extension command returned null - canvas panel may not be active or component not found' + ); + } else { + const preview = screenshot.substring(0, 100); + console.log(`[Browser] ✅ Screenshot received from extension, delivering to Core`, { + requestId: event.requestId, + componentId: event.componentId, + dataUrlLength: screenshot.length, + preview + }); + this.componentService.deliverComponentScreenshot(event.requestId, screenshot); + } + } catch (error) { + console.error(`[Browser] ❌ Error capturing screenshot`, { requestId: event.requestId, componentId: event.componentId, error }); + // Deliver error back to main process + const errorMessage = error instanceof Error + ? `[Browser] ${error.message}` + : '[Browser] Screenshot capture command failed'; + this.componentService.deliverComponentScreenshot(event.requestId, null, errorMessage); + } + })); } } diff --git a/src/vs/workbench/contrib/roopik/common/component/componentService.ts b/src/vs/workbench/contrib/roopik/common/component/componentService.ts index f1a065def..ba2ced87a 100644 --- a/src/vs/workbench/contrib/roopik/common/component/componentService.ts +++ b/src/vs/workbench/contrib/roopik/common/component/componentService.ts @@ -58,6 +58,15 @@ export interface ComponentUpdatedEvent { changes: ('componentName' | 'source')[]; } +/** + * Event fired when a component screenshot is requested (bidirectional IPC) + * Browser process should listen to this event, call extension command, and deliver result via deliverScreenshot() + */ +export interface ComponentScreenshotRequestEvent { + requestId: string; + componentId: string; +} + // ============================================================================ // Service Interface // ============================================================================ @@ -344,4 +353,33 @@ export interface IComponentService { * Fired when component metadata changes (name, etc.) */ readonly onComponentUpdated: Event; + + /** + * Fired when a screenshot is requested (bidirectional IPC pattern) + * Browser process should listen and deliver result via deliverScreenshot() + */ + readonly onScreenshotRequested: Event; + + // ======================================================================== + // Screenshot (Bidirectional IPC) + // ======================================================================== + + /** + * Request a component screenshot (used by tools) + * Returns a promise that resolves when browser delivers the screenshot via deliverScreenshot() + * + * @param componentId - Component to screenshot + * @returns Promise - Base64 data URL of screenshot + */ + requestComponentScreenshot(componentId: string): Promise; + + /** + * Deliver screenshot result (called by browser process after extension captures it) + * Resolves the pending promise created by requestComponentScreenshot() + * + * @param requestId - Request ID from ComponentScreenshotRequestEvent + * @param screenshot - Base64 data URL or null if failed + * @param error - Error message if screenshot failed + */ + deliverComponentScreenshot(requestId: string, screenshot: string | null, error?: string): void; } diff --git a/src/vs/workbench/contrib/roopik/electron-main/channel/componentChannel.ts b/src/vs/workbench/contrib/roopik/electron-main/channel/componentChannel.ts index 252d04287..f6fcbdb6d 100644 --- a/src/vs/workbench/contrib/roopik/electron-main/channel/componentChannel.ts +++ b/src/vs/workbench/contrib/roopik/electron-main/channel/componentChannel.ts @@ -35,6 +35,8 @@ export class ComponentChannel implements IServerChannel { return this.service.onComponentDeleted; case 'onComponentUpdated': return this.service.onComponentUpdated; + case 'onScreenshotRequested': + return this.service.onScreenshotRequested; default: throw new Error(`[ComponentChannel] Unknown event: ${event}`); } @@ -143,6 +145,17 @@ export class ComponentChannel implements IServerChannel { this.service.clearRuntimeError(arg as string); return Promise.resolve(); + // ================================================================ + // Screenshot (Bidirectional IPC) + // ================================================================ + case 'requestComponentScreenshot': + return this.service.requestComponentScreenshot(arg as string); + case 'deliverComponentScreenshot': { + const { requestId, screenshot, error } = arg as { requestId: string; screenshot: string | null; error?: string }; + this.service.deliverComponentScreenshot(requestId, screenshot, error); + return Promise.resolve(); + } + default: throw new Error(`[ComponentChannel] Unknown command: ${command}`); } diff --git a/src/vs/workbench/contrib/roopik/electron-main/channel/roopikToolsChannel.ts b/src/vs/workbench/contrib/roopik/electron-main/channel/roopikToolsChannel.ts index 2f704fd9f..39915257b 100644 --- a/src/vs/workbench/contrib/roopik/electron-main/channel/roopikToolsChannel.ts +++ b/src/vs/workbench/contrib/roopik/electron-main/channel/roopikToolsChannel.ts @@ -24,8 +24,8 @@ * browser_set_viewport, browser_get_network_requests * - Project Tools (3): project_get_active, project_start, project_stop * - Canvas Tools (3): canvas_list, canvas_get_active, canvas_create - * - Component Tools (6): component_add, component_add_batch, component_remove, - * component_get_info, component_list, component_rebuild + * - Component Tools (7): component_add, component_add_batch, component_remove, + * component_get_info, component_list, component_rebuild, canvas_validate_components */ import { Event } from '../../../../../base/common/event.js'; @@ -81,9 +81,9 @@ export class RoopikToolsChannel implements IServerChannel { // Create unified tool services this.browserToolService = new BrowserToolService(browserViewService, this.cdpMonitorService); - this.canvasToolService = new CanvasToolService(canvasService); + this.canvasToolService = new CanvasToolService(canvasService, componentService); this.componentToolService = new ComponentToolService(componentService, canvasService); - this.projectToolService = new ProjectToolService(devServerService, storageService); + this.projectToolService = new ProjectToolService(devServerService, storageService, browserViewService); } /** @@ -214,6 +214,13 @@ export class RoopikToolsChannel implements IServerChannel { case 'component_rebuild': return this.handleRebuildComponent(arg as { componentId: string }); + case 'canvas_validate_components': + return this.handleValidateComponents(arg as { canvasId?: string }); + + // TODO: Feature pending - race condition with webview init + // case 'component_screenshot': + // return this.handleComponentScreenshot(arg as { componentId: string; canvasId: string }); + default: return { success: false, @@ -807,4 +814,14 @@ export class RoopikToolsChannel implements IServerChannel { // Delegate to unified ComponentToolService return this.componentToolService.rebuild(args.componentId); } + + private async handleValidateComponents(args: { canvasId?: string }): Promise { + // Delegate to unified ComponentToolService + return this.componentToolService.validateComponents(args.canvasId); + } + + // TODO: Feature pending - race condition with webview init + // private async handleComponentScreenshot(args: { componentId: string; canvasId: string }): Promise { + // return this.componentToolService.screenshot(args.componentId, args.canvasId); + // } } diff --git a/src/vs/workbench/contrib/roopik/electron-main/component/componentService.ts b/src/vs/workbench/contrib/roopik/electron-main/component/componentService.ts index 3111cc513..38b2ee9a8 100644 --- a/src/vs/workbench/contrib/roopik/electron-main/component/componentService.ts +++ b/src/vs/workbench/contrib/roopik/electron-main/component/componentService.ts @@ -25,7 +25,8 @@ import { ComponentCreatedEvent, ComponentBuildEvent, ComponentDeletedEvent, - ComponentUpdatedEvent + ComponentUpdatedEvent, + ComponentScreenshotRequestEvent } from '../../common/component/componentService.js'; import { Component, @@ -150,6 +151,16 @@ export class ComponentService extends Disposable implements IComponentService { private readonly _onComponentUpdated = this._register(new Emitter()); readonly onComponentUpdated: Event = this._onComponentUpdated.event; + private readonly _onScreenshotRequested = this._register(new Emitter()); + readonly onScreenshotRequested: Event = this._onScreenshotRequested.event; + + // Screenshot request tracking (bidirectional IPC) + private screenshotRequests = new Map void; + reject: (error: Error) => void; + timeout: ReturnType; + }>(); + // ======================================================================== // Constructor // ======================================================================== @@ -1199,4 +1210,51 @@ export class ComponentService extends Disposable implements IComponentService { this.logger.debug('Runtime error cleared for component', { componentId }); } } + + // ======================================================================== + // Screenshot (Bidirectional IPC) + // ======================================================================== + + /** + * Request a component screenshot (bidirectional IPC pattern) + * + * Flow: + * 1. Fire onScreenshotRequested event with requestId + componentId + * 2. Browser listens to event and calls extension command + * 3. Extension captures screenshot from webview + * 4. Browser calls deliverComponentScreenshot() with result + * 5. Promise resolves with screenshot data + * + * @param componentId - Component to screenshot + * @returns Promise - Base64 data URL of screenshot + */ + requestComponentScreenshot(componentId: string): Promise { + const requestId = `screenshot-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; + + return new Promise((resolve, reject) => { + const timeout = setTimeout(() => { + this.screenshotRequests.delete(requestId); + reject(new Error('Screenshot request timed out after 30 seconds')); + }, 30000); + + this.screenshotRequests.set(requestId, { resolve, reject, timeout }); + this._onScreenshotRequested.fire({ requestId, componentId }); + }); + } + + deliverComponentScreenshot(requestId: string, screenshot: string | null, error?: string): void { + const request = this.screenshotRequests.get(requestId); + if (!request) { + return; + } + + clearTimeout(request.timeout); + this.screenshotRequests.delete(requestId); + + if (screenshot) { + request.resolve(screenshot); + } else { + request.reject(new Error(error || 'Screenshot capture failed')); + } + } } diff --git a/src/vs/workbench/contrib/roopik/electron-main/mcp/executor/toolExecutor.ts b/src/vs/workbench/contrib/roopik/electron-main/mcp/executor/toolExecutor.ts index f5eb55f8d..056b0aa49 100644 --- a/src/vs/workbench/contrib/roopik/electron-main/mcp/executor/toolExecutor.ts +++ b/src/vs/workbench/contrib/roopik/electron-main/mcp/executor/toolExecutor.ts @@ -71,9 +71,9 @@ export class ToolExecutor { // Create unified tool services this.browserToolService = new BrowserToolService(browserViewService, this.cdpMonitorService); - this.canvasToolService = new CanvasToolService(canvasService); + this.canvasToolService = new CanvasToolService(canvasService, componentService); this.componentToolService = new ComponentToolService(componentService, canvasService); - this.projectToolService = new ProjectToolService(devServerService, storageService); + this.projectToolService = new ProjectToolService(devServerService, storageService, browserViewService); } /** @@ -132,17 +132,19 @@ export class ToolExecutor { 'browser_get_state', 'browser_set_viewport', 'browser_get_network_requests', - // Canvas tools (3) + // Canvas tools (4) 'canvas_list', 'canvas_get_active', 'canvas_create', - // Component tools (6) + 'canvas_validate_components', + // Component tools (8) 'component_add', 'component_add_batch', 'component_remove', 'component_get_info', 'component_list', 'component_rebuild', + // 'component_screenshot', // TODO: Disabled - race condition with webview init // Project tools (3) 'project_get_active', 'project_start', @@ -237,7 +239,7 @@ export class ToolExecutor { } // ========================================================================== - // Canvas Tool Routing (3 tools) - Delegates to CanvasToolService + // Canvas Tool Routing (4 tools) - Delegates to CanvasToolService // ========================================================================== private async executeCanvasTool(tool: string, params: Record): Promise> { @@ -255,6 +257,17 @@ export class ToolExecutor { case 'canvas_create': return this.canvasToolService.create(params.name as string); + case 'canvas_open': + return this.canvasToolService.open({ + canvasId: params.canvasId as string | undefined, + name: params.name as string | undefined, + }); + + case 'canvas_validate_components': + // Note: Despite the 'canvas_' prefix, this is implemented in ComponentToolService + // because it operates on components within a canvas + return this.componentToolService.validateComponents(params.canvasId as string | undefined); + default: return { success: false, @@ -264,7 +277,7 @@ export class ToolExecutor { } // ========================================================================== - // Component Tool Routing (6 tools) - Delegates to ComponentToolService + // Component Tool Routing (8 tools) - Delegates to ComponentToolService // ========================================================================== private async executeComponentTool(tool: string, params: Record): Promise> { @@ -302,6 +315,13 @@ export class ToolExecutor { case 'component_rebuild': return this.componentToolService.rebuild(params.componentId as string); + // TODO: Disabled - race condition with webview initialization + // case 'component_screenshot': + // return this.componentToolService.screenshot( + // params.componentId as string, + // params.canvasId as string + // ); + default: return { success: false, diff --git a/src/vs/workbench/contrib/roopik/electron-main/mcp/mcpServerService.ts b/src/vs/workbench/contrib/roopik/electron-main/mcp/mcpServerService.ts index 49f2dfa76..cfe3fce13 100644 --- a/src/vs/workbench/contrib/roopik/electron-main/mcp/mcpServerService.ts +++ b/src/vs/workbench/contrib/roopik/electron-main/mcp/mcpServerService.ts @@ -310,12 +310,12 @@ export class McpServerService extends Disposable implements IMcpServerService { // Initialize Tool Executor if needed if (!this.toolExecutor) { this.toolExecutor = new ToolExecutor( - this.browserViewService, - this.storageService, - this.canvasService, - this.componentService, - this.devServerService - ); + this.browserViewService, + this.storageService, + this.canvasService, + this.componentService, + this.devServerService + ); } await this.startWebSocketServer(); await this.initializeInstaller(); diff --git a/src/vs/workbench/contrib/roopik/electron-main/mcp/toolSchemas.ts b/src/vs/workbench/contrib/roopik/electron-main/mcp/toolSchemas.ts index e9e78371b..d55ce5a23 100644 --- a/src/vs/workbench/contrib/roopik/electron-main/mcp/toolSchemas.ts +++ b/src/vs/workbench/contrib/roopik/electron-main/mcp/toolSchemas.ts @@ -84,7 +84,7 @@ export const browserGetNetworkRequestsSchema = z.object({ export const emptySchema = z.object({}); // ============================================================================ -// Canvas Tool Schemas (3) +// Canvas Tool Schemas (4) // ============================================================================ export const canvasListSchema = z.object({ @@ -97,6 +97,11 @@ export const canvasCreateSchema = z.object({ name: z.string().describe('Canvas name') }); +export const canvasOpenSchema = z.object({ + canvasId: z.string().optional().describe('Canvas ID to open'), + name: z.string().optional().describe('Canvas name to open (will look up by name)') +}); + // ============================================================================ // Component Tool Schemas (6) // ============================================================================ @@ -137,6 +142,16 @@ export const componentRebuildSchema = z.object({ componentId: z.string().describe('Component ID') }); +export const componentValidateSchema = z.object({ + canvasId: z.string().optional().describe('Canvas ID (uses active canvas if not provided)') +}); + +// TODO: Feature pending - has race condition with webview initialization +// export const componentScreenshotSchema = z.object({ +// componentId: z.string().describe('Component ID to screenshot'), +// canvasId: z.string().describe('Canvas ID where the component is located') +// }); + // ============================================================================ // Project Tool Schemas (3) // ============================================================================ @@ -164,7 +179,7 @@ export const TOOL_DEFINITIONS: ToolDefinition[] = [ // ========== Browser Tools (14) ========== { name: 'browser_open', - description: 'Open the browser view. Optionally navigate to a URL.', + description: 'Open the browser view. Optionally navigate to a URL. For local project files: file:// URLs are not supported. Use project_start for Vite-based projects, or manually start a server (e.g., npx serve) and use browser_navigate with the http://localhost address.', schema: browserOpenSchema }, { @@ -179,7 +194,7 @@ export const TOOL_DEFINITIONS: ToolDefinition[] = [ }, { name: 'browser_navigate', - description: 'Navigate to a URL in the browser.', + description: 'Navigate to a URL in the browser. For local project files: file:// URLs are not supported. Use project_start for Vite-based projects, or manually start a server (e.g., npx serve) and use the http://localhost address.', schema: browserNavigateSchema }, { @@ -233,7 +248,7 @@ export const TOOL_DEFINITIONS: ToolDefinition[] = [ schema: browserGetNetworkRequestsSchema }, - // ========== Canvas Tools (3) ========== + // ========== Canvas Tools (4) ========== { name: 'canvas_list', description: 'List all canvases.', @@ -246,9 +261,14 @@ export const TOOL_DEFINITIONS: ToolDefinition[] = [ }, { name: 'canvas_create', - description: 'Create a new canvas or get existing one with same name.', + description: 'Create a new canvas or get existing one with same name. Use canvas mode for design exploration and component iteration before committing to a full project. If user asks for any design/creative/UI work (e.g., "design a landing page", "show me some ideas", "create a dashboard", "build a login form") and intent is unclear, ask if they want canvas mode (component designs side-by-side to iterate) or project mode (full running app).', schema: canvasCreateSchema }, + { + name: 'canvas_open', + description: 'Open an existing canvas by ID or name. Opens the canvas panel in the UI and returns canvas info with all components (id, name, path, status).', + schema: canvasOpenSchema + }, // ========== Component Tools (6) ========== { @@ -281,6 +301,17 @@ export const TOOL_DEFINITIONS: ToolDefinition[] = [ description: 'Trigger rebuild of a component.', schema: componentRebuildSchema }, + { + name: 'canvas_validate_components', + description: 'Validate all components in a canvas. Returns summary (total, success, failed, building counts) plus detailed error info for failed components. Efficient way to check component health without individual calls.', + schema: componentValidateSchema + }, + // TODO: Feature pending - has race condition with webview initialization + // { + // name: 'component_screenshot', + // description: 'Capture a screenshot of a specific component rendered in the canvas. Requires both componentId and canvasId. The canvas will be opened/focused if not already visible. Returns base64 data URL of the component\'s visual appearance.', + // schema: componentScreenshotSchema + // }, // ========== Project Tools (3) ========== { @@ -290,7 +321,7 @@ export const TOOL_DEFINITIONS: ToolDefinition[] = [ }, { name: 'project_start', - description: 'Start a development server for a project.', + description: 'Start a development server for a Vite-based project (React, Vue, Svelte, etc. with Vite). Use project mode when user explicitly wants a full running app (e.g., "build me a Vite React app", "create a full running project"). If user asks for design/creative/UI work and intent is unclear, ask if they want canvas mode (component iteration) or project mode (full running app) first.', schema: projectStartSchema }, { @@ -317,4 +348,4 @@ export function getToolDefinitionsAsJsonSchema(): Array<{ })); } -// Total: 26 Tools +// Total: 29 Tools (14 Browser + 4 Canvas + 8 Component + 3 Project) diff --git a/src/vs/workbench/contrib/roopik/electron-main/projectMode/browserViewService.ts b/src/vs/workbench/contrib/roopik/electron-main/projectMode/browserViewService.ts index 65f85ebb2..461f28e23 100644 --- a/src/vs/workbench/contrib/roopik/electron-main/projectMode/browserViewService.ts +++ b/src/vs/workbench/contrib/roopik/electron-main/projectMode/browserViewService.ts @@ -60,6 +60,13 @@ export class BrowserViewService extends Disposable implements IProjectModeServic private readonly _onMcpBrowserCloseRequest = new Emitter(); readonly onMcpBrowserCloseRequest: Event = this._onMcpBrowserCloseRequest.event; + // CDP Monitoring Lifecycle Events + private readonly _onBrowserViewCreated = new Emitter<{ browserViewId: number }>(); + readonly onBrowserViewCreated: Event<{ browserViewId: number }> = this._onBrowserViewCreated.event; + + private readonly _onBrowserViewDestroyed = new Emitter<{ browserViewId: number }>(); + readonly onBrowserViewDestroyed: Event<{ browserViewId: number }> = this._onBrowserViewDestroyed.event; + // Static set of managed webContents IDs for navigation whitelist // This is used by app.ts to allow navigation for our browser views private static managedWebContentsIds = new Set(); @@ -233,23 +240,24 @@ export class BrowserViewService extends Disposable implements IProjectModeServic allowRunningInsecureContent: true, session: browserSession // CRITICAL: Use our configured session for localhost support } - }); + }); + + // CRITICAL: Get ID and register BEFORE adding to window + // Adding to window can trigger immediate navigation attempts! + const browserViewId = browserView.webContents.id; + const debuggingPort = this.debuggingPortCounter++; + BrowserViewService.managedWebContentsIds.add(browserViewId); + + // Now safe to add to window - ID is already whitelisted + window.contentView.addChildView(browserView); - // Add to window - window.contentView.addChildView(browserView); - const browserViewId = browserView.webContents.id; - const debuggingPort = this.debuggingPortCounter++; this.logger.info('Browser view created', { browserViewId, windowId, debuggingPort, webContentsId: browserView.webContents.id }); // Store references this.browserViews.set(browserViewId, browserView); this.browserWindows.set(browserViewId, window); - - // Add to static set for navigation whitelist - BrowserViewService.managedWebContentsIds.add(browserViewId); - // Setup zoom handlers (keyboard + touchpad pinch) this.setupZoomHandlers(browserView); @@ -263,12 +271,18 @@ export class BrowserViewService extends Disposable implements IProjectModeServic // ========================================================================= this.attachSafetyLeash(window, browserViewId); + // Fire event for CDP monitoring to auto-initialize + this._onBrowserViewCreated.fire({ browserViewId }); + return { browserViewId, debuggingPort }; } async destroyBrowserView(browserViewId: number): Promise { this.logger.info('destroyBrowserView called', { browserViewId }); + // Fire event for CDP monitoring cleanup + this._onBrowserViewDestroyed.fire({ browserViewId }); + // Cleanup CDP monitoring if active (from MCP CDP tools) cleanupCDPMonitoring(browserViewId); diff --git a/src/vs/workbench/contrib/roopik/electron-main/tools/browserToolService.ts b/src/vs/workbench/contrib/roopik/electron-main/tools/browserToolService.ts index c37da78d7..4b4a5e794 100644 --- a/src/vs/workbench/contrib/roopik/electron-main/tools/browserToolService.ts +++ b/src/vs/workbench/contrib/roopik/electron-main/tools/browserToolService.ts @@ -61,9 +61,9 @@ export class BrowserToolService { // Browser is already open - navigate if URL provided if (url) { - await this.browserViewService.navigate(browserViewId, url); - // Enable CDP monitoring proactively - await this.cdpMonitorService.ensureMonitoring(browserViewId); + // Enable CDP monitoring BEFORE navigating to capture all network events + await this.cdpMonitorService.ensureMonitoring(browserViewId); + await this.browserViewService.navigate(browserViewId, url); return { success: true, data: { @@ -166,10 +166,10 @@ export class BrowserToolService { return { success: false, error: 'No browser is open. Use browser_open first.' }; } + // Enable CDP monitoring BEFORE navigating to capture all network events + await this.cdpMonitorService.ensureMonitoring(browserViewId); await this.browserViewService.navigate(browserViewId, url); - // Enable CDP monitoring proactively - await this.cdpMonitorService.ensureMonitoring(browserViewId); return { success: true, @@ -194,10 +194,10 @@ export class BrowserToolService { return { success: false, error: 'No browser is open. Use browser_open first.' }; } + // Enable CDP monitoring BEFORE reloading to capture all network events + await this.cdpMonitorService.ensureMonitoring(browserViewId); await this.browserViewService.reload(browserViewId, ignoreCache); - // Enable CDP monitoring proactively - await this.cdpMonitorService.ensureMonitoring(browserViewId); // Get current URL from navigation state const navState = await this.browserViewService.getNavigationState(browserViewId); diff --git a/src/vs/workbench/contrib/roopik/electron-main/tools/canvasToolService.ts b/src/vs/workbench/contrib/roopik/electron-main/tools/canvasToolService.ts index 24716cb8e..4b82b75dc 100644 --- a/src/vs/workbench/contrib/roopik/electron-main/tools/canvasToolService.ts +++ b/src/vs/workbench/contrib/roopik/electron-main/tools/canvasToolService.ts @@ -11,10 +11,12 @@ */ import type { ICanvasService } from '../../common/canvas/canvasService.js'; +import type { ComponentService } from '../component/componentService.js'; import type { ToolResult, CanvasListResult, CanvasInfo, + ComponentInfo, } from '../mcp/executor/types.js'; // ============================================================================ @@ -23,7 +25,8 @@ import type { export class CanvasToolService { constructor( - private readonly canvasService: ICanvasService + private readonly canvasService: ICanvasService, + private readonly componentService?: ComponentService ) {} // ========================================================================== @@ -177,4 +180,89 @@ export class CanvasToolService { }; } } + + // ========================================================================== + // Open Canvas + // ========================================================================== + + async open(params: { canvasId?: string; name?: string }): Promise> { + try { + // Need either canvasId or name + if (!params.canvasId && !params.name) { + return { + success: false, + error: 'Must provide either canvasId or name' + }; + } + + let canvas; + + // If canvasId provided, look up by ID + if (params.canvasId) { + canvas = await this.canvasService.getCanvasAsync(params.canvasId); + if (!canvas) { + return { + success: false, + error: `Canvas not found: ${params.canvasId}` + }; + } + } + // If name provided, look up by name in the list + else if (params.name) { + const canvases = await this.canvasService.listCanvasesAsync(); + canvas = canvases.find(c => c.name.toLowerCase() === params.name!.toLowerCase()); + if (!canvas) { + return { + success: false, + error: `Canvas not found: ${params.name}` + }; + } + } + + // Trigger the canvas to open by calling createCanvas with the existing name + // This will fire the onCanvasCreated event which opens the UI panel + const result = await this.canvasService.createCanvas(canvas!.name); + + // Get components list if componentService is available + let components: ComponentInfo[] | undefined; + if (this.componentService) { + const componentsInCanvas = this.componentService.getComponentsForCanvas(result.canvasId); + components = componentsInCanvas.map(c => { + // Runtime error takes precedence over build status + let status: 'pending' | 'building' | 'ready' | 'error'; + if (c.runtimeError) { + status = 'error'; // Component crashed at runtime + } else { + status = c.buildState.status; + } + + return { + id: c.id, + name: c.componentName, + path: c.folderPath, + canvasId: c.canvasId, + status + }; + }); + } + + return { + success: true, + data: { + id: result.canvasId, + name: result.canvas.name, + description: result.canvas.description, + componentCount: result.canvas.componentCount || 0, + createdAt: result.canvas.createdAt, + updatedAt: result.canvas.updatedAt, + components + } + }; + } catch (error) { + return { + success: false, + error: error instanceof Error ? error.message : 'Failed to open canvas' + }; + } + } } diff --git a/src/vs/workbench/contrib/roopik/electron-main/tools/cdpMonitorService.ts b/src/vs/workbench/contrib/roopik/electron-main/tools/cdpMonitorService.ts index 8fbec303e..458d4e9fe 100644 --- a/src/vs/workbench/contrib/roopik/electron-main/tools/cdpMonitorService.ts +++ b/src/vs/workbench/contrib/roopik/electron-main/tools/cdpMonitorService.ts @@ -137,7 +137,21 @@ export function cleanupCDPMonitoring(browserViewId: number): void { export class CDPMonitorService { constructor( private readonly browserViewService: BrowserViewService - ) { } + ) { + // Auto-initialize CDP monitoring for ALL browser views + // This ensures monitoring works regardless of how browser was opened (tool, manual, etc.) + this.browserViewService.onBrowserViewCreated(({ browserViewId }) => { + // Initialize asynchronously, don't block browser creation + this.ensureMonitoring(browserViewId).catch(err => { + console.error('[CDP] Failed to initialize monitoring for browserViewId:', browserViewId, err); + }); + }); + + // Optional: Explicit cleanup on destroy (though cleanup already happens in destroyBrowserView) + this.browserViewService.onBrowserViewDestroyed(({ browserViewId }) => { + this.cleanup(browserViewId); + }); + } // ========================================================================== // Public API @@ -171,7 +185,10 @@ export class CDPMonitorService { // Attach debugger (throws on failure) try { await this.browserViewService.attachDebugger(browserViewId); - } catch { + } catch (err) { + // Log the actual error instead of silently failing + console.error('[CDP] Failed to attach debugger to browserViewId:', browserViewId, err); + // Don't throw - CDP monitoring is optional, browser should still work return; } @@ -497,14 +514,15 @@ export class CDPMonitorService { // ========================================================================== /** - * Clear all data for a monitor (called on Page.loadEventFired). - * Gives agents a fresh state after page reload/HMR. + * Clear console logs only (called on Page.loadEventFired). + * Console logs are cleared to remove HMR noise after page reload. + * Network requests are KEPT so agents can analyze them after page load completes. */ private clearMonitorData(monitor: CDPMonitor): void { + // Only clear console logs - they're noisy with HMR/Vite spam monitor.consoleLogs = []; - monitor.networkRequests = []; - monitor.networkResponses = []; - monitor.requestStartTimes.clear(); + // Keep network requests - agents need these after page loads! + // Network requests will accumulate until browser is closed or manually cleared } /** diff --git a/src/vs/workbench/contrib/roopik/electron-main/tools/componentToolService.ts b/src/vs/workbench/contrib/roopik/electron-main/tools/componentToolService.ts index 8cca73e11..3d5046f2c 100644 --- a/src/vs/workbench/contrib/roopik/electron-main/tools/componentToolService.ts +++ b/src/vs/workbench/contrib/roopik/electron-main/tools/componentToolService.ts @@ -285,4 +285,177 @@ export class ComponentToolService { }; } } + + // ========================================================================== + // Validate Components (Batch Status Check) + // ========================================================================== + + async validateComponents(canvasId?: string): Promise; + }>> { + try { + // Get canvas ID (use active if not provided) + let targetCanvasId = canvasId; + if (!targetCanvasId) { + const activeCanvasId = await this.canvasService.getFocusedCanvasIdAsync(); + if (!activeCanvasId) { + return { + success: false, + error: 'No active canvas. Specify canvasId or open a canvas first.' + }; + } + targetCanvasId = activeCanvasId; + } + + // Get all components for canvas + const components = this.componentService.getComponentsForCanvas(targetCanvasId); + + // Gather detailed info for each component + const componentDetails = await Promise.all( + components.map(async (c) => { + const info = await this.componentService.getComponentInfo(c.id); + + // Determine final status (runtime error overrides build status) + let buildState: string; + if (info.runtimeError) { + buildState = 'error'; + } else { + buildState = info.buildStatus; + } + + // Create base result object (always include these fields) + const result: { + id: string; + name: string; + buildState: string; + folderPath?: string; + error?: string; + errorStack?: string; + runtimeError?: { message: string; type: string; stack?: string }; + timestamp?: number; + } = { + id: c.id, + name: c.componentName || 'Unknown', + buildState + }; + + // For success/ready, return minimal data + if (buildState === 'ready' && !info.runtimeError) { + return result; + } + + // For errors/building, add detailed data + result.folderPath = c.folderPath; + + // Add build error details + if (info.buildStatus === 'error' && info.buildErrorInfo) { + result.error = info.buildErrorInfo.message; + result.timestamp = Date.now(); + } + + // Add runtime error details (takes precedence) + if (info.runtimeError) { + result.runtimeError = { + message: info.runtimeError.message, + type: info.runtimeError.type, + stack: info.runtimeError.stack + }; + } + + return result; + }) + ); + + // Calculate summary + let success = 0; + let failed = 0; + let building = 0; + + for (const comp of componentDetails) { + if (comp.buildState === 'ready' && !comp.runtimeError) { + success++; + } else if (comp.buildState === 'building') { + building++; + } else if (comp.buildState === 'error' || comp.runtimeError) { + failed++; + } + } + + return { + success: true, + data: { + summary: { + total: components.length, + success, + failed, + building, + hasErrors: failed > 0 + }, + components: componentDetails + } + }; + } catch (error) { + return { + success: false, + error: error instanceof Error ? error.message : 'Failed to validate components' + }; + } + } + + // ========================================================================== + // Component Screenshot + // ========================================================================== + + // TODO: Feature pending - race condition with webview initialization needs proper fix + async screenshot(componentId: string, canvasId: string): Promise> { + try { + const component = this.componentService.getComponent(componentId); + if (!component) { + return { + success: false, + error: `Component not found: ${componentId}` + }; + } + + if (component.canvasId !== canvasId) { + return { + success: false, + error: `Component ${componentId} is not in canvas ${canvasId}. Component belongs to canvas: ${component.canvasId}` + }; + } + + await this.canvasService.createCanvas(canvasId); + await new Promise(resolve => setTimeout(resolve, 1000)); + const screenshot = await this.componentService.requestComponentScreenshot(componentId); + + return { + success: true, + data: { + image: screenshot, + format: 'data-url' + } + }; + } catch (error) { + return { + success: false, + error: error instanceof Error ? error.message : 'Failed to capture component screenshot' + }; + } + } } diff --git a/src/vs/workbench/contrib/roopik/electron-main/tools/projectToolService.ts b/src/vs/workbench/contrib/roopik/electron-main/tools/projectToolService.ts index 2eabad2dc..f620e4861 100644 --- a/src/vs/workbench/contrib/roopik/electron-main/tools/projectToolService.ts +++ b/src/vs/workbench/contrib/roopik/electron-main/tools/projectToolService.ts @@ -11,12 +11,15 @@ */ import type { DevServerService } from '../projectMode/devServer/devServerService.js'; +import type { IRoopikStorageService } from '../../common/storage/storageService.js'; +import type { BrowserViewService } from '../projectMode/browserViewService.js'; import type { ToolResult, ProjectServerInfo, ProjectStartResult, ProjectStopResult, } from '../mcp/executor/types.js'; +import { resolve, normalize } from '../../../../../base/common/path.js'; // ============================================================================ // Project Tool Service @@ -24,7 +27,9 @@ import type { export class ProjectToolService { constructor( - private readonly devServerService: DevServerService + private readonly devServerService: DevServerService, + private readonly storageService: IRoopikStorageService, + private readonly browserViewService: BrowserViewService ) {} // ========================================================================== @@ -80,22 +85,47 @@ export class ProjectToolService { }; } + // Resolve relative paths against workspace (handles both relative and absolute) + let inputPath = projectPath; + + // On non-Windows platforms, convert backslashes to forward slashes + if (process.platform !== 'win32') { + inputPath = inputPath.replace(/\\/g, '/'); + } + + const workspacePath = this.storageService.getWorkspaceRootPath(); + const resolvedPath = resolve(workspacePath, normalize(inputPath)); + // Start the dev server - returns URL on success, throws on failure const url = await this.devServerService.startServer({ - projectRoot: projectPath, + projectRoot: resolvedPath, port }); // Get server info to retrieve framework const serverInfo = await this.devServerService.getRunningServer(); + // Wait for server to be actually ready before navigating + await this.waitForServerReady(url); + + // Auto-open and navigate browser to the dev server URL + // This ensures browser shows the project immediately after starting + const browserViewId = this.browserViewService.getActiveBrowserViewId(); + if (browserViewId !== undefined) { + // Browser already open - navigate to URL + await this.browserViewService.navigate(browserViewId, url); + } else { + // Browser not open - request to open with URL + this.browserViewService.requestBrowserOpen(url); + } + return { success: true, data: { url, - projectRoot: projectPath, + projectRoot: resolvedPath, framework: serverInfo?.framework, - message: `Dev server starting for ${projectPath}` + message: `Dev server starting for ${resolvedPath}` } }; } catch (error) { @@ -141,4 +171,38 @@ export class ProjectToolService { }; } } + + /** + * Wait for server to be actually ready to accept connections + * Retries with exponential backoff up to ~5 seconds + */ + private async waitForServerReady(url: string): Promise { + const maxAttempts = 10; + const baseDelay = 100; // Start with 100ms + + for (let attempt = 0; attempt < maxAttempts; attempt++) { + try { + // Try to fetch the URL with GET to ensure HTML is actually compiled + const response = await fetch(url, { method: 'GET' }); + if (response.ok || response.status === 304) { + // Verify we got HTML content, not an error page + const contentType = response.headers.get('content-type') || ''; + if (contentType.includes('text/html')) { + // Add a small grace period for Vite to fully initialize + await new Promise(resolve => setTimeout(resolve, 200)); + return; + } + } + } catch { + // Server not ready yet, wait and retry + if (attempt < maxAttempts - 1) { + const delay = baseDelay * Math.pow(1.5, attempt); // Exponential backoff + await new Promise(resolve => setTimeout(resolve, delay)); + } + } + } + // If we get here, server didn't respond in time, but continue anyway + // The navigation might still work, or it will show a proper error + } + } diff --git a/tools/roopik/mcp-stdio-server/src/tools.ts b/tools/roopik/mcp-stdio-server/src/tools.ts index d0fc0a626..ed736f690 100644 --- a/tools/roopik/mcp-stdio-server/src/tools.ts +++ b/tools/roopik/mcp-stdio-server/src/tools.ts @@ -88,7 +88,7 @@ export const browserGetNetworkRequestsSchema = z.object({ export const emptySchema = z.object({}); // ============================================================================ -// Canvas Tool Schemas (3) +// Canvas Tool Schemas (4) // ============================================================================ export const canvasListSchema = z.object({ @@ -101,6 +101,11 @@ export const canvasCreateSchema = z.object({ name: z.string().describe('Canvas name') }); +export const canvasOpenSchema = z.object({ + canvasId: z.string().optional().describe('Canvas ID to open'), + name: z.string().optional().describe('Canvas name to open (will look up by name)') +}); + // ============================================================================ // Component Tool Schemas (6) // ============================================================================ @@ -141,6 +146,16 @@ export const componentRebuildSchema = z.object({ componentId: z.string().describe('Component ID') }); +export const componentValidateSchema = z.object({ + canvasId: z.string().optional().describe('Canvas ID (uses active canvas if not provided)') +}); + +// TODO: Feature pending - has race condition with webview initialization +// export const componentScreenshotSchema = z.object({ +// componentId: z.string().describe('Component ID to screenshot'), +// canvasId: z.string().describe('Canvas ID where the component is located') +// }); + // ============================================================================ // Project Tool Schemas (3) // ============================================================================ @@ -168,7 +183,7 @@ export const TOOL_DEFINITIONS: ToolDefinition[] = [ // ========== Browser Tools (14) ========== { name: 'browser_open', - description: 'Open the browser view. Optionally navigate to a URL.', + description: 'Open the browser view. Optionally navigate to a URL. For local project files: file:// URLs are not supported. Use project_start for Vite-based projects, or manually start a server (e.g., npx serve) and use browser_navigate with the http://localhost address.', schema: browserOpenSchema }, { @@ -183,7 +198,7 @@ export const TOOL_DEFINITIONS: ToolDefinition[] = [ }, { name: 'browser_navigate', - description: 'Navigate to a URL in the browser.', + description: 'Navigate to a URL in the browser. For local project files: file:// URLs are not supported. Use project_start for Vite-based projects, or manually start a server (e.g., npx serve) and use the http://localhost address.', schema: browserNavigateSchema }, { @@ -237,7 +252,7 @@ export const TOOL_DEFINITIONS: ToolDefinition[] = [ schema: browserGetNetworkRequestsSchema }, - // ========== Canvas Tools (3) ========== + // ========== Canvas Tools (4) ========== { name: 'canvas_list', description: 'List all canvases.', @@ -250,9 +265,14 @@ export const TOOL_DEFINITIONS: ToolDefinition[] = [ }, { name: 'canvas_create', - description: 'Create a new canvas or get existing one with same name.', + description: 'Create a new canvas or get existing one with same name. Use canvas mode for design exploration and component iteration before committing to a full project. If user asks for any design/creative/UI work (e.g., "design a landing page", "show me some ideas", "create a dashboard", "build a login form") and intent is unclear, ask if they want canvas mode (component designs side-by-side to iterate) or project mode (full running app).', schema: canvasCreateSchema }, + { + name: 'canvas_open', + description: 'Open an existing canvas by ID or name. Opens the canvas panel in the UI and returns canvas info with all components (id, name, path, status).', + schema: canvasOpenSchema + }, // ========== Component Tools (6) ========== { @@ -285,6 +305,17 @@ export const TOOL_DEFINITIONS: ToolDefinition[] = [ description: 'Trigger rebuild of a component.', schema: componentRebuildSchema }, + { + name: 'canvas_validate_components', + description: 'Validate all components in a canvas. Returns summary (total, success, failed, building counts) plus detailed error info for failed components. Efficient way to check component health without individual calls.', + schema: componentValidateSchema + }, + // TODO: Feature pending - has race condition with webview initialization + // { + // name: 'component_screenshot', + // description: 'Capture a screenshot of a specific component rendered in the canvas. Requires both componentId and canvasId. The canvas will be opened/focused if not already visible. Returns base64 data URL of the component\'s visual appearance.', + // schema: componentScreenshotSchema + // }, // ========== Project Tools (3) ========== { @@ -294,7 +325,7 @@ export const TOOL_DEFINITIONS: ToolDefinition[] = [ }, { name: 'project_start', - description: 'Start a development server for a project.', + description: 'Start a development server for a Vite-based project (React, Vue, Svelte, etc. with Vite). Use project mode when user explicitly wants a full running app (e.g., "build me a Vite React app", "create a full running project"). If user asks for design/creative/UI work and intent is unclear, ask if they want canvas mode (component iteration) or project mode (full running app) first.', schema: projectStartSchema }, { @@ -315,6 +346,44 @@ export interface PromptDefinition { } export const PROMPT_DEFINITIONS: PromptDefinition[] = [ + { + name: 'canvas-vs-project-workflow', + description: 'When to use canvas mode for component iteration vs full project mode', + content: `# Canvas vs Project: Which Workflow to Use? + +When a user asks for design/creative work (e.g., "design a landing page", "show me some UI ideas", "build a dashboard"), you must determine the right workflow. + +## Decision Rule + +**ASK the user** if their intent is unclear. Do NOT assume they want a full project. + +### When to ASK: +- User says: "design a landing page" → unclear if they want iteration or full project +- User says: "show me some ideas for a dashboard" → sounds like exploration +- User says: "help me build a login form" → could be either + +**Prompt the user with:** +"Would you like to: +1. **Canvas mode** - See a few component designs side-by-side to refine ideas first, or +2. **Project mode** - Build a full running [framework] project in the browser?" + +### When to go STRAIGHT to full project: +- User explicitly says: "build me a React website" +- User explicitly says: "create a full running project" +- User explicitly says: "scaffold a Next.js app" + +### When to use Canvas mode directly: +- User says: "show me some design variations" +- User says: "I want to experiment with different styles" +- User says: "let me see a few options first" + +## Why This Matters + +- **Canvas mode** = Fast iteration, multiple components side-by-side, quick to tweak +- **Project mode** = Full running app, routing, state management, production-like + +Canvas mode is ideal for design exploration before committing to a full project structure.` + }, { name: 'how-to-start-project', description: 'Step-by-step guide for starting a development server for an existing project', @@ -479,9 +548,152 @@ browser_get_errors → browser_get_console_logs → (fix code) → browser_reloa - **Browser Core** (6): browser_open, browser_close, browser_screenshot, browser_navigate, browser_reload, browser_action_input - **Browser Debug** (4): browser_execute_script, browser_inspect_element, browser_get_errors, browser_get_console_logs - **Browser Info** (4): browser_get_performance, browser_get_state, browser_set_viewport, browser_get_network_requests -- **Canvas Tools** (3): canvas_list, canvas_get_active, canvas_create -- **Component Tools** (6): component_add, component_add_batch, component_remove, component_get_info, component_list, component_rebuild +- **Canvas Tools** (4): canvas_list, canvas_get_active, canvas_create, canvas_open +- **Component Tools** (7): component_add, component_add_batch, component_remove, component_get_info, component_list, component_rebuild, canvas_validate_components + +## Total: 27 Tools` + }, + { + name: 'how-to-test-responsive', + description: 'Step-by-step guide for testing responsive designs across different viewport sizes', + content: `# How to Test Responsive Design in Roopik + +This workflow teaches you how to test components/pages at different screen sizes. -## Total: 26 Tools` +## Step-by-Step Process: + +### 1. Set Mobile Viewport +Call **browser_set_viewport** with mobile dimensions: +- width: 375, height: 812 (iPhone X) +- width: 390, height: 844 (iPhone 12/13/14) +- width: 360, height: 800 (Android common) +- mobile: true (enables touch emulation) + +### 2. Take Screenshot +Call **browser_screenshot** to capture mobile view. + +### 3. Test Tablet +Call **browser_set_viewport** with tablet dimensions: +- width: 768, height: 1024 (iPad) +- width: 820, height: 1180 (iPad Air) + +### 4. Test Desktop +Call **browser_set_viewport** with desktop dimensions: +- width: 1280, height: 800 (laptop) +- width: 1920, height: 1080 (full HD) + +### 5. Clear Viewport Override +Call **browser_set_viewport** with NO parameters to restore natural browser size. + +## Common Breakpoints to Test: +- 320px - Small mobile +- 375px - iPhone +- 768px - Tablet +- 1024px - Small desktop/landscape tablet +- 1280px - Laptop +- 1920px - Full HD desktop + +## Example Tool Chain: +browser_set_viewport (mobile) → browser_screenshot → browser_set_viewport (tablet) → browser_screenshot → browser_set_viewport (clear)` + }, + { + name: 'how-to-debug-network', + description: 'Step-by-step guide for inspecting API calls and network requests', + content: `# How to Debug Network Requests in Roopik + +This workflow teaches you how to inspect API calls and network activity. + +## Step-by-Step Process: + +### 1. Get API Requests Only +Call **browser_get_network_requests** with defaults: +- By default, static assets (JS/CSS/images) are hidden +- Shows only API calls (fetch, XHR) + +### 2. Filter by Status +Use statusFilter parameter: +- statusFilter: 'error' → Only failed requests (4xx, 5xx, network errors) +- statusFilter: 'success' → Only successful requests (2xx, 3xx) +- statusFilter: 'all' → Everything + +### 3. Filter by URL +Use urlFilter parameter: +- urlFilter: '/api/' → Only requests containing '/api/' +- urlFilter: 'users' → Only requests containing 'users' + +### 4. Filter by Method +Use method parameter: +- method: 'POST' → Only POST requests +- method: 'GET' → Only GET requests + +### 5. Include Static Assets +Set includeStaticAssets: true to see everything: +- JavaScript files +- CSS files +- Images +- Fonts + +## Response Data Includes: +- url: Full request URL +- method: HTTP method +- status: HTTP status code (or 'failed' for network errors) +- timing: Request duration in ms +- size: Response size in bytes + +## Example Tool Chain: +browser_get_network_requests (errors only) → browser_get_network_requests (filter by URL) → (fix API issue) → browser_reload` + }, + { + name: 'component-design-guidelines', + description: 'Professional design principles for creating high-quality UI components', + content: `# Component Design Guidelines + +When creating UI components, follow these professional design principles to avoid generic AI aesthetics. + +## Typography (CRITICAL) +**NEVER use these fonts**: Inter, Roboto, Open Sans, Lato, Montserrat, Arial, Helvetica +**USE distinctive fonts instead**: +- Headers: Clash Display, Cabinet Grotesk, Satoshi, General Sans, Switzer +- Body: Geist, Plus Jakarta Sans, DM Sans, Outfit, Manrope +- Accent: Space Grotesk, Syne, Unbounded + +Import from: https://api.fontshare.com or Google Fonts (less common ones) + +## Color & Theme +- Use CSS custom properties (--color-primary, --bg-surface, etc.) +- Pick a dominant color and use 60-30-10 rule +- Avoid pure black (#000) - use dark grays (#0a0a0a, #111) +- Add subtle color tints to grays for cohesion + +## Motion & Animation +- Use framer-motion for React, Vue Transition for Vue +- Stagger children animations (0.05-0.1s delays) +- Micro-interactions on hover/focus (scale, glow, color shift) +- Keep durations short: 150-300ms for UI, 300-500ms for reveals + +## Spatial Composition +- Break the grid intentionally - overlap elements, use negative margins +- Vary spacing rhythm - not everything needs equal padding +- Use asymmetry for visual interest +- Layer elements with z-index for depth + +## Visual Details +- Subtle gradients over flat colors +- Noise textures for depth (opacity 0.02-0.05) +- Glassmorphism: backdrop-blur + semi-transparent backgrounds +- Soft shadows with color tints, not pure black + +## Component Structure +- Mobile-first responsive design +- Single File Component (SFC) format +- Self-contained with scoped styles +- No external dependencies beyond the framework + +## Anti-Patterns to Avoid +- Generic card layouts with uniform spacing +- Blue primary buttons with white text +- Perfect symmetry everywhere +- Stock icon libraries without customization +- Cookie-cutter hero sections` } ]; From 18f53501125fd353b5021b93e6cc441fd875872a Mon Sep 17 00:00:00 2001 From: Dinesh Yadav <13635627+HumbleBee14@users.noreply.github.com> Date: Sat, 31 Jan 2026 16:58:47 -0500 Subject: [PATCH 8/8] Update .gitignore --- .gitignore | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/.gitignore b/.gitignore index 95220e2ac..cc4a59934 100644 --- a/.gitignore +++ b/.gitignore @@ -35,10 +35,8 @@ test-output.json .github/CODENOTIFY .github/commands/ .github/commands.json -/extensions/roopik-roo -/docs -/docs # Roopik IDE metadata +/docs .roopik/ -/TESTING-LAB +/resources/mcp-binaries