UNPKG

chrome-devtools-frontend

Version:
321 lines (284 loc) • 12 kB
// Copyright 2025 The Chromium Authors // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. import * as Common from '../../core/common/common.js'; import * as Host from '../../core/host/host.js'; import * as i18n from '../../core/i18n/i18n.js'; import * as Platform from '../../core/platform/platform.js'; import * as Root from '../../core/root/root.js'; import * as SDK from '../../core/sdk/sdk.js'; import * as NetworkTimeCalculator from '../network_time_calculator/network_time_calculator.js'; import { type AiAgent, type ExternalRequestResponse, ExternalRequestResponseType, ResponseType } from './agents/AiAgent.js'; import {NetworkAgent, RequestContext} from './agents/NetworkAgent.js'; import type {PerformanceTraceContext} from './agents/PerformanceAgent.js'; import {NodeContext, StylingAgent} from './agents/StylingAgent.js'; import {AiConversation} from './AiConversation.js'; import { ConversationType, } from './AiHistoryStorage.js'; import {getDisabledReasons} from './AiUtils.js'; interface ExternalStylingRequestParameters { conversationType: ConversationType.STYLING; prompt: string; selector?: string; } interface ExternalNetworkRequestParameters { conversationType: ConversationType.NETWORK; prompt: string; requestUrl: string; } export interface ExternalPerformanceAIConversationData { conversationHandler: ConversationHandler; conversation: AiConversation; selected: PerformanceTraceContext; } export interface ExternalPerformanceRequestParameters { conversationType: ConversationType.PERFORMANCE; prompt: string; data: ExternalPerformanceAIConversationData; } /* * Strings that don't need to be translated at this time. */ const UIStringsNotTranslate = { /** * @description Error message shown when AI assistance is not enabled in DevTools settings. */ enableInSettings: 'For AI features to be available, you need to enable AI assistance in DevTools settings.', } as const; const lockedString = i18n.i18n.lockedString; function isAiAssistanceServerSideLoggingEnabled(): boolean { return !Root.Runtime.hostConfig.aidaAvailability?.disallowLogging; } async function inspectElementBySelector(selector: string): Promise<SDK.DOMModel.DOMNode|null> { const whitespaceTrimmedQuery = selector.trim(); if (!whitespaceTrimmedQuery.length) { return null; } const showUAShadowDOM = Common.Settings.Settings.instance().moduleSetting('show-ua-shadow-dom').get(); const domModels = SDK.TargetManager.TargetManager.instance().models(SDK.DOMModel.DOMModel, {scoped: true}); const performSearchPromises = domModels.map(domModel => domModel.performSearch(whitespaceTrimmedQuery, showUAShadowDOM)); const resultCounts = await Promise.all(performSearchPromises); // If the selector matches multiple times, this returns the first match. const index = resultCounts.findIndex(value => value > 0); if (index >= 0) { return await domModels[index].searchResult(0); } return null; } async function inspectNetworkRequestByUrl(selector: string): Promise<SDK.NetworkRequest.NetworkRequest|null> { const networkManagers = SDK.TargetManager.TargetManager.instance().models(SDK.NetworkManager.NetworkManager, {scoped: true}); const results = networkManagers .map(networkManager => { let request = networkManager.requestForURL(Platform.DevToolsPath.urlString`${selector}`); if (!request && selector.at(-1) === '/') { request = networkManager.requestForURL(Platform.DevToolsPath.urlString`${selector.slice(0, -1)}`); } else if (!request && selector.at(-1) !== '/') { request = networkManager.requestForURL(Platform.DevToolsPath.urlString`${selector}/`); } return request; }) .filter(req => !!req); const request = results.at(0); return request ?? null; } let conversationHandlerInstance: ConversationHandler|undefined; export class ConversationHandler extends Common.ObjectWrapper.ObjectWrapper<EventTypes> { #aiAssistanceEnabledSetting: Common.Settings.Setting<boolean>|undefined; #aidaClient: Host.AidaClient.AidaClient; #aidaAvailability?: Host.AidaClient.AidaAccessPreconditions; private constructor( aidaClient: Host.AidaClient.AidaClient, aidaAvailability?: Host.AidaClient.AidaAccessPreconditions) { super(); this.#aidaClient = aidaClient; this.#aidaAvailability = aidaAvailability; this.#aiAssistanceEnabledSetting = this.#getAiAssistanceEnabledSetting(); } static instance(opts?: { aidaClient?: Host.AidaClient.AidaClient, aidaAvailability?: Host.AidaClient.AidaAccessPreconditions, forceNew?: boolean, }): ConversationHandler { if (opts?.forceNew || conversationHandlerInstance === undefined) { const aidaClient = opts?.aidaClient ?? new Host.AidaClient.AidaClient(); conversationHandlerInstance = new ConversationHandler(aidaClient, opts?.aidaAvailability ?? undefined); } return conversationHandlerInstance; } static removeInstance(): void { conversationHandlerInstance = undefined; } get aidaClient(): Host.AidaClient.AidaClient { return this.#aidaClient; } #getAiAssistanceEnabledSetting(): Common.Settings.Setting<boolean>|undefined { try { return Common.Settings.moduleSetting('ai-assistance-enabled') as Common.Settings.Setting<boolean>; } catch { return; } } async #getDisabledReasons(): Promise<Platform.UIString.LocalizedString[]> { if (this.#aidaAvailability === undefined) { this.#aidaAvailability = await Host.AidaClient.AidaClient.checkAccessPreconditions(); } return getDisabledReasons(this.#aidaAvailability); } // eslint-disable-next-line require-yield async * #generateErrorResponse(message: string): AsyncGenerator<ExternalRequestResponse, ExternalRequestResponse> { return { type: ExternalRequestResponseType.ERROR, message, }; } /** * Handles an external request using the given prompt and uses the * conversation type to use the correct agent. */ async handleExternalRequest( parameters: ExternalStylingRequestParameters|ExternalNetworkRequestParameters| ExternalPerformanceRequestParameters, ): Promise<AsyncGenerator<ExternalRequestResponse, ExternalRequestResponse>> { try { this.dispatchEventToListeners(ConversationHandlerEvents.EXTERNAL_REQUEST_RECEIVED); const disabledReasons = await this.#getDisabledReasons(); const aiAssistanceSetting = this.#aiAssistanceEnabledSetting?.getIfNotDisabled(); if (!aiAssistanceSetting) { disabledReasons.push(lockedString(UIStringsNotTranslate.enableInSettings)); } if (disabledReasons.length > 0) { return this.#generateErrorResponse(disabledReasons.join(' ')); } this.dispatchEventToListeners( ConversationHandlerEvents.EXTERNAL_CONVERSATION_STARTED, parameters.conversationType); switch (parameters.conversationType) { case ConversationType.STYLING: { return await this.#handleExternalStylingConversation(parameters.prompt, parameters.selector); } case ConversationType.PERFORMANCE: return await this.#handleExternalPerformanceConversation(parameters.prompt, parameters.data); case ConversationType.NETWORK: if (!parameters.requestUrl) { return this.#generateErrorResponse('The url is required for debugging a network request.'); } return await this.#handleExternalNetworkConversation(parameters.prompt, parameters.requestUrl); } } catch (error) { return this.#generateErrorResponse(error.message); } } async * #createAndDoExternalConversation(opts: { conversationType: ConversationType, aiAgent: AiAgent<unknown>, prompt: string, selected: NodeContext|PerformanceTraceContext|RequestContext|null, }): AsyncGenerator<ExternalRequestResponse, ExternalRequestResponse> { const {conversationType, aiAgent, prompt, selected} = opts; const conversation = new AiConversation( conversationType, [], aiAgent.sessionId, /* isReadOnly */ true, this.#aidaClient, undefined, /* isExternal */ true, ); return yield* this.#doExternalConversation({conversation, prompt, selected}); } async * #doExternalConversation(opts: { conversation: AiConversation, prompt: string, selected: NodeContext|PerformanceTraceContext|RequestContext|null, }): AsyncGenerator<ExternalRequestResponse, ExternalRequestResponse> { const {conversation, prompt, selected} = opts; conversation.setContext(selected); const generator = conversation.run(prompt); const devToolsLogs: object[] = []; for await (const data of generator) { if (data.type !== ResponseType.ANSWER || data.complete) { devToolsLogs.push(data); } if (data.type === ResponseType.CONTEXT || data.type === ResponseType.TITLE) { yield { type: ExternalRequestResponseType.NOTIFICATION, message: data.title, }; } if (data.type === ResponseType.SIDE_EFFECT) { data.confirm(true); } if (data.type === ResponseType.ANSWER && data.complete) { return { type: ExternalRequestResponseType.ANSWER, message: data.text, devToolsLogs, }; } } return { type: ExternalRequestResponseType.ERROR, message: 'Something went wrong. No answer was generated.', }; } async #handleExternalStylingConversation(prompt: string, selector = 'body'): Promise<AsyncGenerator<ExternalRequestResponse, ExternalRequestResponse>> { const stylingAgent = new StylingAgent({ aidaClient: this.#aidaClient, serverSideLoggingEnabled: isAiAssistanceServerSideLoggingEnabled(), }); const node = await inspectElementBySelector(selector); if (node) { await node.setAsInspectedNode(); } const selected = node ? new NodeContext(node) : null; return this.#createAndDoExternalConversation({ conversationType: ConversationType.STYLING, aiAgent: stylingAgent, prompt, selected, }); } async #handleExternalPerformanceConversation(prompt: string, data: ExternalPerformanceAIConversationData): Promise<AsyncGenerator<ExternalRequestResponse, ExternalRequestResponse>> { return this.#doExternalConversation({ conversation: data.conversation, prompt, selected: data.selected, }); } async #handleExternalNetworkConversation(prompt: string, requestUrl: string): Promise<AsyncGenerator<ExternalRequestResponse, ExternalRequestResponse>> { const networkAgent = new NetworkAgent({ aidaClient: this.#aidaClient, serverSideLoggingEnabled: isAiAssistanceServerSideLoggingEnabled(), }); const request = await inspectNetworkRequestByUrl(requestUrl); if (!request) { return this.#generateErrorResponse(`Can't find request with the given selector ${requestUrl}`); } const calculator = new NetworkTimeCalculator.NetworkTransferTimeCalculator(); calculator.updateBoundaries(request); return this.#createAndDoExternalConversation({ conversationType: ConversationType.NETWORK, aiAgent: networkAgent, prompt, selected: new RequestContext(request, calculator), }); } } export const enum ConversationHandlerEvents { EXTERNAL_REQUEST_RECEIVED = 'ExternalRequestReceived', EXTERNAL_CONVERSATION_STARTED = 'ExternalConversationStarted', } export interface EventTypes { [ConversationHandlerEvents.EXTERNAL_REQUEST_RECEIVED]: void; [ConversationHandlerEvents.EXTERNAL_CONVERSATION_STARTED]: ConversationType; }