Merge "Fix npm run eslintfix and enable ESLint caching"
diff --git a/external_deps.lock.json b/external_deps.lock.json
index 9b63a74..141bbfc 100644
--- a/external_deps.lock.json
+++ b/external_deps.lock.json
@@ -156,9 +156,9 @@
     "org.apache.mina:mina-core": -1403307213,
     "org.apache.sshd:sshd-common": 1844247612,
     "org.apache.sshd:sshd-core": 602454664,
-    "org.apache.sshd:sshd-mina": -1915268089,
-    "org.apache.sshd:sshd-osgi": 1681538032,
-    "org.apache.sshd:sshd-sftp": 2089761863,
+    "org.apache.sshd:sshd-mina": 582747848,
+    "org.apache.sshd:sshd-osgi": -115413327,
+    "org.apache.sshd:sshd-sftp": 292810504,
     "org.asciidoctor:asciidoctorj": -457860213,
     "org.assertj:assertj-core": -1145412507,
     "org.bouncycastle:bcpg-jdk18on": -1572213535,
@@ -169,7 +169,7 @@
     "org.commonmark:commonmark-ext-autolink": -1853742120,
     "org.commonmark:commonmark-ext-gfm-strikethrough": 350394231,
     "org.commonmark:commonmark-ext-gfm-tables": 1881582931,
-    "org.eclipse.jetty.ee10:jetty-ee10-servlet": 835495267,
+    "org.eclipse.jetty.ee11:jetty-ee11-servlet": -562048764,
     "org.eclipse.jetty.ee8:jetty-ee8-nested": 860016002,
     "org.eclipse.jetty.ee8:jetty-ee8-security": -175183477,
     "org.eclipse.jetty.ee8:jetty-ee8-servlet": 1148815122,
@@ -402,12 +402,12 @@
     "org.apache.lucene:lucene-queryparser:jar:sources": 1992621418,
     "org.apache.mina:mina-core": -584591076,
     "org.apache.mina:mina-core:jar:sources": 71382131,
-    "org.apache.sshd:sshd-mina": 1147629185,
-    "org.apache.sshd:sshd-mina:jar:sources": -506380854,
-    "org.apache.sshd:sshd-osgi": 1092773821,
-    "org.apache.sshd:sshd-osgi:jar:sources": -2061003935,
-    "org.apache.sshd:sshd-sftp": -70173235,
-    "org.apache.sshd:sshd-sftp:jar:sources": 1034555480,
+    "org.apache.sshd:sshd-mina": -36716477,
+    "org.apache.sshd:sshd-mina:jar:sources": 97528844,
+    "org.apache.sshd:sshd-osgi": 1810958617,
+    "org.apache.sshd:sshd-osgi:jar:sources": 710209222,
+    "org.apache.sshd:sshd-sftp": -320832840,
+    "org.apache.sshd:sshd-sftp:jar:sources": -1268961386,
     "org.asciidoctor:asciidoctorj": 1685789893,
     "org.asciidoctor:asciidoctorj:jar:sources": 2091708864,
     "org.assertj:assertj-core": -59722598,
@@ -432,8 +432,8 @@
     "org.commonmark:commonmark-ext-gfm-tables": -1205584749,
     "org.commonmark:commonmark-ext-gfm-tables:jar:sources": 1341057091,
     "org.commonmark:commonmark:jar:sources": -1511261547,
-    "org.eclipse.jetty.ee10:jetty-ee10-servlet": -585403144,
-    "org.eclipse.jetty.ee10:jetty-ee10-servlet:jar:sources": 501419503,
+    "org.eclipse.jetty.ee11:jetty-ee11-servlet": -2443254,
+    "org.eclipse.jetty.ee11:jetty-ee11-servlet:jar:sources": -1546113901,
     "org.eclipse.jetty.ee8:jetty-ee8-nested": -1915352225,
     "org.eclipse.jetty.ee8:jetty-ee8-nested:jar:sources": 673934800,
     "org.eclipse.jetty.ee8:jetty-ee8-security": -1918552702,
@@ -1194,24 +1194,24 @@
     },
     "org.apache.sshd:sshd-mina": {
       "shasums": {
-        "jar": "7399961346882ef50952ae9774e8dd1eac7ce232d2f82c058ade2c20c02591f1",
-        "sources": "cc526f8a54d375e87b21419e1c3ce7462637718c6e692fce3940cff98a5151a5"
+        "jar": "da1088d16ba7953444b7d9aefcfc60c9b5e000bf26542d4c3ed0573f692bf14a",
+        "sources": "a8e548e09d8744effe88d83dcd55faa87dc14c37f8a0a595b4c6af3b20e558aa"
       },
-      "version": "2.18.0"
+      "version": "2.19.0"
     },
     "org.apache.sshd:sshd-osgi": {
       "shasums": {
-        "jar": "e771c4239b1c4818c102368d57d0cc082c224eefb76224524473bf7021d1a32f",
-        "sources": "dd5838716293c8aeaf9487c50c6b313dff304e7ddca84ce16923f73865ba4d1d"
+        "jar": "0b6b9371c6a97430390b84c23c58f4470927a747e74704a0ca7d19a09a66fe37",
+        "sources": "d11b69618dba155f075571cbd9b21741953c27dfa9a1af09b4024f2183226abe"
       },
-      "version": "2.18.0"
+      "version": "2.19.0"
     },
     "org.apache.sshd:sshd-sftp": {
       "shasums": {
-        "jar": "bf52fd81d63337cb40050c70ee460c80e78a0a4b77f15da7125eb1841f527e93",
-        "sources": "5171d9bac4ee76a013beccc1fdeab93fb43aeb84172d19b929ee158b9281c648"
+        "jar": "a3ad30d2feba9fad589c7ad9117fcd9d332823697d59eb424fe2902f4d805575",
+        "sources": "e26b7df972cead5c529bca6ab3e782c27dd43b323f9a54f98d9e23d4f5d03c30"
       },
-      "version": "2.18.0"
+      "version": "2.19.0"
     },
     "org.asciidoctor:asciidoctorj": {
       "shasums": {
@@ -1297,12 +1297,12 @@
       },
       "version": "0.24.0"
     },
-    "org.eclipse.jetty.ee10:jetty-ee10-servlet": {
+    "org.eclipse.jetty.ee11:jetty-ee11-servlet": {
       "shasums": {
-        "jar": "05cccc74bbe1478ceb765e967b1cb1a3c8d9b51e99acf586190887e467ffd87d",
-        "sources": "51a734b4bc04235b24ccd759515dd2947e5b675c227c556f02262b765bce47eb"
+        "jar": "f88e19a29f4e46322df60fe43d5ac4d852edefad923e8c38ef17ff14dc522e84",
+        "sources": "345f50f1a2937968934f354134cf4dd7e2b94bbfec36a177bb015ba675bd345c"
       },
-      "version": "12.1.10"
+      "version": "12.1.11"
     },
     "org.eclipse.jetty.ee8:jetty-ee8-nested": {
       "shasums": {
@@ -1809,7 +1809,7 @@
     "org.commonmark:commonmark-ext-gfm-tables": [
       "org.commonmark:commonmark"
     ],
-    "org.eclipse.jetty.ee10:jetty-ee10-servlet": [
+    "org.eclipse.jetty.ee11:jetty-ee11-servlet": [
       "jakarta.servlet:jakarta.servlet-api",
       "org.eclipse.jetty:jetty-security",
       "org.eclipse.jetty:jetty-server",
@@ -3775,14 +3775,14 @@
       "org.commonmark.ext.gfm.tables",
       "org.commonmark.ext.gfm.tables.internal"
     ],
-    "org.eclipse.jetty.ee10:jetty-ee10-servlet": [
-      "org.eclipse.jetty.ee10.servlet",
-      "org.eclipse.jetty.ee10.servlet.internal",
-      "org.eclipse.jetty.ee10.servlet.jmx",
-      "org.eclipse.jetty.ee10.servlet.listener",
-      "org.eclipse.jetty.ee10.servlet.security",
-      "org.eclipse.jetty.ee10.servlet.security.authentication",
-      "org.eclipse.jetty.ee10.servlet.util"
+    "org.eclipse.jetty.ee11:jetty-ee11-servlet": [
+      "org.eclipse.jetty.ee11.servlet",
+      "org.eclipse.jetty.ee11.servlet.internal",
+      "org.eclipse.jetty.ee11.servlet.jmx",
+      "org.eclipse.jetty.ee11.servlet.listener",
+      "org.eclipse.jetty.ee11.servlet.security",
+      "org.eclipse.jetty.ee11.servlet.security.authentication",
+      "org.eclipse.jetty.ee11.servlet.util"
     ],
     "org.eclipse.jetty.ee8:jetty-ee8-nested": [
       "org.eclipse.jetty.ee8.nested",
@@ -4548,8 +4548,8 @@
       "org.commonmark:commonmark-ext-gfm-tables",
       "org.commonmark:commonmark-ext-gfm-tables:jar:sources",
       "org.commonmark:commonmark:jar:sources",
-      "org.eclipse.jetty.ee10:jetty-ee10-servlet",
-      "org.eclipse.jetty.ee10:jetty-ee10-servlet:jar:sources",
+      "org.eclipse.jetty.ee11:jetty-ee11-servlet",
+      "org.eclipse.jetty.ee11:jetty-ee11-servlet:jar:sources",
       "org.eclipse.jetty.ee8:jetty-ee8-nested",
       "org.eclipse.jetty.ee8:jetty-ee8-nested:jar:sources",
       "org.eclipse.jetty.ee8:jetty-ee8-security",
@@ -4848,8 +4848,8 @@
       "org.commonmark:commonmark-ext-gfm-tables",
       "org.commonmark:commonmark-ext-gfm-tables:jar:sources",
       "org.commonmark:commonmark:jar:sources",
-      "org.eclipse.jetty.ee10:jetty-ee10-servlet",
-      "org.eclipse.jetty.ee10:jetty-ee10-servlet:jar:sources",
+      "org.eclipse.jetty.ee11:jetty-ee11-servlet",
+      "org.eclipse.jetty.ee11:jetty-ee11-servlet:jar:sources",
       "org.eclipse.jetty.ee8:jetty-ee8-nested",
       "org.eclipse.jetty.ee8:jetty-ee8-nested:jar:sources",
       "org.eclipse.jetty.ee8:jetty-ee8-security",
diff --git a/modules/jgit b/modules/jgit
index d29c264..bf0f0ad 160000
--- a/modules/jgit
+++ b/modules/jgit
@@ -1 +1 @@
-Subproject commit d29c264b2983782418b4c8357a6422750edfa15c
+Subproject commit bf0f0ad1cc2ce422604383272a4a4b8b3947b465
diff --git a/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog.ts b/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog.ts
index 95df2fac..f808b1c 100644
--- a/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog.ts
+++ b/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog.ts
@@ -168,6 +168,7 @@
           .commands=${this.computeDownloadCommands()}
           .schemes=${this.schemes}
           .selectedScheme=${this.selectedScheme}
+          .disableAutoSelect=${true}
           show-keyboard-shortcut-tooltips
           @selected-scheme-changed=${(e: BindValueChangeEvent) => {
             this.selectedScheme = e.detail.value;
diff --git a/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog_test.ts b/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog_test.ts
index c107c20..438e151 100644
--- a/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog_test.ts
+++ b/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog_test.ts
@@ -122,6 +122,7 @@
       </section>
       <section class="hidden">
         <gr-download-commands
+          disable-auto-select=""
           id="downloadCommands"
           show-keyboard-shortcut-tooltips=""
         >
@@ -234,6 +235,14 @@
       await element.updateComplete;
     });
 
+    test('passes disableAutoSelect to gr-download-commands', () => {
+      const commands = queryAndAssert<GrDownloadCommands>(
+        element,
+        '#downloadCommands'
+      );
+      assert.isTrue(commands.disableAutoSelect);
+    });
+
     test('focuses on first copy link', async () => {
       const focusStub = sinon.stub(
         queryAndAssert<GrDownloadCommands>(element, '#downloadCommands'),
diff --git a/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread.ts b/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread.ts
index 53811e9..3020bff 100644
--- a/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread.ts
+++ b/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread.ts
@@ -355,6 +355,7 @@
       sharedStyles,
       css`
         :host {
+          display: block;
           font-family: var(--font-family);
           font-size: var(--font-size-normal);
           font-weight: var(--font-weight-normal);
@@ -722,10 +723,19 @@
   }
 
   override firstUpdated() {
+    const lastComment = this.thread ? this.getLastComment() : undefined;
+    const isNewDraft =
+      isDraft(lastComment) && (lastComment?.message ?? '') === '';
     if (this.shouldScrollIntoView) {
       whenRendered(this, () => {
         this.expandCollapseComments(false);
-        this.commentBox?.focus();
+        // Because of the non-deterministic order of focus events firing from
+        // the JS event loop, focusing the comment box on a new draft can result
+        // in the draft comment not being focused, which means the user has to
+        // click into it to start typing.
+        if (!isNewDraft) {
+          this.commentBox?.focus();
+        }
         // The delay is a hack because we don't know exactly when to
         // scroll the comment into center.
         // TODO: Find a better solution without a setTimeout
@@ -735,9 +745,9 @@
         }, 500);
       });
     }
-    if (this.thread && isDraft(this.getFirstComment())) {
-      const msg = this.getFirstComment()?.message ?? '';
-      if (msg.length === 0) this.editDraft();
+    // Focus the draft comment input to avoid the user having to click into it.
+    if (isNewDraft) {
+      this.editDraft();
     }
   }
 
diff --git a/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread_test.ts b/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread_test.ts
index 816054b..8b5e166 100644
--- a/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread_test.ts
@@ -173,6 +173,36 @@
     );
   });
 
+  test('focuses commentBox when it is NOT a new draft', async () => {
+    const thread = createThread(c1);
+    const element = await fixture<GrCommentThread>(html`
+      <gr-comment-thread
+        .thread=${thread}
+        .shouldScrollIntoView=${true}
+      ></gr-comment-thread>
+    `);
+    await element.updateComplete;
+    await new Promise(resolve => setTimeout(resolve, 0));
+
+    const commentBox = queryAndAssert<HTMLElement>(element, '.comment-box');
+    assert.equal(element.shadowRoot?.activeElement, commentBox);
+  });
+
+  test('does not focus commentBox when it IS a new draft', async () => {
+    const thread = createThread(createNewDraft({message: ''}));
+    const element = await fixture<GrCommentThread>(html`
+      <gr-comment-thread
+        .thread=${thread}
+        .shouldScrollIntoView=${true}
+      ></gr-comment-thread>
+    `);
+    await element.updateComplete;
+    await new Promise(resolve => setTimeout(resolve, 0));
+
+    const commentBox = queryAndAssert<HTMLElement>(element, '.comment-box');
+    assert.notEqual(element.shadowRoot?.activeElement, commentBox);
+  });
+
   test('comment box spans 100% of container width', async () => {
     const wrapper = await fixture(html`
       <div style="width: 500px;">
@@ -518,6 +548,85 @@
       // The draft should be discarded completely
       assert.equal(draftElement.messageText, '');
     });
+
+    test('handle Quote with multi-line message', async () => {
+      stubAdd.restore();
+      stubAdd = sinon
+        .stub(testResolver(commentsModelToken), 'addNewDraft')
+        .callsFake(draft => {
+          const newDraft = {
+            ...draft,
+            id: 'new-draft' as UrlEncodedCommentId,
+            __draft: true,
+          };
+          if (element.thread) {
+            element.thread = {
+              ...element.thread,
+              comments: [...element.thread.comments, newDraft],
+            };
+          }
+          return Promise.resolve(newDraft);
+        });
+
+      element.thread = createThread(c1, {
+        ...c2,
+        message: 'first line\nsecond line\nthird line',
+        unresolved: true,
+      });
+      await element.updateComplete;
+
+      queryAndAssert<GrButton>(element, '#quoteBtn').click();
+      assert.isTrue(stubAdd.called);
+      assert.equal(stubAdd.lastCall.firstArg.in_reply_to, c2.id);
+      await element.updateComplete;
+
+      const draftElement = queryAndAssert<GrComment>(
+        element,
+        'gr-comment.draft'
+      );
+      await draftElement.updateComplete;
+      await waitUntil(
+        () =>
+          draftElement.messageText ===
+          '> first line\n> second line\n> third line\n\n'
+      );
+      assert.equal(
+        draftElement.messageText,
+        '> first line\n> second line\n> third line\n\n'
+      );
+    });
+
+    test('handle reply-to-comment event from child comment', async () => {
+      element.thread = createThread(c1, {...c2, unresolved: true});
+      await element.updateComplete;
+
+      const commentEl = queryAndAssert<GrComment>(element, 'gr-comment');
+      commentEl.dispatchEvent(
+        new CustomEvent('reply-to-comment', {
+          detail: {
+            content: 'custom response',
+            userWantsToEdit: true,
+            unresolved: true,
+          },
+          bubbles: true,
+          composed: true,
+        })
+      );
+
+      assert.isTrue(stubAdd.called);
+      assert.equal(stubAdd.lastCall.firstArg.in_reply_to, c2.id);
+      assert.equal(stubAdd.lastCall.firstArg.unresolved, true);
+    });
+
+    test('reply sets in_reply_to to the last comment id in thread', async () => {
+      element.thread = createThread(c1, c2);
+      await element.updateComplete;
+
+      queryAndAssert<GrButton>(element, '#replyBtn').click();
+      assert.isTrue(stubAdd.called);
+      const newDraft = stubAdd.lastCall.firstArg;
+      assert.equal(newDraft.in_reply_to, c2.id);
+    });
   });
 
   test('comments are sorted correctly', () => {
diff --git a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.ts b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.ts
index 418ddb0..24ca939 100644
--- a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.ts
+++ b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.ts
@@ -1381,11 +1381,9 @@
   override updated(changed: PropertyValues) {
     if (changed.has('editing')) {
       if (this.editing && !this.permanentEditingMode) {
-        // Note that this is a bit fragile, because we are relying on the
-        // comment to become visible soonish. If that does not happen, then we
-        // will be waiting indefinitely and grab focus at some point in the
-        // distant future.
-        whenVisible(this, () => this.textarea?.putCursorAtEnd());
+        this.focusTextarea().catch(() => {
+          // Ignore error since failure to focus is non-fatal.
+        });
       }
     }
     if (changed.has('changeNum') || changed.has('comment')) {
@@ -1400,6 +1398,27 @@
     }
   }
 
+  private async focusTextarea(): Promise<void> {
+    await this.updateComplete;
+    if (!this.textarea) {
+      return;
+    }
+    await this.textarea.updateComplete;
+    if (this.isVisible()) {
+      this.textarea.putCursorAtEnd();
+    } else {
+      // Note that this is a bit fragile, because we are relying on the
+      // comment to become visible soonish. If that does not happen, then we
+      // will be waiting indefinitely and grab focus at some point in the
+      // distant future.
+      whenVisible(this, () => this.textarea?.putCursorAtEnd());
+    }
+  }
+
+  private isVisible(): boolean {
+    return this.offsetWidth > 0 || this.offsetHeight > 0;
+  }
+
   override willUpdate(changed: PropertyValues) {
     this.firstWillUpdate();
     if (changed.has('comment')) {
diff --git a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment_test.ts b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment_test.ts
index 58a64bd..b437a74 100644
--- a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment_test.ts
@@ -54,6 +54,7 @@
 import {suggestionsServiceToken} from '../../../services/suggestions/suggestions-service';
 import {AutocompletionContext} from '../../../utils/autocomplete-cache';
 import {FixSuggestionInfo} from '../../../api/rest-api';
+import {GrSuggestionTextarea} from '../gr-suggestion-textarea/gr-suggestion-textarea';
 
 suite('gr-comment tests', () => {
   let element: GrComment;
@@ -533,6 +534,24 @@
       assert.isTrue(element.isSaveDisabled());
     });
 
+    test('focuses textarea when editing is set to true', async () => {
+      const spy = sinon.spy(GrSuggestionTextarea.prototype, 'putCursorAtEnd');
+      try {
+        element.comment = createDraft();
+        element.editing = false;
+        await element.updateComplete;
+
+        element.editing = true;
+        await element.updateComplete;
+        // focusTextarea is async, wait for it to complete.
+        await new Promise(resolve => setTimeout(resolve, 0));
+
+        assert.isTrue(spy.called);
+      } finally {
+        spy.restore();
+      }
+    });
+
     test('ctrl+s saves comment', async () => {
       const spy = sinon.stub(element, 'save');
       element.messageText = 'is that the horse from horsing around??';
diff --git a/polygerrit-ui/app/elements/shared/gr-copy-clipboard/gr-copy-clipboard.ts b/polygerrit-ui/app/elements/shared/gr-copy-clipboard/gr-copy-clipboard.ts
index 0ee4aa3..bfc6462 100644
--- a/polygerrit-ui/app/elements/shared/gr-copy-clipboard/gr-copy-clipboard.ts
+++ b/polygerrit-ui/app/elements/shared/gr-copy-clipboard/gr-copy-clipboard.ts
@@ -52,6 +52,9 @@
   @property({type: Boolean, reflect: true})
   hideInput = false;
 
+  @property({type: Boolean, attribute: 'disable-auto-select', reflect: true})
+  disableAutoSelect = false;
+
   @property({type: String})
   label?: string;
 
@@ -216,6 +219,7 @@
   }
 
   private handleInputClick(e: MouseEvent) {
+    if (this.disableAutoSelect) return;
     e.preventDefault();
     const rootTarget = e.composedPath()[0];
     (rootTarget as HTMLInputElement).select();
diff --git a/polygerrit-ui/app/elements/shared/gr-copy-clipboard/gr-copy-clipboard_test.ts b/polygerrit-ui/app/elements/shared/gr-copy-clipboard/gr-copy-clipboard_test.ts
index b50ecfd..acc1180 100644
--- a/polygerrit-ui/app/elements/shared/gr-copy-clipboard/gr-copy-clipboard_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-copy-clipboard/gr-copy-clipboard_test.ts
@@ -128,6 +128,22 @@
     assert.equal(mdOutlinedTextField.selectionEnd, element.text!.length - 1);
   });
 
+  test('handleInputClick with disableAutoSelect', async () => {
+    element.disableAutoSelect = true;
+    await element.updateComplete;
+
+    const mdOutlinedTextField = queryAndAssert<MdOutlinedTextField>(
+      element,
+      'md-outlined-text-field'
+    );
+    mdOutlinedTextField.selectionStart = 0;
+    mdOutlinedTextField.selectionEnd = 0;
+
+    mdOutlinedTextField.click();
+    assert.equal(mdOutlinedTextField.selectionStart, 0);
+    assert.equal(mdOutlinedTextField.selectionEnd, 0);
+  });
+
   test('hideInput', async () => {
     const mdOutlinedTextField = queryAndAssert<MdOutlinedTextField>(
       element,
diff --git a/polygerrit-ui/app/elements/shared/gr-download-commands/gr-download-commands.ts b/polygerrit-ui/app/elements/shared/gr-download-commands/gr-download-commands.ts
index 9a7cabc..30defc5 100644
--- a/polygerrit-ui/app/elements/shared/gr-download-commands/gr-download-commands.ts
+++ b/polygerrit-ui/app/elements/shared/gr-download-commands/gr-download-commands.ts
@@ -54,6 +54,9 @@
   @property({type: Boolean, attribute: 'show-keyboard-shortcut-tooltips'})
   showKeyboardShortcutTooltips = false;
 
+  @property({type: Boolean, attribute: 'disable-auto-select', reflect: true})
+  disableAutoSelect = false;
+
   // Private but used in tests.
   readonly getUserModel = resolve(this, userModelToken);
 
@@ -171,6 +174,7 @@
         .label=${command.title}
         .command=${command.command}
         .tooltip=${this.computeTooltip(index)}
+        .disableAutoSelect=${this.disableAutoSelect}
       ></gr-shell-command>
     `;
   }
diff --git a/polygerrit-ui/app/elements/shared/gr-download-commands/gr-download-commands_test.ts b/polygerrit-ui/app/elements/shared/gr-download-commands/gr-download-commands_test.ts
index 05011f8f..3f3b4bc 100644
--- a/polygerrit-ui/app/elements/shared/gr-download-commands/gr-download-commands_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-download-commands/gr-download-commands_test.ts
@@ -97,6 +97,16 @@
       assert.isTrue(focusStub.called);
     });
 
+    test('passes disableAutoSelect to gr-shell-command', async () => {
+      element.disableAutoSelect = true;
+      await element.updateComplete;
+      const shellCommand = queryAndAssert<GrShellCommand>(
+        element,
+        'gr-shell-command'
+      );
+      assert.isTrue(shellCommand.disableAutoSelect);
+    });
+
     test('element visibility', async () => {
       assert.isFalse(isHidden(queryAndAssert(element, 'md-tabs')));
       assert.isFalse(isHidden(queryAndAssert(element, '.commands')));
diff --git a/polygerrit-ui/app/elements/shared/gr-shell-command/gr-shell-command.ts b/polygerrit-ui/app/elements/shared/gr-shell-command/gr-shell-command.ts
index 0c9be74..54aecb3 100644
--- a/polygerrit-ui/app/elements/shared/gr-shell-command/gr-shell-command.ts
+++ b/polygerrit-ui/app/elements/shared/gr-shell-command/gr-shell-command.ts
@@ -27,6 +27,9 @@
   @property({type: String})
   tooltip = '';
 
+  @property({type: Boolean, attribute: 'disable-auto-select', reflect: true})
+  disableAutoSelect = false;
+
   static override get styles() {
     return [
       sharedStyles,
@@ -73,6 +76,7 @@
           .text=${this.command}
           hasTooltip
           buttonTitle=${this.tooltip}
+          .disableAutoSelect=${this.disableAutoSelect}
         ></gr-copy-clipboard>
       </div>`;
   }
diff --git a/polygerrit-ui/app/elements/shared/gr-shell-command/gr-shell-command_test.ts b/polygerrit-ui/app/elements/shared/gr-shell-command/gr-shell-command_test.ts
index 1e0ff11..8fb66f5 100644
--- a/polygerrit-ui/app/elements/shared/gr-shell-command/gr-shell-command_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-shell-command/gr-shell-command_test.ts
@@ -44,4 +44,14 @@
     await element.focusOnCopy();
     assert.isTrue(focusStub.called);
   });
+
+  test('passes disableAutoSelect to gr-copy-clipboard', async () => {
+    element.disableAutoSelect = true;
+    await element.updateComplete;
+    const copyClipboard = queryAndAssert<GrCopyClipboard>(
+      element,
+      'gr-copy-clipboard'
+    );
+    assert.isTrue(copyClipboard.disableAutoSelect);
+  });
 });
diff --git a/tools/nongoogle.toml b/tools/nongoogle.toml
index 701c4bc..1f2e24a 100644
--- a/tools/nongoogle.toml
+++ b/tools/nongoogle.toml
@@ -12,7 +12,7 @@
 guice = "6.0.0"
 lucene = "10.4.0"
 slf4j = "2.0.18"
-sshd = "2.18.0"
+sshd = "2.19.0"
 truth = "1.4.4"
 
 [libraries]