Skip to content

Commit f897a19

Browse files
Samiya CaurDevtools-frontend LUCI CQ
authored andcommitted
Add a summary toolbar for AI code completion to the bottom of console
Also added an event, which Console View can listen to, and update the citations. Pending: - Add a spinner to track progress - ARIA labels - (Non-blocking for this feature): A way to differentiate events triggered due to code completion in Console and Sources Bug: 433884684 Change-Id: I6c6fec115bf498bd0f6c912d9d2c4622d456fe0a Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/6778986 Commit-Queue: Samiya Caur <[email protected]> Reviewed-by: Wolfgang Beyer <[email protected]>
1 parent 2144d7e commit f897a19

File tree

10 files changed

+444
-3
lines changed

10 files changed

+444
-3
lines changed

config/gni/devtools_grd_files.gni

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1373,8 +1373,10 @@ grd_files_unbundled_sources = [
13731373
"front_end/panels/changes/changesSidebar.css.js",
13741374
"front_end/panels/changes/changesView.css.js",
13751375
"front_end/panels/changes/combinedDiffView.css.js",
1376+
"front_end/panels/common/AiCodeCompletionSummaryToolbar.js",
13761377
"front_end/panels/common/AiCodeCompletionTeaser.js",
13771378
"front_end/panels/common/FreDialog.js",
1379+
"front_end/panels/common/aiCodeCompletionSummaryToolbar.css.js",
13781380
"front_end/panels/common/aiCodeCompletionTeaser.css.js",
13791381
"front_end/panels/common/common.css.js",
13801382
"front_end/panels/common/freDialog.css.js",

front_end/models/ai_code_completion/AiCodeCompletion.ts

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,13 @@
22
// Use of this source code is governed by a BSD-style license that can be
33
// found in the LICENSE file.
44

5-
import type * as Common from '../../core/common/common.js';
5+
import * as Common from '../../core/common/common.js';
66
import * as Host from '../../core/host/host.js';
77
import * as Root from '../../core/root/root.js';
88
import * as TextEditor from '../../ui/components/text_editor/text_editor.js';
99
import type {AgentOptions, RequestOptions} from '../ai_assistance/ai_assistance.js';
1010

11-
export class AiCodeCompletion {
11+
export class AiCodeCompletion extends Common.ObjectWrapper.ObjectWrapper<EventTypes> {
1212
#aidaRequestThrottler: Common.Throttler.Throttler;
1313
#editor: TextEditor.TextEditor.TextEditor;
1414

@@ -17,6 +17,7 @@ export class AiCodeCompletion {
1717
readonly #serverSideLoggingEnabled: boolean;
1818

1919
constructor(opts: AgentOptions, editor: TextEditor.TextEditor.TextEditor, throttler: Common.Throttler.Throttler) {
20+
super();
2021
this.#aidaClient = opts.aidaClient;
2122
this.#serverSideLoggingEnabled = opts.serverSideLoggingEnabled ?? false;
2223
this.#editor = editor;
@@ -56,6 +57,10 @@ export class AiCodeCompletion {
5657
this.#editor.dispatch({
5758
effects: TextEditor.Config.setAiAutoCompleteSuggestion.of(response.generatedSamples[0].generationString),
5859
});
60+
const citations = response.generatedSamples[0].attributionMetadata?.citations;
61+
if (citations) {
62+
this.dispatchEventToListeners(Events.CITATIONS_UPDATED, {citations});
63+
}
5964
}
6065
}
6166

@@ -77,3 +82,15 @@ export class AiCodeCompletion {
7782
void this.#aidaRequestThrottler.schedule(() => this.#requestAidaSuggestion(this.#buildRequest(prefix, suffix)));
7883
}
7984
}
85+
86+
export const enum Events {
87+
CITATIONS_UPDATED = 'CitationsUpdated',
88+
}
89+
90+
export interface CitationsUpdatedEvent {
91+
citations: Host.AidaClient.Citation[];
92+
}
93+
94+
export interface EventTypes {
95+
[Events.CITATIONS_UPDATED]: CitationsUpdatedEvent;
96+
}
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
// Copyright 2025 The Chromium Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
import * as Root from '../../core/root/root.js';
6+
import {renderElementIntoDOM} from '../../testing/DOMHelpers.js';
7+
import {describeWithEnvironment, updateHostConfig} from '../../testing/EnvironmentHelpers.js';
8+
import {createViewFunctionStub} from '../../testing/ViewFunctionHelpers.js';
9+
import * as UI from '../../ui/legacy/legacy.js';
10+
11+
import * as Common from './common.js';
12+
13+
describeWithEnvironment('AiCodeCompletionSummaryToolbar', () => {
14+
async function createToolbar(panelName = 'console') {
15+
const view = createViewFunctionStub(Common.AiCodeCompletionSummaryToolbar);
16+
const widget =
17+
new Common.AiCodeCompletionSummaryToolbar('disclaimer-tooltip', 'citations-tooltip', panelName, view);
18+
widget.markAsRoot();
19+
renderElementIntoDOM(widget);
20+
await view.nextInput;
21+
return {view, widget};
22+
}
23+
24+
afterEach(() => {
25+
sinon.restore();
26+
});
27+
28+
it('should show disclaimer with no logging text when enterprise policy value is ALLOW_WITHOUT_LOGGING', async () => {
29+
updateHostConfig({
30+
aidaAvailability: {enterprisePolicyValue: Root.Runtime.GenAiEnterprisePolicyValue.ALLOW_WITHOUT_LOGGING},
31+
});
32+
33+
const {view, widget} = await createToolbar();
34+
35+
assert.isTrue(view.input.noLogging);
36+
widget.detach();
37+
});
38+
39+
it('should show disclaimer without no logging text when enterprise policy value is ALLOW', async () => {
40+
updateHostConfig({aidaAvailability: {enterprisePolicyValue: Root.Runtime.GenAiEnterprisePolicyValue.ALLOW}});
41+
42+
const {view, widget} = await createToolbar();
43+
44+
assert.isFalse(view.input.noLogging);
45+
widget.detach();
46+
});
47+
48+
it('should open settings on manage in settings tooltip click', async () => {
49+
const {view, widget} = await createToolbar();
50+
const showViewStub = sinon.stub(UI.ViewManager.ViewManager.instance(), 'showView');
51+
52+
view.input.onManageInSettingsTooltipClick();
53+
54+
assert.isTrue(showViewStub.calledOnceWith('chrome-ai'));
55+
widget.detach();
56+
});
57+
58+
it('should update citations', async () => {
59+
const {view, widget} = await createToolbar();
60+
61+
assert.deepEqual(view.input.citations, []);
62+
63+
widget.updateCitations(['https://example.com/1']);
64+
await view.nextInput;
65+
66+
assert.deepEqual(view.input.citations, ['https://example.com/1']);
67+
68+
widget.updateCitations(['https://example.com/2']);
69+
await view.nextInput;
70+
71+
assert.deepEqual(view.input.citations, ['https://example.com/1', 'https://example.com/2']);
72+
73+
widget.detach();
74+
});
75+
76+
it('should not add duplicate citations', async () => {
77+
const {view, widget} = await createToolbar();
78+
79+
assert.deepEqual(view.input.citations, []);
80+
81+
widget.updateCitations(['https://example.com/1']);
82+
await view.nextInput;
83+
84+
assert.deepEqual(view.input.citations, ['https://example.com/1']);
85+
86+
widget.updateCitations(['https://example.com/1']);
87+
await view.nextInput;
88+
89+
assert.deepEqual(view.input.citations, ['https://example.com/1']);
90+
91+
widget.detach();
92+
});
93+
94+
it('should clear citations', async () => {
95+
const {view, widget} = await createToolbar();
96+
97+
widget.updateCitations(['https://example.com/1']);
98+
await view.nextInput;
99+
100+
assert.deepEqual(view.input.citations, ['https://example.com/1']);
101+
102+
widget.clearCitations();
103+
await view.nextInput;
104+
105+
assert.deepEqual(view.input.citations, []);
106+
107+
widget.detach();
108+
});
109+
});
Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
// Copyright 2025 The Chromium Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
import '../../ui/components/tooltips/tooltips.js';
6+
7+
import * as i18n from '../../core/i18n/i18n.js';
8+
import * as Root from '../../core/root/root.js';
9+
import * as UI from '../../ui/legacy/legacy.js';
10+
import {Directives, html, nothing, render} from '../../ui/lit/lit.js';
11+
import * as VisualLogging from '../../ui/visual_logging/visual_logging.js';
12+
13+
import styles from './aiCodeCompletionSummaryToolbar.css.js';
14+
15+
const UIStrings = {
16+
/**
17+
*@description Disclaimer text for AI code completion
18+
*/
19+
relevantData: 'Relevant data',
20+
/**
21+
* @description Disclaimer text for AI code completion
22+
*/
23+
isSentToGoogle: 'is sent to Google',
24+
/**
25+
*@description Text for tooltip shown on hovering over "Relevant Data" in the disclaimer text for AI code completion.
26+
*/
27+
tooltipDisclaimerTextForAiCodeCompletion:
28+
'To generate code suggestions, your console input and the history of your current console session are shared with Google. This data may be seen by human reviewers to improve this feature.',
29+
/**
30+
*@description Text for tooltip shown on hovering over "Relevant Data" in the disclaimer text for AI code completion.
31+
*/
32+
tooltipDisclaimerTextForAiCodeCompletionNoLogging:
33+
'To generate code suggestions, your console input and the history of your current console session are shared with Google. This data will not be used to improve Google’s AI models.',
34+
/**
35+
*@description Text for tooltip button which redirects to AI settings
36+
*/
37+
manageInSettings: 'Manage in settings',
38+
/**
39+
*@description Text for recitation notice
40+
*/
41+
generatedCodeMayBeSubjectToALicense: 'Generated code may be subject to a license.',
42+
/**
43+
*@description Text for citations
44+
*/
45+
viewSources: 'View Sources',
46+
} as const;
47+
48+
const lockedString = i18n.i18n.lockedString;
49+
50+
export interface ViewInput {
51+
disclaimerTooltipId: string;
52+
panelName: string;
53+
citations?: string[];
54+
citationsTooltipId: string;
55+
noLogging: boolean;
56+
onManageInSettingsTooltipClick: () => void;
57+
}
58+
59+
export interface ViewOutput {
60+
tooltipRef?: Directives.Ref<HTMLElement>;
61+
}
62+
63+
export type View = (input: ViewInput, output: ViewOutput, target: HTMLElement) => void;
64+
65+
export const DEFAULT_SUMMARY_TOOLBAR_VIEW: View = (input, output, target) => {
66+
output.tooltipRef = output.tooltipRef ?? Directives.createRef<HTMLElement>();
67+
68+
// clang-format off
69+
const viewSourcesSpan = input.citations && input.citations.length > 0 ?
70+
html`<span class="link" role="link" aria-details=${input.citationsTooltipId}>
71+
${lockedString(UIStrings.viewSources)}&nbsp;${lockedString('(' + input.citations.length + ')')}</span>` : nothing;
72+
const viewSourcesTooltip = input.citations && input.citations.length > 0 ?
73+
html`<devtools-tooltip
74+
id=${input.citationsTooltipId}
75+
variant=${'rich'}
76+
jslogContext=${input.panelName + '.ai-code-completion-citations'}
77+
><div class="citations-tooltip-container">
78+
${Directives.repeat(input.citations, citation => html`<x-link
79+
href=${citation}
80+
jslog=${VisualLogging.link(input.panelName + '.ai-code-completion-citations.citation-link').track({
81+
click: true
82+
})}>${citation}</x-link>`)}</div></devtools-tooltip>` : nothing;
83+
84+
render(
85+
html`
86+
<style>${styles}</style>
87+
<div class="ai-code-completion-summary-toolbar">
88+
<div class="ai-code-completion-disclaimer">
89+
<span
90+
class="link"
91+
role="link"
92+
jslog=${VisualLogging.link('open-ai-settings').track({
93+
click: true,
94+
})}
95+
aria-details=${input.disclaimerTooltipId}
96+
@click=${() => {
97+
void UI.ViewManager.ViewManager.instance().showView('chrome-ai');
98+
}}
99+
>${lockedString(UIStrings.relevantData)}</span>&nbsp;${lockedString(UIStrings.isSentToGoogle)}
100+
<devtools-tooltip
101+
id=${input.disclaimerTooltipId}
102+
variant=${'rich'}
103+
jslogContext=${input.panelName + '.ai-code-completion-disclaimer'}
104+
${Directives.ref(output.tooltipRef)}
105+
><div class="disclaimer-tooltip-container">
106+
<div class="tooltip-text">
107+
${input.noLogging ? lockedString(UIStrings.tooltipDisclaimerTextForAiCodeCompletionNoLogging) : lockedString(UIStrings.tooltipDisclaimerTextForAiCodeCompletion)}
108+
</div>
109+
<div
110+
class="link"
111+
role="link"
112+
jslog=${VisualLogging.link('open-ai-settings').track({
113+
click: true,
114+
})}
115+
@click=${input.onManageInSettingsTooltipClick}
116+
>${lockedString(UIStrings.manageInSettings)}</div></div></devtools-tooltip>
117+
</div>
118+
<div class="ai-code-completion-recitation-notice">${lockedString(UIStrings.generatedCodeMayBeSubjectToALicense)}
119+
${viewSourcesSpan}
120+
${viewSourcesTooltip}
121+
</div>
122+
</div>
123+
`, target, {host: input});
124+
// clang-format on
125+
};
126+
127+
export class AiCodeCompletionSummaryToolbar extends UI.Widget.Widget {
128+
readonly #view: View;
129+
#viewOutput: ViewOutput = {};
130+
131+
#disclaimerTooltipId: string;
132+
#citationsTooltipId: string;
133+
#panelName: string;
134+
#citations: string[] = [];
135+
136+
#noLogging: boolean; // Whether the enterprise setting is `ALLOW_WITHOUT_LOGGING` or not.
137+
138+
constructor(disclaimerTooltipId: string, citationsTooltipId: string, panelName: string, view?: View) {
139+
super();
140+
this.#disclaimerTooltipId = disclaimerTooltipId;
141+
this.#citationsTooltipId = citationsTooltipId;
142+
this.#panelName = panelName;
143+
this.#noLogging = Root.Runtime.hostConfig.aidaAvailability?.enterprisePolicyValue ===
144+
Root.Runtime.GenAiEnterprisePolicyValue.ALLOW_WITHOUT_LOGGING;
145+
this.#view = view ?? DEFAULT_SUMMARY_TOOLBAR_VIEW;
146+
this.requestUpdate();
147+
}
148+
149+
#onManageInSettingsTooltipClick(): void {
150+
this.#viewOutput.tooltipRef?.value?.hidePopover();
151+
void UI.ViewManager.ViewManager.instance().showView('chrome-ai');
152+
}
153+
154+
updateCitations(citations: string[]): void {
155+
citations.forEach(citation => {
156+
if (!this.#citations.includes(citation)) {
157+
this.#citations.push(citation);
158+
}
159+
});
160+
this.requestUpdate();
161+
}
162+
163+
clearCitations(): void {
164+
this.#citations = [];
165+
this.requestUpdate();
166+
}
167+
168+
override performUpdate(): void {
169+
this.#view(
170+
{
171+
disclaimerTooltipId: this.#disclaimerTooltipId,
172+
citations: this.#citations,
173+
citationsTooltipId: this.#citationsTooltipId,
174+
panelName: this.#panelName,
175+
noLogging: this.#noLogging,
176+
onManageInSettingsTooltipClick: this.#onManageInSettingsTooltipClick.bind(this),
177+
},
178+
this.#viewOutput, this.contentElement);
179+
}
180+
}

front_end/panels/common/BUILD.gn

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import("../visibility.gni")
1010

1111
generate_css("css_files") {
1212
sources = [
13+
"aiCodeCompletionSummaryToolbar.css",
1314
"aiCodeCompletionTeaser.css",
1415
"common.css",
1516
"freDialog.css",
@@ -18,6 +19,7 @@ generate_css("css_files") {
1819

1920
devtools_module("common") {
2021
sources = [
22+
"AiCodeCompletionSummaryToolbar.ts",
2123
"AiCodeCompletionTeaser.ts",
2224
"FreDialog.ts",
2325
]
@@ -29,6 +31,7 @@ devtools_module("common") {
2931
"../../core/platform:bundle",
3032
"../../ui/components/buttons:bundle",
3133
"../../ui/components/snackbars:bundle",
34+
"../../ui/components/tooltips:bundle",
3235
"../../ui/legacy:bundle",
3336
"../../ui/lit:bundle",
3437
"../../ui/visual_logging:bundle",
@@ -57,7 +60,10 @@ devtools_entrypoint("bundle") {
5760
ts_library("unittests") {
5861
testonly = true
5962

60-
sources = [ "AiCodeCompletionTeaser.test.ts" ]
63+
sources = [
64+
"AiCodeCompletionSummaryToolbar.test.ts",
65+
"AiCodeCompletionTeaser.test.ts",
66+
]
6167

6268
deps = [
6369
":bundle",

0 commit comments

Comments
 (0)