UNPKG

chrome-devtools-frontend

Version:
355 lines (308 loc) • 11.7 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 Host from '../../core/host/host.js'; import * as Root from '../../core/root/root.js'; import {debugLog} from './debug.js'; /** * TODO(b/404796739): Remove these definitions of AgentOptions and RequestOptions and * use the existing ones which are used for AI assistance panel agents. **/ interface AgentOptions { aidaClient: Host.AidaClient.AidaClient; serverSideLoggingEnabled?: boolean; confirmSideEffectForTest?: typeof Promise.withResolvers; } interface RequestOptions { temperature?: number; modelId?: string; } interface CachedRequest { request: Host.AidaClient.CompletionRequest; response: Host.AidaClient.CompletionResponse; } export interface Callbacks { getSelectionHead: () => number; getCompletionHint: () => string | undefined | null; setAiAutoCompletion: (args: { text: string, from: number, startTime: number, onImpression: (rpcGlobalId: Host.AidaClient.RpcGlobalId, latency: number, sampleId?: number) => void, clearCachedRequest: () => void, rpcGlobalId?: Host.AidaClient.RpcGlobalId, sampleId?: number, }|null) => void; } /* clang-format off */ export const consoleAdditionalContextFileContent = `/** * This file describes the execution environment of the Chrome DevTools Console. * The code is JavaScript, but with special global functions and variables. * Top-level await is available. * The console has direct access to the inspected page's \`window\` and \`document\`. */ /** * @description Returns the value of the most recently evaluated expression. */ let $_; /** * @description A reference to the most recently selected DOM element. * $0, $1, $2, $3, $4 can be used to reference the last five selected DOM elements. */ let $0; /** * @description A query selector alias. $$('.my-class') is equivalent to document.querySelectorAll('.my-class'). */ function $$(selector, startNode) {} /** * @description An XPath selector. $x('//p') returns an array of all <p> elements. */ function $x(path, startNode) {} function clear() {} function copy(object) {} /** * @description Selects and reveals the specified element in the Elements panel. */ function inspect(object) {} function keys(object) {} function values(object) {} /** * @description When the specified function is called, the debugger is invoked. */ function debug(func) {} /** * @description Stops the debugging of the specified function. */ function undebug(func) {} /** * @description Logs a message to the console whenever the specified function is called, * along with the arguments passed to it. */ function monitor(func) {} /** * @description Stops monitoring the specified function. */ function unmonitor(func) {} /** * @description Logs all events dispatched to the specified object to the console. */ function monitorEvents(object, events) {} /** * @description Returns an object containing all event listeners registered on the specified object. */ function getEventListeners(object) {} /** * The global \`console\` object has several helpful methods */ const console = { log: (...args) => {}, warn: (...args) => {}, error: (...args) => {}, info: (...args) => {}, debug: (...args) => {}, assert: (assertion, ...args) => {}, dir: (object) => {}, // Displays an interactive property listing of an object. dirxml: (object) => {}, // Displays an XML/HTML representation of an object. table: (data, columns) => {}, // Displays tabular data as a table. group: (label) => {}, // Creates a new inline collapsible group. groupEnd: () => {}, time: (label) => {}, // Starts a timer. timeEnd: (label) => {} // Stops a timer and logs the elapsed time. };`; /* clang-format on */ /** * The AiCodeCompletion class is responsible for fetching code completion suggestions * from the AIDA backend. */ export class AiCodeCompletion { #stopSequences: string[]; #renderingTimeout?: number; #aidaRequestCache?: CachedRequest; // TODO(b/445394511): Remove panel from the class #panel: ContextFlavor; #callbacks?: Callbacks; readonly #sessionId: string = crypto.randomUUID(); readonly #aidaClient: Host.AidaClient.AidaClient; readonly #serverSideLoggingEnabled: boolean; constructor(opts: AgentOptions, panel: ContextFlavor, callbacks?: Callbacks, stopSequences?: string[]) { this.#aidaClient = opts.aidaClient; this.#serverSideLoggingEnabled = opts.serverSideLoggingEnabled ?? false; this.#panel = panel; this.#stopSequences = stopSequences ?? []; this.#callbacks = callbacks; } #buildRequest( prefix: string, suffix: string, inferenceLanguage: Host.AidaClient.AidaInferenceLanguage = Host.AidaClient.AidaInferenceLanguage.JAVASCRIPT, additionalFiles?: Host.AidaClient.AdditionalFile[]): Host.AidaClient.CompletionRequest { const userTier = Host.AidaClient.convertToUserTierEnum(this.#userTier); function validTemperature(temperature: number|undefined): number|undefined { return typeof temperature === 'number' && temperature >= 0 ? temperature : undefined; } // As a temporary fix for b/441221870 we are prepending a newline for each prefix. prefix = '\n' + prefix; let additionalContextFiles = additionalFiles ?? undefined; if (!additionalContextFiles) { additionalContextFiles = this.#panel === ContextFlavor.CONSOLE ? [{ path: 'devtools-console-context.js', content: consoleAdditionalContextFileContent, included_reason: Host.AidaClient.Reason.RELATED_FILE, }] : undefined; } return { client: Host.AidaClient.CLIENT_NAME, prefix, suffix, options: { inference_language: inferenceLanguage, temperature: validTemperature(this.#options.temperature), model_id: this.#options.modelId || undefined, stop_sequences: this.#stopSequences, }, metadata: { disable_user_content_logging: !(this.#serverSideLoggingEnabled ?? false), string_session_id: this.#sessionId, user_tier: userTier, client_version: Root.Runtime.getChromeVersion(), }, additional_files: additionalContextFiles, }; } async #completeCodeCached(request: Host.AidaClient.CompletionRequest): Promise<{ response: Host.AidaClient.CompletionResponse | null, fromCache: boolean, }> { const cachedResponse = this.#checkCachedRequestForResponse(request); if (cachedResponse) { return {response: cachedResponse, fromCache: true}; } const response = await this.#aidaClient.completeCode(request); if (!response) { return { response: null, fromCache: false, }; } this.#updateCachedRequest(request, response); return { response, fromCache: false, }; } get #userTier(): string|undefined { return Root.Runtime.hostConfig.devToolsAiCodeCompletion?.userTier; } get #options(): RequestOptions { const temperature = Root.Runtime.hostConfig.devToolsAiCodeCompletion?.temperature; const modelId = Root.Runtime.hostConfig.devToolsAiCodeCompletion?.modelId; return { temperature, modelId, }; } #checkCachedRequestForResponse(request: Host.AidaClient.CompletionRequest): Host.AidaClient.CompletionResponse|null { if (!this.#aidaRequestCache || this.#aidaRequestCache.request.suffix !== request.suffix || JSON.stringify(this.#aidaRequestCache.request.options) !== JSON.stringify(request.options)) { return null; } const possibleGeneratedSamples: Host.AidaClient.GenerationSample[] = []; for (const sample of this.#aidaRequestCache.response.generatedSamples) { const prefixWithSample = this.#aidaRequestCache.request.prefix + sample.generationString; if (prefixWithSample.startsWith(request.prefix)) { possibleGeneratedSamples.push({ generationString: prefixWithSample.substring(request.prefix.length), sampleId: sample.sampleId, score: sample.score, attributionMetadata: sample.attributionMetadata, }); } } if (possibleGeneratedSamples.length === 0) { return null; } return {generatedSamples: possibleGeneratedSamples, metadata: this.#aidaRequestCache.response.metadata}; } #updateCachedRequest(request: Host.AidaClient.CompletionRequest, response: Host.AidaClient.CompletionResponse): void { this.#aidaRequestCache = {request, response}; } registerUserImpression(rpcGlobalId: Host.AidaClient.RpcGlobalId, latency: number, sampleId?: number): void { const seconds = Math.floor(latency / 1_000); const remainingMs = latency % 1_000; const nanos = Math.floor(remainingMs * 1_000_000); void this.#aidaClient.registerClientEvent({ corresponding_aida_rpc_global_id: rpcGlobalId, disable_user_content_logging: true, complete_code_client_event: { user_impression: { sample: { sample_id: sampleId, }, latency: { duration: { seconds, nanos, }, } }, }, }); debugLog('Registered user impression with latency {seconds:', seconds, ', nanos:', nanos, '}'); Host.userMetrics.actionTaken(Host.UserMetrics.Action.AiCodeCompletionSuggestionDisplayed); } registerUserAcceptance(rpcGlobalId: Host.AidaClient.RpcGlobalId, sampleId?: number): void { void this.#aidaClient.registerClientEvent({ corresponding_aida_rpc_global_id: rpcGlobalId, disable_user_content_logging: true, complete_code_client_event: { user_acceptance: { sample: { sample_id: sampleId, } }, }, }); debugLog('Registered user acceptance'); Host.userMetrics.actionTaken(Host.UserMetrics.Action.AiCodeCompletionSuggestionAccepted); } clearCachedRequest(): void { this.#aidaRequestCache = undefined; } async completeCode( prefix: string, suffix: string, cursorPositionAtRequest: number, inferenceLanguage?: Host.AidaClient.AidaInferenceLanguage, additionalFiles?: Host.AidaClient.AdditionalFile[]): Promise<{ response: Host.AidaClient.CompletionResponse | null, fromCache: boolean, }> { const request = this.#buildRequest(prefix, suffix, inferenceLanguage, additionalFiles); const {response, fromCache} = await this.#completeCodeCached(request); debugLog('At cursor position', cursorPositionAtRequest, {request, response, fromCache}); if (!response) { return {response: null, fromCache: false}; } return {response, fromCache}; } remove(): void { if (this.#renderingTimeout) { clearTimeout(this.#renderingTimeout); this.#renderingTimeout = undefined; } this.#callbacks?.setAiAutoCompletion(null); } static isAiCodeCompletionEnabled(locale: string): boolean { if (!locale.startsWith('en-')) { return false; } const aidaAvailability = Root.Runtime.hostConfig.aidaAvailability; if (!aidaAvailability || aidaAvailability.blockedByGeo || aidaAvailability.blockedByAge || aidaAvailability.blockedByEnterprisePolicy) { return false; } return Boolean(aidaAvailability.enabled && Root.Runtime.hostConfig.devToolsAiCodeCompletion?.enabled); } } export const enum ContextFlavor { CONSOLE = 'console', // generated code can contain console specific APIs like `$0`. SOURCES = 'sources', }