Merge "Add REST API for migrating label functions to submit requirements (Reland)"
diff --git a/Documentation/config-gerrit.txt b/Documentation/config-gerrit.txt
index a603c42..c3cc5f3 100644
--- a/Documentation/config-gerrit.txt
+++ b/Documentation/config-gerrit.txt
@@ -5536,6 +5536,16 @@
+
By default 25.
+[[performance.slowRequestThreshold]]performance.slowRequestThreshold::
++
+Threshold from which on a request is considered as "slow" if exceeded.
++
+For slow requests a performance summary is logged on warning level (only if
+performance logging is enabled (see
+link:#tracing.performanceLogging[tracing.performanceLogging] setting).
++
+By default 30s.
+
[[performance.metric]]
==== Subsection performance.metric
diff --git a/Documentation/rest-api-projects.txt b/Documentation/rest-api-projects.txt
index 16dab80..4c60d37 100644
--- a/Documentation/rest-api-projects.txt
+++ b/Documentation/rest-api-projects.txt
@@ -114,8 +114,7 @@
},
"some-other-project": {
"id": "some-other-project",
- "description": "Description of some other project."
- }
+ "description": "Description of some other project."
},
}
----
@@ -140,7 +139,7 @@
{
"some-project": {
"id": "some-project",
- _more_projects: true
+ "_more_projects": true
}
}
----
diff --git a/WORKSPACE b/WORKSPACE
index 3d32937..666137b 100644
--- a/WORKSPACE
+++ b/WORKSPACE
@@ -108,8 +108,19 @@
load("@build_bazel_rules_nodejs//:index.bzl", "node_repositories", "yarn_install")
+NODE_20_REPO = {
+ "20.19.5-darwin_arm64": ("node-v20.19.5-darwin-arm64.tar.gz", "node-v20.19.5-darwin-arm64", "cfed7503d8d99fbcf2f52e408ec52f616058eb0867b34dbc3437259993ef5cba"),
+ "20.19.5-darwin_amd64": ("node-v20.19.5-darwin-x64.tar.gz", "node-v20.19.5-darwin-x64", "f9cff058f2766d4d0631dc69b5f7f27664b3a42ff186e25ac7e1ac269af7e696"),
+ "20.19.5-linux_arm64": ("node-v20.19.5-linux-arm64.tar.xz", "node-v20.19.5-linux-arm64", "d462267863ae8ee556039ebdf559055a8ec562c633889ef1403f3adb449ba1dd"),
+ "20.19.5-linux_ppc64le": ("node-v20.19.5-linux-ppc64le.tar.xz", "node-v20.19.5-linux-ppc64le", "ef98025e71d6d498476a95f144e353be074b24431b22eaa81bc64f921ea7d57f"),
+ "20.19.5-linux_s390x": ("node-v20.19.5-linux-s390x.tar.xz", "node-v20.19.5-linux-s390x", "a2e56c4b7fbffd0e6eef3a89e1c5945962fe85b4e2acfa59edc77a9238cc7901"),
+ "20.19.5-linux_amd64": ("node-v20.19.5-linux-x64.tar.xz", "node-v20.19.5-linux-x64", "315046739a513a70e03a4a55a8afda8cf979f30852e576075c340084e3f8ac0f"),
+ "20.19.5-windows_amd64": ("node-v20.19.5-win-x64.zip", "node-v20.19.5-win-x64", "c48159529572a5a947eef2d55d6485dfdc4ce8e67216402e2f6de52ad5d95695"),
+}
+
node_repositories(
- node_version = "20.14.0",
+ node_repositories = NODE_20_REPO,
+ node_version = "20.19.5",
yarn_version = "1.22.19",
)
diff --git a/java/com/google/gerrit/server/logging/PerformanceLogContext.java b/java/com/google/gerrit/server/logging/PerformanceLogContext.java
index f2bdc29..703e340 100644
--- a/java/com/google/gerrit/server/logging/PerformanceLogContext.java
+++ b/java/com/google/gerrit/server/logging/PerformanceLogContext.java
@@ -17,6 +17,8 @@
import static com.google.common.collect.ImmutableList.toImmutableList;
import static com.google.common.flogger.LazyArgs.lazy;
import static java.util.Comparator.comparing;
+import static java.util.concurrent.TimeUnit.NANOSECONDS;
+import static java.util.concurrent.TimeUnit.SECONDS;
import com.google.common.base.Joiner;
import com.google.common.collect.ImmutableList;
@@ -24,10 +26,12 @@
import com.google.gerrit.extensions.registration.DynamicSet;
import com.google.gerrit.extensions.registration.Extension;
import com.google.gerrit.server.cancellation.PerformanceSummaryProvider;
+import com.google.gerrit.server.util.time.TimeUtil;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
+import java.util.logging.Level;
import org.eclipse.jgit.lib.Config;
/**
@@ -62,6 +66,11 @@
/** Maximum number of operations for which the latency should be logged. */
private final int maxOperationsToLog;
+ /** Threshold in nanos from which on a request is considered as "slow" if exceeded. */
+ private final long slowRequestThresholdNanos;
+
+ private long startNanos;
+
public PerformanceLogContext(
Config gerritConfig, DynamicSet<PerformanceLogger> performanceLoggers) {
this.performanceLoggers = performanceLoggers;
@@ -76,6 +85,15 @@
this.maxOperationsToLog =
gerritConfig.getInt("performance", "maxOperationsToLog", DEFAULT_MAX_OPERATIONS_TO_LOG);
+
+ this.slowRequestThresholdNanos =
+ gerritConfig.getTimeUnit(
+ "performance",
+ /* subsection= */ null,
+ "slowRequestThreshold",
+ SECONDS.toNanos(30),
+ NANOSECONDS);
+ this.startNanos = TimeUtil.nowNanos();
}
@Override
@@ -84,12 +102,23 @@
runEach(performanceLoggers, LoggingContext.getInstance().getPerformanceLogRecords());
}
- logger.atFine().log(
+ long elapsedNanos = TimeUtil.nowNanos() - startNanos;
+ logger.at(elapsedNanos > slowRequestThresholdNanos ? Level.WARNING : Level.FINE).log(
"%s",
lazy(
() ->
getPerformanceSummary()
- .map(performanceSummary -> "[Performance Summary]\n\n" + performanceSummary)
+ .map(
+ performanceSummary ->
+ String.format(
+ "%s\n\n%s",
+ elapsedNanos > slowRequestThresholdNanos
+ ? String.format(
+ "Performance Summary for slow request (request took longer"
+ + " than %ss)",
+ NANOSECONDS.toSeconds(slowRequestThresholdNanos))
+ : "[Performance Summary]",
+ performanceSummary))
.orElse("No performance summary available")));
// Restore old state. Required to support nesting of PerformanceLogContext's.
diff --git a/javatests/com/google/gerrit/acceptance/api/change/PostReviewIT.java b/javatests/com/google/gerrit/acceptance/api/change/PostReviewIT.java
index 7d0d70e..6085900 100644
--- a/javatests/com/google/gerrit/acceptance/api/change/PostReviewIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/change/PostReviewIT.java
@@ -23,7 +23,6 @@
import static com.google.gerrit.testing.GerritJUnit.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.Mockito.clearInvocations;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import static org.mockito.MockitoAnnotations.initMocks;
@@ -76,12 +75,10 @@
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.notedb.ChangeNotes;
import com.google.gerrit.server.restapi.change.OnPostReview;
-import com.google.gerrit.server.restapi.change.PostReview;
import com.google.gerrit.server.update.CommentsRejectedException;
import com.google.gerrit.testing.FakeEmailSender;
import com.google.gerrit.testing.TestCommentHelper;
import com.google.inject.Inject;
-import com.google.inject.Module;
import java.sql.Timestamp;
import java.time.Instant;
import java.util.ArrayList;
@@ -101,7 +98,6 @@
/** Tests for comment validation in {@link PostReview}. */
public class PostReviewIT extends AbstractDaemonTest {
- @Inject private CommentValidator mockCommentValidator;
@Inject private TestCommentHelper testCommentHelper;
@Inject private RequestScopeOperations requestScopeOperations;
@Inject private ExtensionRegistry extensionRegistry;
@@ -127,9 +123,13 @@
COMMENT_TEXT,
COMMENT_TEXT.length());
+ private static CommentValidator mockCommentValidator;
@Captor private ArgumentCaptor<ImmutableList<CommentForValidation>> captor;
@Captor private ArgumentCaptor<CommentValidationContext> captorCtx;
+ @SuppressWarnings("unused")
+ private AutoCloseable mockCommentValidatorPlugin;
+
private static final Correspondence<CommentForValidation, CommentForValidation>
COMMENT_CORRESPONDENCE =
Correspondence.from(
@@ -141,28 +141,23 @@
&& left.getText().equals(right.getText()),
"matches (ignoring size approximation)");
- @Override
- public Module createModule() {
- return new FactoryModule() {
- @Override
- public void configure() {
- CommentValidator mockCommentValidator = mock(CommentValidator.class);
-
- // by default return no validation errors
- when(mockCommentValidator.validateComments(any(), any())).thenReturn(ImmutableList.of());
-
- bind(CommentValidator.class)
- .annotatedWith(Exports.named(mockCommentValidator.getClass()))
- .toInstance(mockCommentValidator);
- bind(CommentValidator.class).toInstance(mockCommentValidator);
- }
- };
+ public static class MockCommentValidatorModule extends FactoryModule {
+ @Override
+ public void configure() {
+ // by default return no validation errors
+ bind(CommentValidator.class)
+ .annotatedWith(Exports.named(mockCommentValidator.getClass()))
+ .toInstance(mockCommentValidator);
+ bind(CommentValidator.class).toInstance(mockCommentValidator);
+ }
}
@Before
- public void resetMock() {
+ public void resetMockCommentValidator() throws Exception {
+ mockCommentValidator = mock(CommentValidator.class);
initMocks(this);
- clearInvocations(mockCommentValidator);
+ mockCommentValidatorPlugin = super.installPlugin("validator", MockCommentValidatorModule.class);
+ when(mockCommentValidator.validateComments(any(), any())).thenReturn(ImmutableList.of());
}
@Test
diff --git a/plugins/download-commands b/plugins/download-commands
index 04a31c1..dc2b671 160000
--- a/plugins/download-commands
+++ b/plugins/download-commands
@@ -1 +1 @@
-Subproject commit 04a31c1a520eac9d1844acc52665d0892bc4e35f
+Subproject commit dc2b67185694c220a95525b6aeb717e663075e3c
diff --git a/polygerrit-ui/app/api/ai-code-review.ts b/polygerrit-ui/app/api/ai-code-review.ts
new file mode 100644
index 0000000..c3148a6
--- /dev/null
+++ b/polygerrit-ui/app/api/ai-code-review.ts
@@ -0,0 +1,66 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+import {ChangeInfo} from './rest-api';
+
+export declare interface AiCodeReviewPluginApi {
+ /**
+ * Must only be called once. You cannot register twice (throws an error).
+ * You cannot unregister.
+ */
+ register(provider: AiCodeReviewProvider): void;
+}
+
+export declare interface ChatRequest {
+ /**
+ * The prompt to be sent to the LLM.
+ */
+ prompt: string;
+ /**
+ * UUID of the conversation. To start a new conversation, the caller should
+ * generate a new UUID.
+ * To continue an existing conversation, the caller should provide the UUID of
+ * the conversation.
+ */
+ conversationId: string;
+ /**
+ * Plugins can choose what context they want to derive from the change and
+ * send along to their backends. `changeInfo` contains broadly all the
+ * information about the change, and the plugin can also make additional
+ * requests to the REST API (e.g. getting patch content) by using properties
+ * from the change info.
+ */
+ changeInfo: ChangeInfo;
+}
+
+/**
+ * The chat response may come in as a stream, so instead of just one response
+ * object this listener will get multiple calls until the response is completed.
+ */
+export declare interface ChatResponseListener {
+ /**
+ * Emits one piece of a streaming text response from the backend, to be
+ * interpreted as markdown by the web app and to be shown as is to the user.
+ */
+ emitText(text: string): void;
+ /**
+ * Emits an error message, indicating that the turn has failed. Will be
+ * immediately followed by a done() call.
+ */
+ emitError(error: string): void;
+ /**
+ * The turn is completed. The listener can be discarded.
+ */
+ done(): void;
+}
+
+export declare interface AiCodeReviewProvider {
+ /**
+ * If a AiCodeReviewProvider provider is registered that implements this
+ * method, then Gerrit will offer a side panel for the user to have an AI
+ * Chat conversation. Each chat() call is one turn of such a conversation.
+ */
+ chat?(req: ChatRequest, listener: ChatResponseListener): void;
+}
diff --git a/polygerrit-ui/app/api/plugin.ts b/polygerrit-ui/app/api/plugin.ts
index 6f6dfb6..934c87cd 100644
--- a/polygerrit-ui/app/api/plugin.ts
+++ b/polygerrit-ui/app/api/plugin.ts
@@ -18,6 +18,7 @@
import {StylePluginApi} from './styles';
import {SuggestionsPluginApi} from './suggestions';
import {ChangeUpdatesPluginApi} from './change-updates';
+import {AiCodeReviewPluginApi} from './ai-code-review';
export enum TargetElement {
CHANGE_ACTIONS = 'changeactions',
@@ -65,6 +66,7 @@
*/
url(): string;
admin(): AdminPluginApi;
+ aiCodeReview(): AiCodeReviewPluginApi;
annotationApi(): AnnotationPluginApi;
attributeHelper(element: Element): AttributeHelperPluginApi;
changeActions(): ChangeActionsPluginApi;
diff --git a/polygerrit-ui/app/elements/admin/gr-permission/gr-permission.ts b/polygerrit-ui/app/elements/admin/gr-permission/gr-permission.ts
index d4df7db..ef96c2e 100644
--- a/polygerrit-ui/app/elements/admin/gr-permission/gr-permission.ts
+++ b/polygerrit-ui/app/elements/admin/gr-permission/gr-permission.ts
@@ -45,6 +45,7 @@
} from '../../../types/events';
import {throwingErrorCallback} from '../../shared/gr-rest-api-interface/gr-rest-apis/gr-rest-api-helper';
import {MdSwitch} from '@material/web/switch/switch';
+import {repeat} from 'lit/directives/repeat.js';
const MAX_AUTOCOMPLETE_RESULTS = 20;
@@ -246,7 +247,9 @@
</div>
<!-- end header -->
<div class="rules">
- ${this.rules?.map(
+ ${repeat(
+ this.rules ?? [],
+ rule => rule.id,
(rule, index) => html`
<gr-rule-editor
.hasRange=${this.computeHasRange(this.name)}
diff --git a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.ts b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.ts
index 12e2715..54c1469 100644
--- a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.ts
+++ b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.ts
@@ -121,6 +121,7 @@
import {commentsModelToken} from '../../../models/comments/comments-model';
import {when} from 'lit/directives/when.js';
import {ValidationOptionInfo} from '../../../api/rest-api';
+import {KnownExperimentId} from '../../../services/flags/flags';
const ERR_BRANCH_EMPTY = 'The destination branch can’t be empty.';
const ERR_COMMIT_EMPTY = 'The commit message can’t be empty.';
@@ -180,6 +181,13 @@
method: HttpMethod.POST,
};
+const AI_CHAT_ACTION: UIActionInfo = {
+ __key: 'chat',
+ __type: ActionType.CHANGE,
+ enabled: true,
+ label: 'AI Chat',
+};
+
function isQuickApproveAction(
action: UIActionInfo
): action is QuickApproveUIActionInfo {
@@ -268,6 +276,7 @@
[ChangeActions.REVERT, {icon: 'undo'}],
[ChangeActions.STOP_EDIT, {icon: 'stop', filled: true}],
[QUICK_APPROVE_ACTION.key, {icon: 'check'}],
+ [AI_CHAT_ACTION.__key, {icon: 'stars'}],
[RevisionActions.SUBMIT, {icon: 'done_all'}],
]);
@@ -487,12 +496,16 @@
@state() pluginsLoaded = false;
+ @state() aiPluginsRegistered = false;
+
@state() threadsWithUnappliedSuggestions?: CommentThread[];
private readonly restApiService = getAppContext().restApiService;
private readonly reporting = getAppContext().reportingService;
+ private readonly flagService = getAppContext().flagsService;
+
private readonly getPluginLoader = resolve(this, pluginLoaderToken);
private readonly getUserModel = resolve(this, userModelToken);
@@ -576,6 +589,11 @@
);
subscribe(
this,
+ () => this.getPluginLoader().pluginsModel.aiCodeReviewPlugins$,
+ plugins => (this.aiPluginsRegistered = (plugins.length ?? 0) > 0)
+ );
+ subscribe(
+ this,
() => this.getConfigModel().repoConfig$,
config => (this.privateByDefault = config?.private_by_default)
);
@@ -1219,6 +1237,16 @@
this._hideQuickApproveAction = true;
}
+ private getAiChatAction(): UIActionInfo | null {
+ if (!this.flagService.isEnabled(KnownExperimentId.ENABLE_AI_CHAT)) {
+ return null;
+ }
+ if (!this.aiPluginsRegistered) {
+ return null;
+ }
+ return AI_CHAT_ACTION;
+ }
+
private getQuickApproveAction(): QuickApproveUIActionInfo | null {
if (this._hideQuickApproveAction) {
return null;
@@ -1488,6 +1516,10 @@
this.fireAction(this.prependSlash(key), action, true, action.payload);
break;
}
+ case AI_CHAT_ACTION.__key: {
+ fire(this, 'ai-chat', {});
+ break;
+ }
case ChangeActions.EDIT:
this.handleEditTap();
break;
@@ -2200,6 +2232,10 @@
if (quickApprove) {
changeActionValues.unshift(quickApprove);
}
+ const aiChat = this.getAiChatAction();
+ if (aiChat) {
+ changeActionValues.unshift(aiChat);
+ }
return revisionActionValues
.concat(changeActionValues)
@@ -2327,6 +2363,7 @@
declare global {
interface HTMLElementEventMap {
+ 'ai-chat': CustomEvent<{}>;
'download-tap': CustomEvent<{}>;
'edit-tap': CustomEvent<{}>;
'included-tap': CustomEvent<{}>;
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.ts b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.ts
index 37e7ccf..d0e84ac 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.ts
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.ts
@@ -1185,6 +1185,10 @@
`;
}
+ private toggleChat() {
+ this.showSidebarChat = !this.showSidebarChat;
+ }
+
private renderSidebar() {
if (!this.showSidebarChat) return;
return html` <div>Hello, I am a fancy sidebar for AI Chat.</div> `;
@@ -1310,6 +1314,7 @@
<div class="commitActions">
<gr-change-actions
id="actions"
+ @ai-chat=${() => this.toggleChat()}
@edit-tap=${() => this.handleEditTap()}
@stop-edit-tap=${() => this.handleStopEditTap()}
@download-tap=${() => this.handleOpenDownloadDialog()}
diff --git a/polygerrit-ui/app/elements/plugins/gr-ai-code-review-api/gr-ai-code-review-api.ts b/polygerrit-ui/app/elements/plugins/gr-ai-code-review-api/gr-ai-code-review-api.ts
new file mode 100644
index 0000000..dd7db5a
--- /dev/null
+++ b/polygerrit-ui/app/elements/plugins/gr-ai-code-review-api/gr-ai-code-review-api.ts
@@ -0,0 +1,48 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+import {PluginApi} from '../../../api/plugin';
+import {ReportingService} from '../../../services/gr-reporting/gr-reporting';
+import {PluginsModel} from '../../../models/plugins/plugins-model';
+import {
+ AiCodeReviewPluginApi,
+ AiCodeReviewProvider,
+} from '../../../api/ai-code-review';
+
+enum State {
+ NOT_REGISTERED,
+ REGISTERED,
+}
+
+/**
+ * Plugin API for AI Code Review.
+ *
+ * This object is returned to plugins that want to provide AI Code Review data.
+ * Plugins normally just call register() once at startup and then wait for
+ * calls on the provider interface.
+ */
+export class GrAiCodeReviewApi implements AiCodeReviewPluginApi {
+ private state = State.NOT_REGISTERED;
+
+ constructor(
+ private readonly reporting: ReportingService,
+ private readonly pluginsModel: PluginsModel,
+ readonly plugin: PluginApi
+ ) {
+ this.reporting.trackApi(this.plugin, 'ai-code-review', 'constructor');
+ }
+
+ register(provider: AiCodeReviewProvider): void {
+ this.reporting.trackApi(this.plugin, 'ai-code-review', 'register');
+ if (this.state === State.REGISTERED) {
+ throw new Error('Only one provider can be registered per plugin.');
+ }
+ this.state = State.REGISTERED;
+ this.pluginsModel.aiCodeReviewRegister({
+ pluginName: this.plugin.getPluginName(),
+ provider,
+ });
+ }
+}
diff --git a/polygerrit-ui/app/elements/shared/gr-content-with-sidebar/gr-content-with-sidebar.ts b/polygerrit-ui/app/elements/shared/gr-content-with-sidebar/gr-content-with-sidebar.ts
index b61a10a..f87914f 100644
--- a/polygerrit-ui/app/elements/shared/gr-content-with-sidebar/gr-content-with-sidebar.ts
+++ b/polygerrit-ui/app/elements/shared/gr-content-with-sidebar/gr-content-with-sidebar.ts
@@ -43,8 +43,6 @@
:host {
display: block;
--sidebar-height: calc(100vh - var(--sidebar-top));
- --sidebar-top: var(--sidebar-top, 0px);
- --sidebar-bottom-overflow: var(--sidebar-bottom-overflow, 0px);
}
.sidebar-wrapper {
z-index: 50;
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-public-js-api.ts b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-public-js-api.ts
index 35c93e6..2a46546 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-public-js-api.ts
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-public-js-api.ts
@@ -37,6 +37,7 @@
import {StylePluginApi} from '../../../api/styles';
import {GrSuggestionsApi} from '../../plugins/gr-suggestions-api/gr-suggestions-api';
import {GrChangeUpdatesApi} from '../../plugins/gr-change-updates-api/gr-change-updates-api';
+import {GrAiCodeReviewApi} from '../../plugins/gr-ai-code-review-api/gr-ai-code-review-api';
const PLUGIN_NAME_NOT_SET = 'NULL';
@@ -179,6 +180,10 @@
return `${origin}${base}/x/${this.getPluginName()}${tokenPart}`;
}
+ aiCodeReview(): GrAiCodeReviewApi {
+ return new GrAiCodeReviewApi(this.report, this.pluginsModel, this);
+ }
+
annotationApi(): AnnotationPluginApi {
return new GrAnnotationActionsInterface(
this.report,
diff --git a/polygerrit-ui/app/embed/gr-textarea.ts b/polygerrit-ui/app/embed/gr-textarea.ts
index 7a6cbcf..260ac37 100644
--- a/polygerrit-ui/app/embed/gr-textarea.ts
+++ b/polygerrit-ui/app/embed/gr-textarea.ts
@@ -4,7 +4,13 @@
* SPDX-License-Identifier: Apache-2.0
*/
import {css, html, LitElement} from 'lit';
-import {customElement, property, query, queryAsync} from 'lit/decorators.js';
+import {
+ customElement,
+ property,
+ query,
+ queryAsync,
+ state,
+} from 'lit/decorators.js';
import {classMap} from 'lit/directives/class-map.js';
import {ifDefined} from 'lit/directives/if-defined.js';
import {
@@ -168,6 +174,7 @@
return this.editableDivElement?.scrollTop ?? 0;
}
+ @state()
private innerValue: string | undefined;
private innerHint: string | undefined;
@@ -214,7 +221,7 @@
outline: none;
}
- &:empty::before {
+ &[data-empty='true']::before {
content: attr(data-placeholder);
color: var(--text-secondary, lightgrey);
display: inline;
@@ -271,6 +278,7 @@
aria-multiline="true"
aria-placeholder=${ifDefined(ariaPlaceholder)}
data-placeholder=${ifDefined(placeholder)}
+ data-empty=${this.innerValue === ''}
class=${classes}
contenteditable=${this.contentEditableAttributeValue}
dir="ltr"
@@ -659,44 +667,16 @@
private async getValue() {
const editableDivElement = await this.editableDiv;
if (editableDivElement) {
- const [output] = this.parseText(editableDivElement, false, true);
- return output;
+ // When you delete all text, it leaves a \n (or maybe \r\n?).
+ // Fix this by making it return a empty string.
+ if (/^\r?\n$/.test(editableDivElement.innerText)) {
+ return '';
+ }
+ return editableDivElement.innerText;
}
return '';
}
- private parseText(
- node: Node,
- isLastBr: boolean,
- isFirst: boolean
- ): [string, boolean] {
- let textValue = '';
- let output = '';
- if (node.nodeName === 'BR') {
- return ['\n', true];
- }
-
- if (node.nodeType === Node.TEXT_NODE && node.textContent) {
- return [node.textContent, false];
- }
-
- if (node.nodeName === 'DIV' && !isLastBr && !isFirst) {
- textValue = '\n';
- }
-
- isLastBr = false;
-
- for (let i = 0; i < node.childNodes?.length; i++) {
- [output, isLastBr] = this.parseText(
- node.childNodes[i],
- isLastBr,
- i === 0
- );
- textValue += output;
- }
- return [textValue, isLastBr];
- }
-
public getCursorPosition() {
return this.getCursorPositionForDiv(this.editableDivElement);
}
diff --git a/polygerrit-ui/app/models/plugins/plugins-model.ts b/polygerrit-ui/app/models/plugins/plugins-model.ts
index 9df61bc..3ce60da 100644
--- a/polygerrit-ui/app/models/plugins/plugins-model.ts
+++ b/polygerrit-ui/app/models/plugins/plugins-model.ts
@@ -15,6 +15,7 @@
import {CoverageProvider, TokenHoverListener} from '../../api/annotation';
import {SuggestionsProvider} from '../../api/suggestions';
import {ChangeUpdatesPublisher} from '../../api/change-updates';
+import {AiCodeReviewProvider} from '../../api/ai-code-review';
export interface CoveragePlugin {
pluginName: string;
@@ -32,6 +33,11 @@
publisher: ChangeUpdatesPublisher;
}
+export interface AiCodeReviewPlugin {
+ pluginName: string;
+ provider: AiCodeReviewProvider;
+}
+
export interface SuggestionPlugin {
pluginName: string;
provider: SuggestionsProvider;
@@ -70,6 +76,11 @@
checksPlugins: ChecksPlugin[];
/**
+ * List of plugins that have called aiCodeReview().register().
+ */
+ aiCodeReviewPlugins: AiCodeReviewPlugin[];
+
+ /**
* List of plugins that have called suggestions().register().
*/
suggestionsPlugins: SuggestionPlugin[];
@@ -105,6 +116,11 @@
state => state.changeUpdatesPlugins
);
+ public aiCodeReviewPlugins$ = select(
+ this.state$,
+ state => state.aiCodeReviewPlugins
+ );
+
public suggestionsPlugins$ = select(
this.state$,
state => state.suggestionsPlugins
@@ -118,6 +134,7 @@
coveragePlugins: [],
changeUpdatesPlugins: [],
checksPlugins: [],
+ aiCodeReviewPlugins: [],
suggestionsPlugins: [],
tokenHighlightPlugins: [],
});
@@ -175,6 +192,22 @@
this.setState(nextState);
}
+ aiCodeReviewRegister(plugin: AiCodeReviewPlugin) {
+ const nextState = {...this.getState()};
+ nextState.aiCodeReviewPlugins = [...nextState.aiCodeReviewPlugins];
+ const alreadyRegistered = nextState.aiCodeReviewPlugins.some(
+ p => p.pluginName === plugin.pluginName
+ );
+ if (alreadyRegistered) {
+ console.warn(
+ `${plugin.pluginName} tried to register twice as a AI Code Review provider. Ignored.`
+ );
+ return;
+ }
+ nextState.aiCodeReviewPlugins.push(plugin);
+ this.setState(nextState);
+ }
+
suggestionsRegister(plugin: SuggestionPlugin) {
const nextState = {...this.getState()};
nextState.suggestionsPlugins = [...nextState.suggestionsPlugins];
diff --git a/polygerrit-ui/app/services/flags/flags.ts b/polygerrit-ui/app/services/flags/flags.ts
index 2c7993c..70d24aa 100644
--- a/polygerrit-ui/app/services/flags/flags.ts
+++ b/polygerrit-ui/app/services/flags/flags.ts
@@ -25,4 +25,5 @@
ML_SUGGESTED_EDIT_EDITABLE_SUGGESTION = 'UiFeature__ml_suggested_edit_editable_suggestion',
SHOW_FLOWS_TAB = 'UiFeature__show_flows_tab',
ASYNC_SUBMIT_REQUIREMENTS = 'UiFeature__async_submit_requirements',
+ ENABLE_AI_CHAT = 'UiFeature__enable_ai_chat',
}