feat(review): make comment replies deferred drafts, consistent with new comments - #213
Merged
Conversation
Inline comment view zones were fully torn down and rebuilt on every
comments:changed (a reply/edit/delete anywhere, or the poller pulling a
new remote comment). Each rebuild unmounted the zones' React roots, so an
open inline reply/edit lost its half-typed text — unlike the activity
timeline, which survives via keyed reconciliation.
Give the inline zones the same "reconcile in place" behaviour: refactor
mountInlineZones into a persistent controller (createInlineZones →
{ update, dispose }) that diffs zones by a stable (side, line) key —
unchanged keys re-render their existing root (CommentZone keys children by
remoteId, so an open editor keeps its state), only added/removed lines
mount/unmount. useCommentZones splits into a structural effect (owns the
controller, recreated only on editor/file/view change) and a content
effect (calls controller.update on comments/props change + rebuilds the
stateless glyph decorations).
The one-shot mountInlineZones wrapper is kept byte-compatible, so the
draft zones (useDraftZones) are unchanged. The activity timeline already
handled this and needs no change.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ew comments
Creating a new inline comment records a persisted draft, but replying to a
comment posted immediately with no draft — so an unsubmitted reply was lost
on switching PRs/files/tabs, and the two authoring flows were inconsistent.
Unify replies onto the same draft model:
- ReviewDraft gains kind ('comment' | 'reply', default 'comment' for
back-compat) + replyTo { parentCommentId, threadId? }; anchor becomes
optional (a reply snapshots its inline parent's anchor, or has none when
replying to a summary comment). drafts:create validates the kind
constraints; publishDraftBatch branches reply -> comments.replyToComment
vs comment -> publishInlineComment.
- The shared CommentReplyEditor now creates a pending reply-draft instead of
posting immediately, so every surface (activity timeline + inline diff)
defers identically. A new shared ReplyDraftList renders a parent's pending
reply-drafts below it (reusing DraftZone), self-fetching from the drafts
store, on both surfaces.
- Reply-drafts join the "Publish comments" batch and count; they publish via
the reply API and, on success, the local draft is dropped and the real
reply is fetched back as a normal comment.
- useDraftZones excludes reply-kind (they render nested, not as line zones);
useLineCommentAdder ignores them for '+' occupancy; DraftsPanel /
PublishReviewModal show a "reply" tag and guard the now-optional anchor;
PrPanel / DiffView guard anchor too.
Editing an existing comment stays immediate (it mutates a remote comment,
not new authored content). i18n added for 4 locales.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…s inline comment across refresh)
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.
Problem
Creating a new inline comment records a persisted draft (deferred, batch-published, survives switching PRs/files/tabs), but replying to a comment posted immediately with no draft. So an unsubmitted reply was lost on switching, and the two authoring flows were inconsistent.
Change
Unify replies onto the same draft model (chosen direction: replies become real drafts):
ReviewDraftgainskind: 'comment' | 'reply'(absent =comment, back-compat) andreplyTo { parentCommentId, threadId? };anchorbecomes optional (a reply snapshots its inline parent's anchor, or has none when replying to a summary comment).drafts:createvalidates the kind constraints;drafts:publishBatchbranchesreply → comments.replyToCommentvscomment → publishInlineComment.CommentReplyEditornow creates a pending reply-draft instead of posting immediately, so every surface (activity timeline + inline diff) defers identically (honoring the "comment interactions identical across surfaces" rule via the shared component). A new sharedReplyDraftListrenders a parent's pending reply-drafts below it (reusingDraftZone), self-fetching from the drafts store, on both surfaces.useDraftZonesexcludes reply-kind (they render nested, not as standalone line zones);useLineCommentAdderignores them for+occupancy;DraftsPanel/PublishReviewModalshow a "reply" tag and guard the now-optional anchor;PrPanel/DiffViewguard anchor too.Editing an existing comment stays immediate (it mutates a remote comment, not new authored content). i18n added for all 4 locales.
Notes
comments:replyIPC/controller is retained (still used by the local API / CLI comment-write path); only the renderer stopped calling it directly.fix(diff): preserve in-progress inline comment across comment refresh) —mountInlineZonesnow reconciles zones in place instead of tearing them down on every comment refresh, so an in-progress inline reply/edit isn't discarded. It's independent of the reply-drafts change (different files) and was merged in for convenience.Verification
lint(--max-warnings=0),typecheck,test(all suites pass),build.🤖 Generated with Claude Code