/* eslint-disable max-len */ // ***************************************************************************** // Copyright (C) 2024 EclipseSource GmbH. // // This program and the accompanying materials are made available under the // terms of the Eclipse Public License v. 2.0 which is available at // http://www.eclipse.org/legal/epl-2.0. // // This Source Code may also be made available under the following Secondary // Licenses when the conditions for such availability set forth in the Eclipse // Public License v. 2.0 are satisfied: GNU General Public License, version 2 // with the GNU Classpath Exception which is available at // https://www.gnu.org/software/classpath/license.html. // // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 // ***************************************************************************** import { AbstractStreamParsingChatAgent } from '@theia/ai-chat/lib/common/chat-agents'; import { ErrorChatResponseContentImpl, MarkdownChatResponseContentImpl, MutableChatRequestModel, QuestionResponseContentImpl } from '@theia/ai-chat/lib/common/chat-model'; import { LanguageModelRequirement } from '@theia/ai-core/lib/common'; import { MCPFrontendService, MCPServerDescription } from '@theia/ai-mcp/lib/common/mcp-server-manager'; import { ILogger, nls } from '@theia/core'; import { inject, injectable, named } from '@theia/core/shared/inversify'; import { MCP_SERVERS_PREF } from '@theia/ai-mcp/lib/common/mcp-preferences'; import { PreferenceScope, PreferenceService } from '@theia/core/lib/common'; import { appTesterPlaywrightTemplate, appTesterDefaultTemplate, appTesterNextTemplate, REQUIRED_MCP_SERVERS, REQUIRED_MCP_SERVERS_NEXT } from './app-tester-prompt-template'; export const AppTesterChatAgentId = 'AppTester'; @injectable() export class AppTesterChatAgent extends AbstractStreamParsingChatAgent { @inject(ILogger) @named('ai-ide:AppTesterChatAgent') protected override readonly logger: ILogger; @inject(MCPFrontendService) protected readonly mcpService: MCPFrontendService; @inject(PreferenceService) protected readonly preferenceService: PreferenceService; id: string = AppTesterChatAgentId; name = AppTesterChatAgentId; languageModelRequirements: LanguageModelRequirement[] = [{ purpose: 'chat', identifier: 'default/fast', }]; protected defaultLanguageModelPurpose: string = 'chat'; override description = nls.localize('theia/ai/chat/app-tester/description', 'This agent tests your application user interface to verify user-specified test scenarios through browser automation. ' + 'It can automate testing workflows and provide detailed feedback on application functionality.'); override iconClass: string = 'codicon codicon-beaker'; protected override systemPromptId: string = 'app-tester-system'; override prompts = [ { id: 'app-tester-system', defaultVariant: appTesterDefaultTemplate, variants: [appTesterPlaywrightTemplate, appTesterNextTemplate] } ]; /** * Override invoke to check if the specified MCP server is running, and if not, ask the user if it should be started. */ override async invoke(request: MutableChatRequestModel): Promise { const isPlaywrightVariant = this.isPlaywrightVariant(); try { if (await this.requiresStartingServers()) { request.response.response.addContent(new QuestionResponseContentImpl( isPlaywrightVariant ? nls.localize('theia/ai/ide/app-tester/startPlaywrightServers/question', 'The Playwright MCP servers are not running. Would you like to start them now? This may install the Playwright MCP servers.') : nls.localize('theia/ai/ide/app-tester/startChromeDevToolsMcpServers/question', 'The Chrome DevTools MCP server is not running. Would you like to start it now? This may install the Chrome DevTools MCP server.'), [ { text: nls.localize('theia/ai/ide/app-tester/startMcpServers/yes', 'Yes, start the servers'), value: 'yes' }, { text: nls.localize('theia/ai/ide/app-tester/startMcpServers/no', 'No, cancel'), value: 'no' } ], request, async selectedOption => { request.response.stopWaitingForInput(); if (selectedOption.value === 'yes') { const progress = request.response.addProgressMessage({ content: isPlaywrightVariant ? nls.localize('theia/ai/ide/app-tester/startPlaywrightServers/progress', 'Starting Playwright MCP servers.') : nls.localize('theia/ai/ide/app-tester/startChromeDevToolsMcpServers/progress', 'Starting Chrome DevTools MCP server.'), show: 'whileIncomplete' }); try { await this.startServers(); request.response.updateProgressMessage({ ...progress, show: 'whileIncomplete', status: 'completed' }); await super.invoke(request); } catch (error) { request.response.response.addContent(new ErrorChatResponseContentImpl( new Error(isPlaywrightVariant ? nls.localize('theia/ai/ide/app-tester/startPlaywrightServers/error', 'Failed to start Playwright MCP servers: {0}', error instanceof Error ? error.message : String(error)) : nls.localize('theia/ai/ide/app-tester/startChromeDevToolsMcpServers/error', 'Failed to start Chrome DevTools MCP server: {0}', error instanceof Error ? error.message : String(error))) )); request.response.complete(); } } else { request.response.response.addContent(new MarkdownChatResponseContentImpl( isPlaywrightVariant ? nls.localize('theia/ai/ide/app-tester/startPlaywrightServers/canceled', 'Please setup the Playwright MCP servers.') : nls.localize('theia/ai/ide/app-tester/startChromeDevToolsMcpServers/canceled', 'Please setup the Chrome DevTools MCP server.') )); request.response.complete(); } } )); request.response.waitForInput(); return; } await super.invoke(request); } catch (error) { request.response.response.addContent(new ErrorChatResponseContentImpl( isPlaywrightVariant ? new Error(nls.localize('theia/ai/ide/app-tester/errorCheckingPlaywrightServerStatus', 'Error checking Playwright MCP server status: {0}', error instanceof Error ? error.message : String(error))) : new Error(nls.localize('theia/ai/ide/app-tester/errorCheckingDevToolsServerStatus', 'Error checking DevTools MCP server status: {0}', error instanceof Error ? error.message : String(error))) )); request.response.complete(); } } protected isPlaywrightVariant(): boolean { const effectiveVariantId = this.promptService.getEffectiveVariantId(this.systemPromptId!); return effectiveVariantId === 'app-tester-system-playwright'; } protected getRequiredServers(): MCPServerDescription[] { if (this.isPlaywrightVariant()) { return REQUIRED_MCP_SERVERS; } return REQUIRED_MCP_SERVERS_NEXT; } protected async requiresStartingServers(): Promise { const allStarted = await Promise.all(this.getRequiredServers().map(server => this.mcpService.isServerStarted(server.name))); return allStarted.some(started => !started); } protected async startServers(): Promise { await this.ensureServersStarted(...this.getRequiredServers()); } /** * Starts the defined MCP server if it doesn't exist or isn't running. * * @returns A promise that resolves when the server is started */ async ensureServersStarted(...servers: MCPServerDescription[]): Promise { try { const serversToInstall: MCPServerDescription[] = []; const serversToStart: MCPServerDescription[] = []; for (const server of servers) { if (!(await this.mcpService.hasServer(server.name))) { serversToInstall.push(server); } if (!(await this.mcpService.isServerStarted(server.name))) { serversToStart.push(server); } } for (const server of serversToInstall) { const currentServers = this.preferenceService.get>(MCP_SERVERS_PREF, {}); await this.preferenceService.set(MCP_SERVERS_PREF, { ...currentServers, [server.name]: server }, PreferenceScope.User); await this.mcpService.addOrUpdateServer(server); } for (const server of serversToStart) { await this.mcpService.startServer(server.name); } } catch (error) { this.logger.error(`Error starting MCP servers ${servers.map(s => s.name)}: ${error}`); throw error; } } }