feat: let one block carry several comment threads - #70
Merged
Conversation
A click on a block now always targets a NEW thread, so a second remark about the same paragraph or line range no longer has to be appended to the first. Threads on one anchor resolve independently and keep their own cards, which is what a reply cannot give: a reply shares its parent's status, so a typo cannot be closed while the design question stays open. Reading them back moves to the affordances that can name what they point at. The indicator selects every thread on its anchor and draws one line per card -- it is one chip for all of them, so it cannot name one; a card selects itself alone. A resolved thread keeps its line, drawn faint, so the lines and the count on the chip agree. Underneath was a latent bug the single-comment case hid: a card was resolved from an anchor with querySelector, which silently returned the first of N and dropped the rest. Both kinds of review are covered. In a diff, selecting the same line range again anchors the new thread to the same lines; ranges that merely overlap stay separate anchors, which is left as it was. User request: 人間も1ブロックに複数のコメントを付けられるようにし、複数ついて いる場合にどの comment card を選択するか選べるようにしたい。選択方式は4案の プロトタイプを見比べて「インジケータで束選択」を採用。resolved も束に含め、 connector line の色を薄くする。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The comment indicator on a diff line is stuck to the right edge of the hunk, which is the box that scrolls horizontally. A transformed box counts toward its scroll container's scrollable overflow, so the hover lift reached about a pixel past the padding edge and raised a horizontal scrollbar: hovering the chip resized the diff under the pointer. Measured before and after the pointer moved onto it, the hunk went from 575/575 to 576/575. The lift is dropped here and only here. transform-origin: right center would grow it inwards, but it still grows vertically and would raise the hunk's other scrollbar instead. The chip already changes background on hover everywhere, so the hover still reads. In a Markdown review the chip hangs in a gutter with room around it and keeps the lift, which the same measurement confirms. User request: diff viewer で comment indicator にマウスオーバーすると ドキュメント部分の横幅が広がり、横スクロールバーが現れるのが気になる。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Hovering a commented Markdown block has always previewed its connector dashed. A diff never did -- the handlers were only ever bound to the Markdown blocks and to the panel's cards -- and there was no reason for the two modes to differ. A commented line, any line of a commented range, and a commented file header all preview now. Delegated from the column rather than bound per line: a diff runs to thousands of lines, and their anchors are attributes applyDiffAnchors rewrites every round, so there is nothing stable to bind to. mouseover/mouseout are used because they bubble, guarded so that crossing a child of the same line does not repaint the overlay. The rule that a preview is suppressed while anything is selected or being composed is left alone, and now says why in the code: the overlay carries one statement about what the page is pointing at, and a preview across the compose line would leave the composer's own target ambiguous. It matters more than it did, because clicking any block now enters the compose state and stays there. User request: comment indicator にマウスオーバーしたときの破線が出ないが 意図的か。diff にもプレビューを付け、compose 中の抑止は維持する。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Selecting a comment waited 320ms for the smooth scroll to land before painting the line, and only then started the 0.32s draw-on animation. Half a second of nothing between the click and any response reads as the page having missed the click. The wait was never needed. The scroll handler re-attaches the line on every frame, so it can be drawn against where things are now and travel with them. Measured from the click to the first painted path: 3ms. The draw-on animation is still asked for; a scroll simply overtakes it, which is the right trade when the alternative is waiting to see anything at all. User request: comment indicator をクリックしてから connector line が実線で 表示されるまで0.5秒ほどかかる。即座に表示できないか。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This was referenced Aug 30, 2026
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why this change is necessary
From the human side a block could only ever hold one comment thread. Clicking a commented paragraph — or a commented diff line — selected the thread already there, so no gesture was left to start a second one. Two unrelated remarks about the same paragraph had to share a single thread, and a thread carries one status: the typo could not be closed while the design question stayed open.
The page could already end up holding several threads on one anchor, because an agent opens its own threads by quoting a passage. It just had no way to tell them apart: a card was resolved from an anchor with
querySelector, which silently returned the first of N and dropped the rest. So the count on the indicator could read3while only one card was ever selected or drawn.Approach
Clicking the text always composes a new thread now, and reading the existing ones moves to the two affordances that can actually point at them: the comment indicator selects every thread on its anchor, and a comment card selects itself alone. The indicator is one chip for all of them, so it cannot name one — it lights the whole bunch and draws a line per card. A resolved thread stays in the bunch with its line drawn faint, so the lines and the count on the chip never disagree.
The four candidate selection models were built as a working prototype and compared in a browser rather than argued about on paper, which is how the bunch won: cycling through threads one click at a time needs no new UI but never says what is coming next, and a popover is a new component with new state to own.
Three defects surfaced while actually using it, and each is its own commit: hovering a diff chip raised a horizontal scrollbar on the hunk (a transformed box counts toward scrollable overflow, and the chip is docked to the scrollport's right edge); a diff never previewed a connector on hover, though a Markdown block always had; and selecting a comment waited 320 ms for the smooth scroll to land before painting anything, which reads as a missed click.
Both kinds of review are covered. In a diff, selecting the same line range again anchors the new thread to the same lines.
Big picture
図のソース / Diagram source (mermaid)
flowchart TD subgraph DocCol["Document column"] DOC["Rendered Markdown spec or diff"] ANCHOR["Anchor: Markdown block, diff line range, or whole file"] IND["Comment indicator chip: thread count"] end subgraph Panel["Feedback panel"] CARDS["Comment cards: one card per thread"] COMPOSER["Composer: draft a new thread"] end OVERLAY["Connector overlay: solid = selection, dashed = hover, faint = resolved"] DOC --> ANCHOR ANCHOR -->|"was 1 thread, now N"| THREADS["Threads on one anchor"] THREADS --> IND THREADS --> CARDS G1["Gesture: click block or diff line"] -->|"now ALWAYS a new thread"| COMPOSER G2["Gesture: click indicator chip"] --> SELALL["Select the whole bunch"] G3["Gesture: click a comment card"] --> SELONE["Select that one thread"] G4["Gesture: hover block, diff line, or file header"] --> HOVER["Hover preview"] SELALL -->|"one line per card"| OVERLAY SELONE -->|"one solid line"| OVERLAY HOVER -->|"dashed line, new for diff"| OVERLAY RESOLVED["Resolved thread"] -->|"faint line"| OVERLAY OVERLAY -.-> CARDS classDef changed fill:#fff3e0,stroke:#e8710a,stroke-width:3px,color:#3d2400; class THREADS,IND,G1,G2,G4,SELALL,HOVER,RESOLVED changed;Design Documents
The rationale lives in the repository's own documents, updated in the same commits:
UI_DESIGN.md§5.1 / §5.3 / §6.2 for the interaction decisions and the alternatives turned down,GLOSSARY.mdfor the vocabulary, andDESIGN.md§3 for the constraint underneath — an anchor identifies a target, not a comment.The requirements were settled in a structured interview before any code was written. That record is not committed, so it is reproduced here as it stood at the time — the implementation departs from it in one detail, keying a single-card selection by the comment's index rather than by an id, because a comment written this round has no server-assigned id yet.
deep-interview-multiple-comments-per-block.md (click to expand)
Deep Interview Spec: 1ブロックに複数コメント + 束選択
Metadata
di-2026-08-30-multiple-commentsClarity Breakdown
Goal
人間が1つのアンカー(Markdown のブロック / diff の行範囲・ファイル)に対して複数の独立したコメントスレッドを作れるようにし、
インジケータのクリックでそのアンカーの全スレッドをまとめて選択できるようにする。
現状、1アンカーに複数スレッドが存在し得るのはエージェントが
review_replyで quote からスレッドを開いたときだけで、人間の UI には2本目を作る導線がない。またその状態になっても、選択はアンカー単位・カードは先頭1枚しか解決されない
(
commentCard()がquerySelectorで最初の1枚を返す)。操作モデル(確定)
isCommentedによる分岐を廃止束選択時:
stroke-opacityを落とす。第二の色相は使わない)Constraints
--accent-colorのままstroke-opacityで区別する(UI_DESIGN §2.1)commentsInAppearanceOrder()の「未解決が先、resolved が後」(resolvedRank)は維持する。結果として束が上下に分断され、線が離れた2箇所へ伸びることは許容する
(
allResolved = !openByAnchor[anchorId]が「個別 resolve」の要件をそのまま満たす)a#3-8とa#5-6)は今回扱わない。別アンカーのまま、行の
data-anchorは後勝ちという現行挙動を維持するCommentは既に同一Anchorの重複を許し、mergeFeedback/reAnchorCommentsもアンカーの一意性に依存していない。依存していないことの確認は実装時に行う
Non-Goals
data-anchor/data-anchor-memberの単一値前提の破壊)review_reply) の変更Acceptance Criteria
renderComments→reconcileConnector)も、消えたカードがあれば束から落ちるだけで例外にならないAssumptions Exposed & Resolved
Technical Context (brownfield)
変更が要る箇所(すべて
references/template.html):1811-1821isCommented(anchorId) ? selectComment : setCommentTargetの分岐を廃し、常にsetCommentTarget2091-21102401commentCard(anchor)commentCards(anchor)へ。querySelectorAll+ フィルタ2554selectComment(anchor).selected、全ターゲットに.selected-target、線を N 本connector = {kind, anchor}を{kind, anchor, commentId?}に拡張2428paintConnector/2485drawConnector/2501refreshConnectorstroke-opacityを落とす2583reconcileConnector2352scrollToCommentCard1295#connectorOverlay path(CSS)変更不要が確認済みの箇所:
2281updateCommentIndicators()— 既にアンカー単位で件数を数え、allResolvedも未解決の有無で判定している2655commentsInAppearanceOrder()— 同一アンカーはソート安定性により作成順で隣接する(resolved を除く)1884applyDiffAnchors()— アンカーの重複はanchors.includesで既に排除済み参照プロトタイプ(この会話で作成、リポジトリ外):
/private/tmp/claude-501/.../scratchpad/multi-comment-proto.html(案 C が採用案)Ontology (Key Entities)
spec-element-N/path#start-end/path#fileOntology Convergence
Interview Transcript
Full Q&A (5 rounds)
Round 1 — Goal Clarity
Q: 既にコメントが付いているブロックに、人間が2本目のコメントを始める操作は何か
A: クリック=常に新規作成(indicator と card が選択を担う)
Ambiguity: 55%
Round 2 — Goal Clarity(プロトタイプ提示)
Q: N 本あるとき indicator のクリックで何が起きるべきか(A 順送り / B ポップオーバー / C まとめて選択 / D パネル側だけ)
A: C まとめて選択
Ambiguity: 40%
Round 3 — Constraint Clarity
Q: 束に resolved を含めるか
A: 含める。ただし resolved の connector line は薄い色にする
Ambiguity: 33%
Round 4 — Success Criteria(Contrarian Mode)
Q: 返信機能があるのに別スレッドを立てる実利は何か
A: 話題の分離(可読性) + 個別に resolve できる
Ambiguity: 23%
Round 5 — Constraint Clarity
Q: diff を今回のスコープに含めるか。行範囲の部分的な重なりまで解くか
A: 両モード対応。重なりは後回し
Ambiguity: 17% ✅
Review Points
a#3-8anda#5-6) are deliberately out of scope. They stay separate anchors and a line'sdata-anchoris still last-write-wins, so only one of two overlapping ranges highlights. Several comments per range makes this easier to run into, so it is worth a follow-up.