UNPKG

chrome-devtools-frontend

Version:
227 lines (197 loc) • 8.22 kB
// Copyright 2026 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 Host from '../../../core/host/host.js'; import * as i18n from '../../../core/i18n/i18n.js'; import * as SDK from '../../../core/sdk/sdk.js'; import * as TextUtils from '../../../core/text_utils/text_utils.js'; import * as Logs from '../../logs/logs.js'; import * as Trace from '../../trace/trace.js'; import type {AiWidget} from '../agents/AiAgent.js'; import {PerformanceTraceContext} from '../contexts/PerformanceTraceContext.js'; import {PerformanceInsightFormatter} from '../data_formatters/PerformanceInsightFormatter.js'; import {debugLog} from '../debug.js'; import { type BaseToolCapability, type DataHandlerResult, type DataTool, MAX_FUNCTION_RESULT_BYTE_LENGTH, type TargetCapability, type ToolArgs, ToolName, } from './Tool.js'; const lockedString = i18n.i18n.lockedString; async function getNetworkRequestImageData( target: SDK.Target.Target|null, lcpRequest: Trace.Types.Events.SyntheticNetworkRequest, // eslint-disable-next-line @devtools/no-instance-of-migrated-singletons networkLog: Logs.NetworkLog.NetworkLog = Logs.NetworkLog.NetworkLog.instance(), ): Promise<TextUtils.ContentData.ContentData|undefined> { const networkManager = target?.model(SDK.NetworkManager.NetworkManager); if (!target || !networkManager) { return undefined; } const requestId = lcpRequest.args.data.requestId; const sdkRequest = networkLog.requestByManagerAndId(networkManager, requestId); if (sdkRequest?.contentType().isImage()) { const contentData = await sdkRequest.requestContentData(); if (!TextUtils.ContentData.ContentData.isError(contentData)) { return contentData; } } return undefined; } export interface GetInsightDetailsArgs extends ToolArgs { insightSetId: string; insightName: string; } export class GetInsightDetailsTool implements DataTool<GetInsightDetailsArgs, string, BaseToolCapability&TargetCapability> { readonly name = ToolName.GET_INSIGHT_DETAILS; readonly description = 'Returns detailed information about a specific insight of an insight set. Use this before commenting on any specific issue to get more information.'; readonly parameters: Host.AidaClient.FunctionObjectParam<keyof GetInsightDetailsArgs> = { type: Host.AidaClient.ParametersTypes.OBJECT, description: 'Arguments for getting insight details.', nullable: false, properties: { insightSetId: { type: Host.AidaClient.ParametersTypes.STRING, description: 'The id for the specific insight set. Only use the ids given in the "Available insight sets" list.', nullable: false, }, insightName: { type: Host.AidaClient.ParametersTypes.STRING, description: 'The name of the insight. Only use the insight names given in the "Available insights" list.', nullable: false, }, }, required: ['insightSetId', 'insightName'], }; displayInfoFromArgs(params: GetInsightDetailsArgs): { title: string, action: string, } { return { title: lockedString(`Investigating insight ${params.insightName}`), action: `getInsightDetails('${params.insightSetId}', '${params.insightName}')`, }; } async #generateDOMTreeWidget( insight: Trace.Insights.Types.InsightModel, insightSet: Trace.Insights.Types.InsightSet, target: SDK.Target.Target|null, ): Promise<AiWidget|null> { try { if (!Trace.Insights.Models.LCPDiscovery.isLCPDiscoveryInsight(insight) && !Trace.Insights.Models.LCPBreakdown.isLCPBreakdownInsight(insight)) { return null; } const lcpMetric = Trace.Insights.Common.getLCP(insightSet); const lcpEvent = lcpMetric?.event; if (!lcpEvent || !Trace.Types.Events.isAnyLargestContentfulPaintCandidate(lcpEvent)) { return null; } const nodeId = lcpEvent.args.data?.nodeId; if (!nodeId) { return null; } const domModel = target?.model(SDK.DOMModel.DOMModel); if (!domModel) { return null; } const nodeMap = await domModel.pushNodesByBackendIdsToFrontend(new Set([nodeId])); const node = nodeMap?.get(nodeId); if (!node) { return null; } const lcpSyntheticRequest = insight.lcpRequest; const [snapshot, imageContent] = await Promise.all([ node.takeSnapshot(), lcpSyntheticRequest ? getNetworkRequestImageData(target, lcpSyntheticRequest) : Promise.resolve(undefined), ]); let networkRequest; if (lcpSyntheticRequest) { networkRequest = { url: lcpSyntheticRequest.args.data.url, size: lcpSyntheticRequest.args.data.decodedBodyLength ?? lcpSyntheticRequest.args.data.encodedDataLength ?? 0, resourceType: lcpSyntheticRequest.args.data.resourceType, mimeType: lcpSyntheticRequest.args.data.mimeType ?? '', imageContent, }; } return { name: 'DOM_TREE', data: { root: snapshot, networkRequest, title: lockedString('LCP element'), accessibleRevealLabel: lockedString('Reveal LCP element'), }, }; } catch (err) { debugLog('GetInsightDetails: Failed to generate DOM tree widget', err); return null; } } async handler( params: GetInsightDetailsArgs, capabilities: BaseToolCapability&TargetCapability, ): Promise<DataHandlerResult<string>> { const conversationContext = capabilities.conversationContext; if (!conversationContext || !(conversationContext instanceof PerformanceTraceContext)) { return {error: 'Performance trace context is not available.'}; } if (!params.insightSetId || !params.insightName) { return {error: 'Missing required arguments: insightSetId and insightName must be provided.'}; } const focus = conversationContext.getItem(); const parsedTrace = focus.parsedTrace; const insightSet = parsedTrace.insights?.get(params.insightSetId); if (!insightSet) { const formatter = conversationContext.createFormatter(); const valid = ([...parsedTrace.insights?.values() ?? []]) .map(insightSet => `id: ${insightSet.id}, url: ${insightSet.url}, bounds: ${ formatter.serializeBounds(insightSet.bounds)}`) .join('; '); return {error: `Invalid insight set id. Valid insight set ids are: ${valid || '(none)'}`}; } if (!Trace.Insights.Common.isInsightKey(params.insightName)) { const valid = Object.keys(insightSet.model).join(', '); return {error: `No insight available. Valid insight names are: ${valid || '(none)'}`}; } const insightError = insightSet.modelErrors?.[params.insightName]; if (insightError) { return {error: `Insight "${params.insightName}" failed during trace processing: ${insightError.message}`}; } const insight = insightSet.model[params.insightName]; if (!insight) { const valid = Object.keys(insightSet.model).join(', '); return {error: `No insight available. Valid insight names are: ${valid || '(none)'}`}; } const details = new PerformanceInsightFormatter(focus, insight).formatInsight(); if (details.length > MAX_FUNCTION_RESULT_BYTE_LENGTH) { return { error: 'The insight details output is too large to fit in the context window. Please inspect specific events using getTraceEventByKey or getDetailedCallTree.', }; } const widgets: AiWidget[] = []; const isImportedTrace = conversationContext.getOrigin().startsWith('imported-trace://'); if (!isImportedTrace) { const domTreeWidget = await this.#generateDOMTreeWidget(insight, insightSet, capabilities.getTarget()); if (domTreeWidget) { widgets.push(domTreeWidget); } } widgets.push({ name: 'PERF_INSIGHT', data: { insight: params.insightName, insightData: insight, }, }); return {result: details, widgets}; } }