fix(review): save draft notes exactly once under rapid Ctrl+S#581
Merged
benvinegar merged 2 commits intoJul 21, 2026
Merged
Conversation
Rapid save events (coalesced stdin chunks, double-clicks) re-entered saveDraftNote before the draft-clearing state update committed, saving duplicate notes that shared one user:<ms> id. Guard re-entry with a ref keyed by draft id and suffix note ids with a monotonic counter. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YJ4Wfy9erR3MnYVe9jAtvG
endotakuya
marked this pull request as ready for review
July 20, 2026 17:00
Contributor
|
PR author is not in the allowed authors list. |
benvinegar
requested changes
Jul 21, 2026
benvinegar
left a comment
Member
There was a problem hiding this comment.
The fix itself looks good, and I reproduced the issue and verified the fix in tmux. Could you tighten up these two tests before merging?
- In the PTY test,
session.type("\x13\x13")sends the two characters separately with a short delay, so it doesn't guarantee the coalesced input described in the comment.await session.write("\x13\x13")would send both bytes in one PTY write and make the regression test deterministic. - In the controller test, please freeze or mock
Date.now()for both saves. As written, the IDs may differ because the clock advanced between renders, so it doesn't actually prove IDs stay unique within the same millisecond.
This comment was generated by Pi using OpenAI GPT-5.4
benvinegar
approved these changes
Jul 21, 2026
benvinegar
left a comment
Member
There was a problem hiding this comment.
I pushed the two test fixes: the PTY test now sends both Ctrl+S bytes in one write, and the controller test holds Date.now() constant while checking the exact sequence IDs. The focused tests, typecheck, lint, and formatting all pass.
This comment was generated by Pi using OpenAI GPT-5.4
Contributor
Author
|
@benvinegar Thank you for the review, the test fixes, and the merge! |
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.
Summary
saveDraftNotere-entry with a ref keyed by the draft id, so save events that arrive before the draft-clearing state update commits are no-ops.user:*ids stay unique within one millisecond.Closes #580.
Why
React state is a per-render snapshot, so the old
if (!draftNote)guard could not reject a second Ctrl+S delivered in the same stdin chunk (or aSave (^S)double-click): both events saw the still-non-null draft and each appended a note. The duplicates also shared oneuser:${Date.now()}id, so id-addressed removal (the[x]close button,hunk session comment remove) silently deleted both copies.Testing
bun run typecheckbun run lintbun test src/ui/hooks/useReviewController.test.tsx(24 passed)bun test ./test/pty/notes.test.ts(13 passed, includes the new rapid Ctrl+S regression test, which also reproduces the bug against the released v0.17.3 binary viaHUNK_TEST_EXECUTABLE)bun test ./src ./packages ./scripts ./test/cli(1,180 passed;./test/sessionfails on this machine identically onorigin/maindue to a live local hunk daemon, so relying on CI there)