Skip to content

Commit f994d4a

Browse files
bro314Gerrit Code Review
authored andcommitted
Merge "Add AI Chat"
2 parents 6d4020c + 379310e commit f994d4a

39 files changed

+6984
-20
lines changed

polygerrit-ui/app/api/ai-code-review.ts

Lines changed: 234 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
* Copyright 2025 Google LLC
44
* SPDX-License-Identifier: Apache-2.0
55
*/
6-
import {ChangeInfo} from './rest-api';
6+
import {ChangeInfo, CommentInfo, FileInfoStatus} from './rest-api';
77

88
export declare interface AiCodeReviewPluginApi {
99
/**
@@ -13,7 +13,26 @@ export declare interface AiCodeReviewPluginApi {
1313
register(provider: AiCodeReviewProvider): void;
1414
}
1515

16+
export declare interface Action {
17+
id: string;
18+
display_text: string;
19+
hover_text?: string;
20+
// The subtext for this action. This is displayed below the label.
21+
subtext?: string;
22+
icon?: string;
23+
// Whether to show the splash page card for this action.
24+
enable_splash_page_card?: boolean;
25+
// Whether to send the request without user input.
26+
enable_send_without_input?: boolean;
27+
// The prompt that is fired by this action.
28+
initial_user_prompt?: string;
29+
// The links to the context items that are implicitly added.
30+
context_item_links?: string[];
31+
}
32+
1633
export declare interface ChatRequest {
34+
/** The predefined action the user selected in the chat. */
35+
action: Action;
1736
/**
1837
* The prompt to be sent to the LLM.
1938
*/
@@ -24,15 +43,48 @@ export declare interface ChatRequest {
2443
* To continue an existing conversation, the caller should provide the UUID of
2544
* the conversation.
2645
*/
27-
conversationId: string;
46+
conversation_id: string;
2847
/**
2948
* Plugins can choose what context they want to derive from the change and
3049
* send along to their backends. `changeInfo` contains broadly all the
3150
* information about the change, and the plugin can also make additional
3251
* requests to the REST API (e.g. getting patch content) by using properties
3352
* from the change info.
3453
*/
35-
changeInfo: ChangeInfo;
54+
change: ChangeInfo;
55+
/**
56+
* The list of files in the change is vital information that is missing from
57+
* `changeInfo`. So we are passing this along, as well.
58+
*/
59+
files: {path: string; status: FileInfoStatus}[];
60+
/**
61+
* The 0-based turn index of the request. The caller should set it based on
62+
* the history of the conversation. It should be one more than the last known
63+
* turn. For new conversations, it should be 0.
64+
*/
65+
turn_index: number;
66+
/**
67+
* The 0-based index of the turn regeneration. 0 - original turn, and it is
68+
* incremented by FE every time the user clicks on the regenerate button.
69+
*/
70+
regeneration_index: number;
71+
/**
72+
* A payload containing FE-specific data that is used to restore the chat
73+
* history in the UI. The BE should not use the data, only store it and
74+
* return it as part of GetConversationResponse.
75+
* This is simply encoded/decoded by the chat-model using JSON.stringify()
76+
* and JSON.parse().
77+
*/
78+
client_data: string;
79+
/**
80+
* The name of the model to use. If not set, the default model will
81+
* be used. If invalid, an error will be returned.
82+
*/
83+
model_name?: string;
84+
/**
85+
* The external contexts that should be used in the request.
86+
*/
87+
external_contexts: ContextItem[];
3688
}
3789

3890
/**
@@ -41,26 +93,201 @@ export declare interface ChatRequest {
4193
*/
4294
export declare interface ChatResponseListener {
4395
/**
44-
* Emits one piece of a streaming text response from the backend, to be
45-
* interpreted as markdown by the web app and to be shown as is to the user.
96+
* Emits one piece of a streaming response from the backend. All responses
97+
* must be merged into one response object by the listener, but every
98+
* intermediate state can be shown to the user.
4699
*/
47-
emitText(text: string): void;
100+
emitResponse(response: ChatResponse): void;
48101
/**
49102
* Emits an error message, indicating that the turn has failed. Will be
50103
* immediately followed by a done() call.
51104
*/
52105
emitError(error: string): void;
53106
/**
54-
* The turn is completed. The listener can be discarded.
107+
* The turn is completed. All response parts have been emitted. The listener
108+
* can be discarded.
55109
*/
56110
done(): void;
57111
}
58112

113+
export declare interface ChatResponse {
114+
response_parts: ChatResponsePart[];
115+
/**
116+
* References that were used to generate the response. Corresponds to tool
117+
* usage calls by the model.
118+
*/
119+
references: Reference[];
120+
/** The timestamp when the request was processed */
121+
timestamp_millis?: number;
122+
/**
123+
* The citations that were used to generate the response. Citations are
124+
* passages that are "recited" from potentially copyrighted material.
125+
*/
126+
citations: string[];
127+
}
128+
129+
export declare interface ChatResponsePart {
130+
/** The unique ID of the response part within the turn */
131+
id: number;
132+
/** A text part of the response, to be rendered as markdown */
133+
text?: string;
134+
/** A suggested comment that can be shown to the user */
135+
comment?: Partial<CommentInfo>;
136+
/** A text that can be copied to the clipboard */
137+
copyable_text?: CopyableText;
138+
}
139+
140+
export declare interface CopyableText {
141+
text?: string;
142+
copyable_text?: string;
143+
}
144+
145+
/** A reference that was used by Gemini to generate a response. */
146+
export declare interface Reference {
147+
/** May match the type id of ContextItemType. */
148+
type: string;
149+
displayText: string;
150+
secondaryText?: string;
151+
externalUrl: string;
152+
errorMsg?: string;
153+
tooltip?: string;
154+
}
155+
156+
export declare interface Conversation {
157+
/** UUID of the conversation */
158+
id: string;
159+
/** Title of the conversation */
160+
title: string;
161+
/** Timestamp of the last turn in the conversation */
162+
timestamp_millis: number;
163+
}
164+
165+
export declare interface ConversationTurn {
166+
user_input: UserInput;
167+
response: ChatResponse;
168+
regeneration_index?: number;
169+
timestamp_millis?: number;
170+
}
171+
172+
/**
173+
* The data sent by the client to the backend. It is stored in the database and
174+
* returned in the response. It is used to restore the history of the
175+
* conversation in the UI. The message should contain all data required to make
176+
* a turn.
177+
*/
178+
export declare interface UserInput {
179+
/** The text the user typed in the chat. Can be empty for some actions. */
180+
user_question?: string;
181+
/**
182+
* The data required by the UI to restore the history of the conversation.
183+
* The server only stores the data and doesn't care about the content.
184+
* This is simply encoded/decoded by the chat-model using JSON.stringify()
185+
* and JSON.parse().
186+
*/
187+
client_data?: string;
188+
}
189+
190+
export declare interface Models {
191+
/**
192+
* The models available to the user. Should be displayed in the UI in the
193+
* order of appearance in this list.
194+
*/
195+
models: ModelInfo[];
196+
/**
197+
* The default model to use when the user hasn't selected any model yet or
198+
* when the selected model is not available anymore.
199+
*/
200+
default_model_id: string;
201+
202+
documentation_url?: string;
203+
204+
citation_url?: string;
205+
206+
privacy_url?: string;
207+
}
208+
209+
export declare interface Actions {
210+
/**
211+
* The actions available to the user. Should be displayed in the UI in the
212+
* order of appearance in this list.
213+
*/
214+
actions: Action[];
215+
/**
216+
* The default action to use when the user hasn't made an explicit choice.
217+
*/
218+
default_action_id: string;
219+
}
220+
221+
export declare interface ModelInfo {
222+
/** The model id which is used to identify the model. */
223+
model_id: string;
224+
/** The short text to be displayed in the UI. */
225+
short_text: string;
226+
/** The full text to be displayed in the UI. */
227+
full_display_text: string;
228+
}
229+
230+
export declare interface ContextItemType {
231+
id: string;
232+
name: string;
233+
icon: string;
234+
/**
235+
* The regex to match the context item type. Will be applied to input
236+
* strings, but can also be used to find context items in longer texts such as
237+
* the user prompt.
238+
*/
239+
regex: RegExp;
240+
/**
241+
* The placeholder text to be displayed in input fields. Tells the user what
242+
* kind of input is expected and can be parsed.
243+
*/
244+
placeholder: string;
245+
/** Parses the input string into a context item of this type. */
246+
parse(input: string): ContextItem | undefined;
247+
}
248+
249+
export declare interface ContextItem {
250+
type_id: string;
251+
link: string;
252+
title: string;
253+
identifier?: string;
254+
tooltip?: string;
255+
error_message?: string;
256+
}
257+
59258
export declare interface AiCodeReviewProvider {
60259
/**
61260
* If a AiCodeReviewProvider provider is registered that implements this
62261
* method, then Gerrit will offer a side panel for the user to have an AI
63262
* Chat conversation. Each chat() call is one turn of such a conversation.
64263
*/
65264
chat?(req: ChatRequest, listener: ChatResponseListener): void;
265+
266+
/**
267+
* List all chat conversations for the current user and the given change.
268+
*/
269+
listChatConversations?(change: ChangeInfo): Promise<Conversation[]>;
270+
271+
/**
272+
* Retrieve the details of a single conversation.
273+
*/
274+
getChatConversation?(
275+
change: ChangeInfo,
276+
conversation_id: string
277+
): Promise<ConversationTurn[]>;
278+
279+
/**
280+
* Get available models for the given change.
281+
*/
282+
getModels?(change: ChangeInfo): Promise<Models>;
283+
284+
/**
285+
* Get available actions for the given change.
286+
*/
287+
getActions?(change: ChangeInfo): Promise<Actions>;
288+
289+
/**
290+
* Get the list of context item types that the provider supports.
291+
*/
292+
getContextItemTypes?(): Promise<ContextItemType[]>;
66293
}

polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -277,7 +277,7 @@ const ACTIONS_WITH_ICONS = new Map<
277277
[ChangeActions.REVERT, {icon: 'undo'}],
278278
[ChangeActions.STOP_EDIT, {icon: 'stop', filled: true}],
279279
[QUICK_APPROVE_ACTION.key, {icon: 'check'}],
280-
[AI_CHAT_ACTION.__key, {icon: 'stars'}],
280+
[AI_CHAT_ACTION.__key, {icon: 'star_shine'}],
281281
[RevisionActions.SUBMIT, {icon: 'done_all'}],
282282
]);
283283

polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.ts

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,12 @@
44
* SPDX-License-Identifier: Apache-2.0
55
*/
66
import {BehaviorSubject} from 'rxjs';
7-
import '../gr-copy-links/gr-copy-links';
87
import '../../../styles/gr-a11y-styles';
98
import '../../../styles/gr-material-styles';
109
import '../../../styles/shared-styles';
10+
import '../../chat-panel/chat-panel';
11+
import '../../checks/gr-checks-tab';
12+
import '../../diff/gr-apply-fix-dialog/gr-apply-fix-dialog';
1113
import '../../plugins/gr-endpoint-decorator/gr-endpoint-decorator';
1214
import '../../plugins/gr-endpoint-param/gr-endpoint-param';
1315
import '../../shared/gr-button/gr-button';
@@ -17,6 +19,7 @@ import '../../shared/gr-editable-content/gr-editable-content';
1719
import '../../shared/gr-formatted-text/gr-formatted-text';
1820
import '../../shared/gr-tooltip-content/gr-tooltip-content';
1921
import '../../shared/gr-content-with-sidebar/gr-content-with-sidebar';
22+
import '../gr-copy-links/gr-copy-links';
2023
import '../gr-change-actions/gr-change-actions';
2124
import '../gr-change-summary/gr-change-summary';
2225
import '../gr-change-metadata/gr-change-metadata';
@@ -28,10 +31,8 @@ import '../gr-revision-parents/gr-revision-parents';
2831
import '../gr-included-in-dialog/gr-included-in-dialog';
2932
import '../gr-messages-list/gr-messages-list';
3033
import '../gr-related-changes-list/gr-related-changes-list';
31-
import '../../diff/gr-apply-fix-dialog/gr-apply-fix-dialog';
3234
import '../gr-reply-dialog/gr-reply-dialog';
3335
import '../gr-thread-list/gr-thread-list';
34-
import '../../checks/gr-checks-tab';
3536
import '../gr-flows/gr-flows';
3637
import {ChangeStarToggleStarDetail} from '../../shared/gr-change-star/gr-change-star';
3738
import {GrEditConstants} from '../../edit/gr-edit-constants';
@@ -467,6 +468,10 @@ export class GrChangeView extends LitElement {
467468
);
468469
this.addEventListener('open-fix-preview', e => this.onOpenFixPreview(e));
469470
this.addEventListener('show-tab', e => this.setActiveTab(e));
471+
this.addEventListener(
472+
'close-chat-panel',
473+
() => (this.showSidebarChat = false)
474+
);
470475
}
471476

472477
private setupShortcuts() {
@@ -1122,6 +1127,9 @@ export class GrChangeView extends LitElement {
11221127
.tabContent gr-thread-list::part(threads) {
11231128
padding: var(--spacing-l);
11241129
}
1130+
.sidebar {
1131+
height: var(--sidebar-height);
1132+
}
11251133
`,
11261134
];
11271135
}
@@ -1149,7 +1157,7 @@ export class GrChangeView extends LitElement {
11491157
${this.renderChangeLog()}
11501158
</div>
11511159
</div>
1152-
<div slot="side">${this.renderSidebar()}</div>
1160+
<div class="sidebar" slot="side">${this.renderSidebar()}</div>
11531161
</gr-content-with-sidebar>
11541162
<gr-apply-fix-dialog id="applyFixDialog"></gr-apply-fix-dialog>
11551163
<dialog id="downloadModal" tabindex="-1">
@@ -1200,7 +1208,7 @@ export class GrChangeView extends LitElement {
12001208

12011209
private renderSidebar() {
12021210
if (!this.showSidebarChat) return;
1203-
return html` <div>Hello, I am a fancy sidebar for AI Chat.</div> `;
1211+
return html`<chat-panel></chat-panel>`;
12041212
}
12051213

12061214
private renderChangeInfoSection() {

polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -425,7 +425,7 @@ suite('gr-change-view tests', () => {
425425
</section>
426426
</div>
427427
</div>
428-
<div slot="side"></div>
428+
<div class="sidebar" slot="side"></div>
429429
</gr-content-with-sidebar>
430430
<gr-apply-fix-dialog id="applyFixDialog"> </gr-apply-fix-dialog>
431431
<dialog id="downloadModal" tabindex="-1">

0 commit comments

Comments
 (0)