Skip to content

feat: let one block carry several comment threads - #70

Merged
handlename merged 4 commits into
mainfrom
feat/multiple-comments-per-block
Aug 30, 2026
Merged

feat: let one block carry several comment threads#70
handlename merged 4 commits into
mainfrom
feat/multiple-comments-per-block

Conversation

@handlename

Copy link
Copy Markdown
Owner

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 read 3 while 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

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;
Loading

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.md for the vocabulary, and DESIGN.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ブロックに複数コメント + 束選択

このファイルはインタビューの記録であり、コミット対象ではない(CLAUDE.md「agent が作成したドキュメントの無断コミット禁止」)。
内容を残す場合は Issue / PR の本文に転記すること。
なお、実際に触るコード内コメント・DESIGN.md / UI_DESIGN.md / GLOSSARY.md は既存に合わせて 英語 で書くこと。

Metadata

  • Interview ID: di-2026-08-30-multiple-comments
  • Rounds: 5
  • Final Ambiguity Score: 17%
  • Type: brownfield
  • Generated: 2026-08-30
  • Threshold: 20%
  • Status: PASSED

Clarity Breakdown

Dimension Score Weight Weighted
Goal Clarity 0.92 0.35 0.322
Constraint Clarity 0.85 0.25 0.213
Success Criteria 0.65 0.25 0.163
Context Clarity 0.90 0.15 0.135
Total Clarity 0.832
Ambiguity 0.168

Goal

人間が1つのアンカー(Markdown のブロック / diff の行範囲・ファイル)に対して複数の独立したコメントスレッドを作れるようにし、
インジケータのクリックでそのアンカーの全スレッドをまとめて選択できるようにする。

現状、1アンカーに複数スレッドが存在し得るのはエージェントが review_reply で quote からスレッドを開いたときだけで、
人間の UI には2本目を作る導線がない。またその状態になっても、選択はアンカー単位・カードは先頭1枚しか解決されない
(commentCard()querySelector で最初の1枚を返す)。

操作モデル(確定)

ジェスチャ 挙動
ブロック本文 / diff 行のクリック 常に composer のターゲット指定(新規スレッド作成)。isCommented による分岐を廃止
comment indicator のクリック そのアンカーの全スレッドを束選択。再クリックで解除(既存のトグル挙動を維持)
comment card のクリック そのカード1枚だけを選択(単独選択)
ブロックの hover 既存どおり、コメントがあれば connector をプレビュー(束)

束選択時:

  • そのアンカーの全スレッド(resolved / folded も含む)を選択状態にする
  • connector line は選択された枚数だけ引く
  • resolved のスレッドへ引く線は薄くする(stroke-opacity を落とす。第二の色相は使わない)
  • パネルのスクロール先は束の先頭カード(= パネル上で最初に現れる1枚)

Constraints

  • 色相を増やさない。resolved の線は --accent-color のまま stroke-opacity で区別する(UI_DESIGN §2.1)
  • パネルの並び順は変えないcommentsInAppearanceOrder() の「未解決が先、resolved が後」(resolvedRank)は維持する。
    結果として束が上下に分断され、線が離れた2箇所へ伸びることは許容する
  • インジケータの数字の定義(そのアンカーの outdated でない全スレッド数)は変えない。束の中身と数字は一致する
  • インジケータのアイコン切替規則も変えない — 未解決が1本でも残れば 💬、全部 resolved なら ✓
    (allResolved = !openByAnchor[anchorId] が「個別 resolve」の要件をそのまま満たす)
  • Markdown と diff の両モードを同時に対応する。規則を2つに分裂させない
  • diff の部分的に重なる行範囲(a#3-8a#5-6)は今回扱わない。別アンカーのまま、
    行の data-anchor は後勝ちという現行挙動を維持する
  • Go 側は変更不要の見込み。Comment は既に同一 Anchor の重複を許し、mergeFeedback / reAnchorComments
    アンカーの一意性に依存していない。依存していないことの確認は実装時に行う

Non-Goals

  • 行が複数アンカーに属せるようにするデータ構造変更(data-anchor / data-anchor-member の単一値前提の破壊)
  • スレッドの並べ替え・グルーピング UI(束選択中にカードを隣接させる等)
  • エージェント側 (review_reply) の変更
  • コメントの折り畳み規則・resolve 規則そのものの変更

Acceptance Criteria

  • Markdown レビューで、既にコメントのある段落をクリックすると composer のターゲットになり、2本目のスレッドを追加できる
  • diff レビューで、既にコメントのある行範囲を再度選択して2本目のスレッドを追加できる(完全一致の範囲は同じアンカーになる)
  • 同じアンカーの2本を独立に resolve できる。片方だけ resolved にしても、もう片方は開いたまま
  • 未解決が1本でも残る間、インジケータは 💬 のまま。全部 resolved になって初めて ✓ になる
  • インジケータをクリックすると、そのアンカーの全カードが選択状態になり、枚数分の connector line が引かれる
  • resolved スレッドへの line は未解決のものより薄い
  • 束選択中にもう一度インジケータをクリックすると解除される
  • comment card を直接クリックした場合は、そのカード1枚だけが選択される
  • 束選択と compose ターゲットは排他(片方に入るともう片方が落ちる)
  • リロード後・エージェントのラウンド後(renderCommentsreconcileConnector)も、消えたカードがあれば束から落ちるだけで例外にならない
  • headless Chrome + CDP で上記のうち視覚に関わるもの(線の本数・薄さ・選択リング)を実際に描画して確認する

Assumptions Exposed & Resolved

Assumption Challenge Resolution
コメント済みブロックのクリックは「選択」であるべき 2本目を作る導線がそこしかない クリックは常に新規作成へ。選択はインジケータとカードに委ねる
選択は1本のコメントを指すもの N 本あるときどれを指すのか 選択の単位をアンカーの束にする。単独選択はカードクリックで残す
resolved は「終わった仕事」なので束から外すべき 数字は全件を数えている 全件を束に含める。線の濃さで区別する(ユーザー判断)
複数スレッドは返信で代替できる 返信機能は既にある 代替できない。個別 resolve話題の分離が返信では得られない
diff は後回しでよい 規則が2モードで食い違う期間が生まれる 両モード同時。ただし行範囲の重なりは別問題として切り離す

Technical Context (brownfield)

変更が要る箇所(すべて references/template.html):

箇所 内容
1811-1821 Markdown ブロックの mouseup。isCommented(anchorId) ? selectComment : setCommentTarget の分岐を廃し、常に setCommentTarget
2091-2110 diff の行 / ファイルヘッダのクリック。同上
2401 commentCard(anchor) 1枚返す関数を N枚返す commentCards(anchor) へ。querySelectorAll + フィルタ
2554 selectComment(anchor) 束選択。全カードに .selected、全ターゲットに .selected-target、線を N 本
(新規) 単独選択 カードクリック経路。connector の同一性に comment 個体が入るため、connector = {kind, anchor}{kind, anchor, commentId?} に拡張
2428 paintConnector / 2485 drawConnector / 2501 refreshConnector 1本前提を N 本へ。resolved は stroke-opacity を落とす
2583 reconcileConnector 「カードが消えたら落とす」を「消えたものだけ束から外す。全部消えたら落とす」へ
2352 scrollToCommentCard 束の先頭カードへ
1295 #connectorOverlay path (CSS) resolved 用の薄い variant を足す

変更不要が確認済みの箇所:

  • 2281 updateCommentIndicators() — 既にアンカー単位で件数を数え、allResolved も未解決の有無で判定している
  • 2655 commentsInAppearanceOrder() — 同一アンカーはソート安定性により作成順で隣接する(resolved を除く)
  • 1884 applyDiffAnchors() — アンカーの重複は anchors.includes で既に排除済み

参照プロトタイプ(この会話で作成、リポジトリ外):
/private/tmp/claude-501/.../scratchpad/multi-comment-proto.html(案 C が採用案)

Ontology (Key Entities)

Entity Type Fields Relationships
Anchor core domain spec-element-N / path#start-end / path#file 1つの Block/行範囲を指す。複数の Comment Thread から共有される
Comment Thread core domain id, text, author, status, anchor, messages 1つの Anchor に属する。独立に resolve される
Comment Card supporting data-anchor, data-comment-idx Comment Thread を1枚で表す
Comment Indicator supporting count, open/resolved glyph 1つの Anchor に1個。束選択の起点
Selection core domain kind(bunch/single), anchor, commentId? 束 = Anchor 単位、単独 = Comment Thread 単位
Connector supporting start, end, dimmed Selection 1つにつき N 本
Composer supporting activeAnchor, activeAnchorLines 常にブロッククリックから対象を受け取る

Ontology Convergence

Round Entity Count New Changed Stable Stability Ratio
1 6 6 - - N/A
2 7 1 (Selection) 0 6 86%
3 7 0 0 7 100%
4 7 0 0 7 100%
5 7 0 0 7 100%

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

  • The click rule is the one behaviour existing users will notice changing: a commented block no longer selects its thread on click. The indicator and the cards do that instead. Worth deciding whether that trade reads correctly in daily use.
  • Partially overlapping diff ranges (a#3-8 and a#5-6) are deliberately out of scope. They stay separate anchors and a line's data-anchor is 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.
  • When a bunch's cards sit below the panel's fold, the line to them fades at the viewport edge — the document column is scrolled to meet the first card, but the panel is never scrolled. Pre-existing, and arguably now more visible.
  • The hover preview stays suppressed while anything is selected or being composed. That rule is unchanged, but the compose state is much easier to enter and to stay in now, so previews go quiet more often than they used to.
  • Everything visual was checked in a real browser (headless Chrome driven over CDP), not by reading the diff: two comments on one anchor produce two lines, resolving one leaves the chip open and dims that line alone, a hovered chip no longer changes the hunk's scroll width (575/575 both before and after), and the click-to-line delay is now 3 ms.

handlename and others added 4 commits August 30, 2026 19:19
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>
@handlename
handlename merged commit dc90774 into main Aug 30, 2026
2 checks passed
@handlename
handlename deleted the feat/multiple-comments-per-block branch August 30, 2026 11:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant