From bd03b17a6f10ad66c4d0c78fc5c60c76db900a70 Mon Sep 17 00:00:00 2001 From: Peter Gaultney Date: Sat, 19 Sep 2026 10:09:50 -0400 Subject: [PATCH 1/2] fix: keep the sidebar scroll position steady when a thread is selected Selecting a thread, from a card or from its anchor in the note, rebuilds the sidebar with root.empty(), which collapses the scroller and resets scrollTop to 0. The rebuilt list then either stayed at the top (the lastScrolledItemId guard skipped the re-scroll when the same card was selected twice in one click) or parked the card at the bottom edge of the viewport (scrollIntoView "nearest" from 0). Restore scrollTop after the rebuild so the viewport only moves when the selected card is actually out of view. Scroll to the card the user picked, not the first selected-looking card in document order: a card under the editor cursor also renders as selected, and used to win the scroll target. --- src/ui/ReviewSidebarView.ts | 36 +++++++++++++++++++++++------------- 1 file changed, 23 insertions(+), 13 deletions(-) diff --git a/src/ui/ReviewSidebarView.ts b/src/ui/ReviewSidebarView.ts index d37f8f2..a5605fa 100644 --- a/src/ui/ReviewSidebarView.ts +++ b/src/ui/ReviewSidebarView.ts @@ -119,7 +119,7 @@ export class ReviewSidebarView extends ItemView { private draftText = ""; private pendingFocus: PendingFocus | null = null; private lastDraftKey: string | null = null; - private lastScrolledItemId: string | null = null; + private removeOutsideClickListener: (() => void) | null = null; private composerSubmits = new WeakMap void>(); private composerHintId = 0; @@ -325,22 +325,31 @@ export class ReviewSidebarView extends ItemView { private renderContent(): void { const root = this.contentEl; + // Emptying the scroller collapses it and zeroes scrollTop; put it back + // once the new content exists so a rebuild doesn't move the viewport. + const scrollTop = root.scrollTop; this.resetCommentRenderScope(); root.empty(); root.addClass("critic-sidebar"); + const selectedId = this.renderBody(root); + root.scrollTop = scrollTop; + this.scrollSelectedIntoView(root, selectedId); + } + /** Returns the id of the visibly selected card, if any. */ + private renderBody(root: HTMLElement): string | null { const externalState = this.plugin.getActiveExternalCommentState(); if (externalState) { this.renderHeader(root, externalState.title); this.renderExternalComments(root, externalState); - return; + return null; } const state = this.plugin.getActiveReviewState(); this.renderHeader(root, state?.file.basename ?? "Relay Comments"); if (!state) { this.renderEmptyState(root, "Open a Markdown note to review comments and suggestions."); - return; + return null; } const validMarks = state.marks @@ -371,7 +380,7 @@ export class ReviewSidebarView extends ItemView { "No comments or suggestions in this note yet. Select text and choose “Add comment” to start a discussion.", ); } - return; + return null; } const list = root.createDiv({ cls: "critic-sidebar-list" }); @@ -391,14 +400,16 @@ export class ReviewSidebarView extends ItemView { isSelected(item, state.activeMarkId), state.file.path, ); - if (selected && !visiblySelectedId) { + // The card the user picked wins over the one the editor cursor + // happens to sit in; both render as selected. + if (selected && (!visiblySelectedId || item.id === this.selectedItemId)) { visiblySelectedId = item.id; } } if (draft && !draftRendered) { this.renderDraft(list, draft); } - this.scrollSelectedIntoView(list, visiblySelectedId); + return visiblySelectedId; } private renderExternalComments( @@ -575,15 +586,14 @@ export class ReviewSidebarView extends ItemView { } private scrollSelectedIntoView( - list: HTMLElement, + root: HTMLElement, selectedId: string | null, ): void { - if (selectedId && selectedId !== this.lastScrolledItemId) { - list - .querySelector(`[data-critic-item-id="${CSS.escape(selectedId)}"]`) - ?.scrollIntoView({ block: "nearest" }); - } - this.lastScrolledItemId = selectedId; + if (!selectedId) return; + + root + .querySelector(`[data-critic-item-id="${CSS.escape(selectedId)}"]`) + ?.scrollIntoView({ block: "nearest" }); } private installOutsideClickListener(): void { From 4ba96b22e97b07f10cbf3a9a51c48c455fc383ee Mon Sep 17 00:00:00 2001 From: Peter Gaultney Date: Sat, 19 Sep 2026 10:10:05 -0400 Subject: [PATCH 2/2] feat: mark the picked thread card with the accent color Two cards can render as selected at once: the one the user clicked (or reached from its anchor in the note) and the one under the editor cursor. Give the picked card an accent-colored spine and border so it stands apart from the cursor's card. --- src/ui/ReviewSidebarView.ts | 8 +++++--- styles.css | 9 +++++++++ 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/src/ui/ReviewSidebarView.ts b/src/ui/ReviewSidebarView.ts index a5605fa..cd7a973 100644 --- a/src/ui/ReviewSidebarView.ts +++ b/src/ui/ReviewSidebarView.ts @@ -707,9 +707,11 @@ export class ReviewSidebarView extends ItemView { const isThreadSelected = selected || this.selectedItemId === item.id || this.replyDraftItemId === item.id; const card = parent.createDiv({ - cls: isThreadSelected - ? "critic-card critic-card-selected" - : "critic-card", + cls: [ + "critic-card", + ...(isThreadSelected ? ["critic-card-selected"] : []), + ...(this.selectedItemId === item.id ? ["critic-card-current"] : []), + ], attr: { tabindex: "0", role: "button", diff --git a/styles.css b/styles.css index 7234544..b33952e 100644 --- a/styles.css +++ b/styles.css @@ -391,6 +391,15 @@ box-shadow: 0 3px 10px rgba(0, 0, 0, 0.14); } +/* The card the user picked (clicked, or reached from its anchor in the note) + takes the accent on its spine and border. A card that is merely under the + editor cursor keeps the softer selected look. */ +.critic-card.critic-card-current, +.critic-card.critic-card-current:hover { + --critic-rail: var(--interactive-accent); + border-color: var(--interactive-accent); +} + /* ---------- Card header ---------- */ .critic-comment-header {