Add Sheets cell comments (phase B)#192
Conversation
|
Warning Rate limit exceeded
You’ve run out of usage credits. Purchase more in the billing tab. ⌛ How to resolve this issue?After the wait time has elapsed, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout. Please see our FAQ for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (36)
📝 WalkthroughWalkthroughThis PR implements phase B of sheet cell threaded comments, a complete end-to-end feature spanning comment types, pure mutation helpers, Yorkie persistence, canvas rendering, and React UI components. It includes comprehensive tests validating concurrency and orphan-thread cleanup behavior. ChangesSheet Cell Threaded Comments
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Poem
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
9ed34f7 to
607acd5
Compare
There was a problem hiding this comment.
Actionable comments posted: 11
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
packages/sheets/src/view/worksheet.ts (1)
5041-5074:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winRe-check staleness after
listThreads().This method had a version/sheet guard after
fetchGrid(), but the newawait sheet.getStore().listThreads()adds another suspension point beforegridCanvas.render(...). If a newer render wins during that await, this path can still paint stale grid/comment markers once before the queued render catches up.Suggested fix
const allThreads = await sheet.getStore().listThreads(); + if (sheet !== this.sheet || targetVersion !== this.renderVersion) { + return; + } const commentCellKeys = buildOpenThreadKeySet(allThreads); this.gridCanvas.render(🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/sheets/src/view/worksheet.ts` around lines 5041 - 5074, After awaiting sheet.getStore().listThreads() you must re-check the render staleness/version guard (the same guard used after fetchGrid()) before calling buildOpenThreadKeySet/gridCanvas.render; if the sheet/version has changed or a newer render has won, bail out to avoid painting stale comment markers. Locate the async path where listThreads() is awaited and add the same conditional check (using the existing render token/version or sheet identity check used elsewhere) and return early when stale, then proceed to call buildOpenThreadKeySet and gridCanvas.render only when current.
🧹 Nitpick comments (3)
packages/frontend/tests/app/spreadsheet/yorkie-comments-orphan.test.ts (1)
16-59: ⚡ Quick winMissing test: non-
sheet-cellanchor threads should be preserved.
deleteThreadsForAxisexplicitly skips anchors withkind !== "sheet-cell", but there is no test asserting that behavior. This is a directly-exercisable code path worth covering.✅ Suggested additional test case
it('preserves threads with non-sheet-cell anchors', () => { const ws = { comments: { // A normal cell thread – should be removed t1: thread('t1', 'r1', 'c1'), // A hypothetical non-cell thread – should be kept t2: { ...thread('t2', 'r1', 'c1'), anchor: { kind: 'sheet-range', tabId: 't' }, } as unknown as Thread, }, }; deleteThreadsForAxis(ws as any, 'row', new Set(['r1'])); assert.equal(ws.comments.t1, undefined); assert.ok(ws.comments.t2); // preserved because kind !== 'sheet-cell' });🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/frontend/tests/app/spreadsheet/yorkie-comments-orphan.test.ts` around lines 16 - 59, Add a test that verifies deleteThreadsForAxis preserves threads whose anchor.kind !== "sheet-cell": create a workspace object with comments containing a normal cell thread (use thread('t1', ...)) and a second thread object (e.g., t2) copied from thread('t2', ...) but with anchor: { kind: 'sheet-range', tabId: 't' } cast to Thread; call deleteThreadsForAxis(ws, 'row', new Set(['r1'])); assert the cell thread (t1) was removed and that the non-sheet-cell thread (t2) still exists. Target the test file where other cases live and reference deleteThreadsForAxis, Thread, thread, and anchor to locate the code under test.docs/design/sheets/comments.md (1)
54-75: ⚡ Quick winAdd language identifiers to fenced code blocks.
The code blocks showing directory and component tree structures are missing language identifiers, which affects rendering and syntax highlighting.
📝 Proposed fix
For the directory tree at line 54:
-``` +```text packages/sheets/src/comment/For the component tree at line 278:
-``` +```text SpreadsheetPageAlso applies to: 278-285
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@docs/design/sheets/comments.md` around lines 54 - 75, The fenced code blocks in the documentation (the directory tree starting with "packages/sheets/src/comment/" and the component tree under "packages/frontend/src/app/spreadsheet/") lack language identifiers, which breaks rendering and highlighting; update those fenced blocks to include an appropriate language tag such as "text" (e.g., change ``` to ```text) for the directory/component trees so the tree structures render correctly in the docs (apply to the block showing packages/sheets/src/comment/ and the block showing the SpreadsheetPage/component tree around lines 278–285).packages/frontend/tests/app/spreadsheet/comments-concurrency.test.ts (1)
149-188: ⚡ Quick winAdd a reopen convergence case.
This suite covers
open -> resolved, but notresolved -> openagain. Since reopen is part of the new thread lifecycle, that leaves a key concurrency transition unverified.As per coding guidelines,
**/*.{test,spec}.{js,ts,jsx,tsx}: Write tests for critical business logic and edge cases🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/frontend/tests/app/spreadsheet/comments-concurrency.test.ts` around lines 149 - 188, Add a new test mirroring the "concurrent resolve" case but for reopen: use createTwoUserSpreadsheet to create a thread via ctx.storeA.addThread, sync, then perform concurrent reopen calls with ctx.storeA.setThreadResolved(thread.id, false, authorA) and ctx.storeB.setThreadResolved(thread.id, false, authorB) (or vice versa) inside Promise.all, await ctx.sync(), then assert both ctx.storeA.listThreads() and ctx.storeB.listThreads() return the same single thread and that its resolved flag is false; place it alongside the existing concurrent-resolve test and ensure cleanup via ctx.cleanup().
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@docs/tasks/archive/2026/05/20260508-sheets-comments-todo.md`:
- Line 15: The heading "### Task 1: Comment data model & pure helpers (sheets
package)" is an H3 with no preceding H2 which triggers markdownlint MD001; fix
it by either promoting this line to an H2 (change "###" to "##") or by inserting
an appropriate H2 section above it (e.g., "## Tasks" or "## Sheet tasks") so the
heading hierarchy is contiguous and markdownlint passes.
In
`@packages/frontend/src/app/spreadsheet/components/comments/CommentComposer.tsx`:
- Around line 39-43: The submit function currently validates using trimmed but
calls onSubmit with the untrimmed body, causing stored comments to include
surrounding whitespace; change the submit handler (submit) to pass trimmed to
onSubmit (or trim inside onSubmit) and then clear body via setBody(""), ensuring
the same trimmed value used for the empty-guard/button-disabled check is what
gets submitted (identify submit, onSubmit, body, trimmed, and setBody in
CommentComposer.tsx).
In
`@packages/frontend/src/app/spreadsheet/components/comments/CommentPopover.tsx`:
- Around line 88-91: The expression computing canEditOrDelete assumes
currentUser is non-null and can throw if currentUser is null; update the logic
in the canEditOrDelete calculation to guard access to currentUser (e.g., use a
null-check or optional chaining on currentUser.userId) so it only compares IDs
when currentUser exists, and ensure any early-return or fallback provides a
clear debug-friendly message/context where appropriate; refer to
canEditOrDelete, currentUser, isReadOnly, c.author.userId and editingCommentId
to locate and fix the check.
In
`@packages/frontend/src/app/spreadsheet/components/comments/CommentSidePanel.tsx`:
- Around line 42-67: The tab buttons in CommentSidePanel.tsx use aria-pressed
(toggle semantics) instead of ARIA tabs; change the container <div> to
role="tablist", change each tab button to role="tab" and replace aria-pressed
with aria-selected={tab === "open"/"resolved"} (remove aria-pressed), and add
role="tabpanel" to the content <ul> that renders the tab panels; also implement
keyboard handling (onKeyDown either on the tab buttons or the tablist) to
support Left/Right (and Home/End) to move focus and call setTab appropriately,
ensuring the active tab receives focus when selected so screen readers and
keyboard users get correct tab semantics (reference symbols: CommentSidePanel,
tab state variable, setTab, openCount, resolvedCount, and the <ul> panel).
In `@packages/frontend/src/app/spreadsheet/sheet-view.tsx`:
- Around line 1242-1258: The computed commentPopoverPosition is only updated
during React renders, so when the grid scrolls the popover becomes stale; fix
this by making the position reactive to sheet viewport changes: move the
computation out of a plain IIFE and into React state (e.g.,
commentPopoverPosition state or a render-key) and add a useEffect that
subscribes to the sheet viewport/scroll change events on sheetRef.current (use
the sheet API methods that emit viewport/scroll events or listen to native
scroll if needed) and recomputes/sets the position using getGridViewportRect()
and getCellRect(activeCellForComment) whenever the sheet viewport changes or
when activeCellForComment changes; also clean up the event listener in the
effect cleanup to avoid leaks.
- Around line 809-816: handleCommentResolve currently closes the cell comment
popover unconditionally after resolving any thread; instead, after calling
storeRef.current?.setThreadResolved(threadId, true, commentAuthor) use the store
to check whether that thread's cell still has any unresolved threads (e.g.
retrieve the thread via storeRef.current?.getThread(threadId) to get its cell
id, then query remaining threads for that cell or a hasUnresolvedThreads(cellId)
helper) and only call setCommentPopoverOpen(false) when no unresolved threads
remain; update handleCommentResolve to perform this conditional close while
preserving the existing commentAuthor and storeRef usage.
In `@packages/frontend/src/app/spreadsheet/yorkie-store.ts`:
- Around line 1221-1239: addThread currently ignores anchor.tabId and will
persist a thread into root.sheets[this.tabId] even if anchor.tabId differs;
update addThread to validate that anchor.tabId === this.tabId (or return/reject)
before creating/applying the thread. Specifically, in the addThread function
check anchor.tabId against this.tabId and throw or return an error (or no-op)
when they differ, ensuring applyAddThread is only called on the matching sheet;
also update callers/tests if they expect a different behavior.
- Around line 1287-1307: listThreads currently returns live objects from
ws.comments allowing external mutation; modify listThreads (in yorkie-store.ts)
to materialize and return detached snapshots instead of the raw live objects —
for each thread from Object.values(ws?.comments ?? {}), produce a shallow/deep
copy (e.g., clone top-level properties, clone anchor object and clone comments
array/objects) so callers get immutable snapshots consistent with other getters
in this class; ensure filtering logic (opts?.cellAnchor and opts?.resolved) runs
against the clones or clone after filtering and return the array of cloned
Thread objects.
In `@packages/frontend/src/components/sheet-context-menu.tsx`:
- Around line 206-208: The UI currently hardcodes the Option symbol (⌥)
regardless of platform; update the span in the SheetContextMenu component (where
navigator.platform.startsWith("Mac") is used) to render "⌥" for Mac and "Alt"
for non-Mac, so the text becomes either "⌘+⌥+M" on Mac or "Ctrl+Alt+M" on
Windows/Linux; modify the expression in that span to compute both the modifier
prefix (navigator.platform.startsWith("Mac") ? "⌘" : "Ctrl") and the middle
modifier (navigator.platform.startsWith("Mac") ? "⌥" : "Alt") and concatenate
them appropriately.
In `@packages/sheets/src/store/memory.ts`:
- Around line 553-629: The store is returning references to the internal
Thread/Comment objects (e.g., from addThread, addReply, listThreads) so callers
can mutate MemStore state; instead, return deep-cloned snapshots. After
creating/updating a thread with createThreadHelper, addReplyHelper,
editCommentHelper, deleteCommentHelper, setThreadResolvedHelper, keep the
internal object as-is in this.threads but return a deep clone to callers (use
structuredClone or a reusable cloneThread/cloneComment helper). Ensure
listThreads maps the stored threads to clones before returning and that addReply
returns a clone of the newly appended comment rather than the internal
reference.
In `@packages/sheets/src/view/render-comments.ts`:
- Around line 4-6: JSDoc is out of sync with the MARKER_SIZE constant (docs say
"7x7" but MARKER_SIZE = 9); update the JSDoc on the comment marker function (the
block above the marker drawing logic that references MARKER_SIZE) so it
accurately describes the marker size (e.g., "9x9" or reference MARKER_SIZE) and
its placement, or alternatively change MARKER_SIZE to 7 if the intended marker
is truly 7x7—ensure the JSDoc and the MARKER_SIZE constant (and any calculations
using it) are consistent.
---
Outside diff comments:
In `@packages/sheets/src/view/worksheet.ts`:
- Around line 5041-5074: After awaiting sheet.getStore().listThreads() you must
re-check the render staleness/version guard (the same guard used after
fetchGrid()) before calling buildOpenThreadKeySet/gridCanvas.render; if the
sheet/version has changed or a newer render has won, bail out to avoid painting
stale comment markers. Locate the async path where listThreads() is awaited and
add the same conditional check (using the existing render token/version or sheet
identity check used elsewhere) and return early when stale, then proceed to call
buildOpenThreadKeySet and gridCanvas.render only when current.
---
Nitpick comments:
In `@docs/design/sheets/comments.md`:
- Around line 54-75: The fenced code blocks in the documentation (the directory
tree starting with "packages/sheets/src/comment/" and the component tree under
"packages/frontend/src/app/spreadsheet/") lack language identifiers, which
breaks rendering and highlighting; update those fenced blocks to include an
appropriate language tag such as "text" (e.g., change ``` to ```text) for the
directory/component trees so the tree structures render correctly in the docs
(apply to the block showing packages/sheets/src/comment/ and the block showing
the SpreadsheetPage/component tree around lines 278–285).
In `@packages/frontend/tests/app/spreadsheet/comments-concurrency.test.ts`:
- Around line 149-188: Add a new test mirroring the "concurrent resolve" case
but for reopen: use createTwoUserSpreadsheet to create a thread via
ctx.storeA.addThread, sync, then perform concurrent reopen calls with
ctx.storeA.setThreadResolved(thread.id, false, authorA) and
ctx.storeB.setThreadResolved(thread.id, false, authorB) (or vice versa) inside
Promise.all, await ctx.sync(), then assert both ctx.storeA.listThreads() and
ctx.storeB.listThreads() return the same single thread and that its resolved
flag is false; place it alongside the existing concurrent-resolve test and
ensure cleanup via ctx.cleanup().
In `@packages/frontend/tests/app/spreadsheet/yorkie-comments-orphan.test.ts`:
- Around line 16-59: Add a test that verifies deleteThreadsForAxis preserves
threads whose anchor.kind !== "sheet-cell": create a workspace object with
comments containing a normal cell thread (use thread('t1', ...)) and a second
thread object (e.g., t2) copied from thread('t2', ...) but with anchor: { kind:
'sheet-range', tabId: 't' } cast to Thread; call deleteThreadsForAxis(ws, 'row',
new Set(['r1'])); assert the cell thread (t1) was removed and that the
non-sheet-cell thread (t2) still exists. Target the test file where other cases
live and reference deleteThreadsForAxis, Thread, thread, and anchor to locate
the code under test.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 4ea9d803-b34f-477e-9a25-287fb1db02ba
📒 Files selected for processing (36)
docs/design/README.mddocs/design/sheets/comments.mddocs/tasks/README.mddocs/tasks/archive/2026/05/20260508-sheets-comments-lessons.mddocs/tasks/archive/2026/05/20260508-sheets-comments-todo.mddocs/tasks/archive/README.mdpackages/frontend/src/app/documents/document-detail.tsxpackages/frontend/src/app/spreadsheet/components/comments/CommentComposer.tsxpackages/frontend/src/app/spreadsheet/components/comments/CommentPopover.tsxpackages/frontend/src/app/spreadsheet/components/comments/CommentSidePanel.tsxpackages/frontend/src/app/spreadsheet/sheet-view.tsxpackages/frontend/src/app/spreadsheet/yorkie-store.tspackages/frontend/src/app/spreadsheet/yorkie-worksheet-axis.tspackages/frontend/src/app/spreadsheet/yorkie-worksheet-comments.tspackages/frontend/src/app/spreadsheet/yorkie-worksheet-structure.tspackages/frontend/src/components/sheet-context-menu.tsxpackages/frontend/src/lib/utils.tspackages/frontend/tests/app/spreadsheet/comments-concurrency.test.tspackages/frontend/tests/app/spreadsheet/yorkie-comments-orphan.test.tspackages/frontend/tests/app/spreadsheet/yorkie-worksheet-comments.test.tspackages/frontend/tests/helpers/two-user-spreadsheet-yorkie.tspackages/sheets/src/comment/__tests__/anchor.test.tspackages/sheets/src/comment/__tests__/thread.test.tspackages/sheets/src/comment/anchor.tspackages/sheets/src/comment/thread.tspackages/sheets/src/comment/types.tspackages/sheets/src/index.tspackages/sheets/src/model/workbook/worksheet-document.tspackages/sheets/src/store/__tests__/memory-comments.test.tspackages/sheets/src/store/memory.tspackages/sheets/src/store/readonly.tspackages/sheets/src/store/store.tspackages/sheets/src/view/__tests__/render-comments.test.tspackages/sheets/src/view/gridcanvas.tspackages/sheets/src/view/render-comments.tspackages/sheets/src/view/worksheet.ts
| async addThread( | ||
| anchor: CommentAnchor, | ||
| body: string, | ||
| author: CommentAuthor, | ||
| ): Promise<Thread> { | ||
| const thread = createThreadHelper( | ||
| anchor, | ||
| body, | ||
| author, | ||
| () => this.newThreadId(), | ||
| () => this.newCommentId(), | ||
| () => Date.now(), | ||
| ); | ||
| this.threads.set(thread.id, thread); | ||
| return thread; | ||
| } | ||
|
|
||
| async addReply( | ||
| threadId: string, | ||
| body: string, | ||
| author: CommentAuthor, | ||
| ): Promise<Comment> { | ||
| const t = this.threads.get(threadId); | ||
| if (!t) throw new Error(`Thread not found: ${threadId}`); | ||
| const next = addReplyHelper(t, body, author, () => this.newCommentId(), () => Date.now()); | ||
| this.threads.set(threadId, next); | ||
| return next.comments[next.comments.length - 1]; | ||
| } | ||
|
|
||
| async editComment(threadId: string, commentId: string, body: string): Promise<void> { | ||
| const t = this.threads.get(threadId); | ||
| if (!t) throw new Error(`Thread not found: ${threadId}`); | ||
| this.threads.set(threadId, editCommentHelper(t, commentId, body, () => Date.now())); | ||
| } | ||
|
|
||
| async deleteComment(threadId: string, commentId: string): Promise<void> { | ||
| const t = this.threads.get(threadId); | ||
| if (!t) throw new Error(`Thread not found: ${threadId}`); | ||
| const next = deleteCommentHelper(t, commentId); | ||
| if (next === null) this.threads.delete(threadId); | ||
| else this.threads.set(threadId, next); | ||
| } | ||
|
|
||
| async setThreadResolved( | ||
| threadId: string, | ||
| resolved: boolean, | ||
| by: CommentAuthor, | ||
| ): Promise<void> { | ||
| const t = this.threads.get(threadId); | ||
| if (!t) throw new Error(`Thread not found: ${threadId}`); | ||
| this.threads.set(threadId, setThreadResolvedHelper(t, resolved, by, () => Date.now())); | ||
| } | ||
|
|
||
| async listThreads(opts?: { | ||
| tabId?: string; | ||
| cellAnchor?: { rowId: string; colId: string }; | ||
| resolved?: boolean; | ||
| }): Promise<Thread[]> { | ||
| let result = Array.from(this.threads.values()); | ||
| if (opts?.tabId !== undefined) { | ||
| result = result.filter( | ||
| (t) => t.anchor.kind === 'sheet-cell' && t.anchor.tabId === opts.tabId, | ||
| ); | ||
| } | ||
| if (opts?.cellAnchor) { | ||
| result = result.filter( | ||
| (t) => | ||
| t.anchor.kind === 'sheet-cell' && | ||
| t.anchor.rowId === opts.cellAnchor!.rowId && | ||
| t.anchor.colId === opts.cellAnchor!.colId, | ||
| ); | ||
| } | ||
| if (opts?.resolved !== undefined) { | ||
| result = result.filter((t) => t.resolved === opts.resolved); | ||
| } | ||
| return result; | ||
| } |
There was a problem hiding this comment.
Clone comment snapshots before returning them.
These methods currently expose the same Thread / Comment objects stored in this.threads. A caller mutating the returned value will mutate MemStore state out-of-band, which is especially easy to do in tests.
Suggested fix
+ private cloneThread(thread: Thread): Thread {
+ return structuredClone(thread);
+ }
+
async addThread(
anchor: CommentAnchor,
body: string,
author: CommentAuthor,
): Promise<Thread> {
@@
);
this.threads.set(thread.id, thread);
- return thread;
+ return this.cloneThread(thread);
}
@@
async addReply(
threadId: string,
body: string,
author: CommentAuthor,
): Promise<Comment> {
@@
const next = addReplyHelper(t, body, author, () => this.newCommentId(), () => Date.now());
this.threads.set(threadId, next);
- return next.comments[next.comments.length - 1];
+ return structuredClone(next.comments[next.comments.length - 1]);
}
@@
async listThreads(opts?: {
@@
}): Promise<Thread[]> {
let result = Array.from(this.threads.values());
@@
- return result;
+ return result.map((thread) => this.cloneThread(thread));
}🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/sheets/src/store/memory.ts` around lines 553 - 629, The store is
returning references to the internal Thread/Comment objects (e.g., from
addThread, addReply, listThreads) so callers can mutate MemStore state; instead,
return deep-cloned snapshots. After creating/updating a thread with
createThreadHelper, addReplyHelper, editCommentHelper, deleteCommentHelper,
setThreadResolvedHelper, keep the internal object as-is in this.threads but
return a deep clone to callers (use structuredClone or a reusable
cloneThread/cloneComment helper). Ensure listThreads maps the stored threads to
clones before returning and that addReply returns a clone of the newly appended
comment rather than the internal reference.
Spec out Google Sheets-style threaded comments anchored to cells, with resolve/reopen, multi-thread per cell, and a side panel listing all threads across tabs. Phase B keeps the body plain-text and defers mentions/notifications to phase C. CommentAnchor is a discriminated union from day one so a future @wafflebase/comments extraction (when Docs adds comments) is a move, not a rewrite. Anchors carry rowId/colId, so inserts/moves track for free via the existing axis ID model; row/column delete cascades via same-transaction cleanup so undo restores the thread. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
14 TDD-driven tasks covering data model, Yorkie schema/mutations, orphan cleanup, canvas marker rendering, popover/side panel UI, entry points, and concurrent integration tests. Lessons file seeded for in-flight pattern capture. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Implement pure thread-mutation helpers (createThread, addReply, editComment, deleteComment, setThreadResolved) with injected ID and timestamp callbacks for deterministic testing and flexible integration with Yorkie store and MemStore. All helpers validate non-empty comment bodies and return immutable thread copies.
Convert between CellAnchor (rowId/colId identifiers) and visual Sref strings (e.g. 'B3') using axis order. Includes liveness check to verify both row and column IDs are still valid.
Add `comments?: { [threadId: string]: Thread }` field to the canonical
Worksheet type in the Yorkie document schema. Re-export Comment,
CommentAnchor, CommentAuthor, and Thread types from @wafflebase/sheets.
This allows thread data to be persisted in the Worksheet and materialized
on first thread insertion without requiring migrations.
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Create pure functions that operate on worksheet objects to add threads, replies, edit/delete comments, and resolve/unresolve threads. These are called inside doc.update(...) blocks where Yorkie proxies intercept plain object mutations transparently. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Implement the 6 comment-related methods on YorkieStore: addThread, addReply, editComment, deleteComment, setThreadResolved, and listThreads. Each method delegates to the pure mutations in yorkie-worksheet-comments.ts inside a doc.update(...) block. Also export createThread and addReply from the main sheets index so they can be imported from @wafflebase/sheets. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
When a row or column is deleted, threads anchored to it become orphans. Auto-delete them in the same transaction (via deleteThreadsForAxis) so undo/redo restores both the deleted rows/columns and their threads. - Add deleteThreadsForAxis helper in yorkie-worksheet-structure - Modify deleteYorkieWorksheetAxis to return deleted axis IDs - Wire cleanup call into applyYorkieWorksheetShift for deletes - Add comprehensive test for orphan cleanup behavior Fixes task 7 of sheets-comments-phase-b roadmap.
Eliminate type duplication by using Worksheet from @wafflebase/sheets in both yorkie-worksheet-structure.ts and yorkie-worksheet-comments.ts. The Worksheet type already includes the comments field, making the local WorksheetCommentsView and WorksheetWithComments typedefs redundant. Add a fourth test case to verify mixed scenarios: some threads in the deleted set are removed while those outside the set are preserved. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Implement Task 8 of comments phase B: draw a 7×7 yellow triangle in the top-right corner of cells that have at least one unresolved comment thread on the active tab. Changes: - Add render-comments.ts module (both sheets and frontend packages) with buildOpenThreadKeySet() helper and drawCommentMarker() canvas function - Add render-comments.test.ts unit tests for buildOpenThreadKeySet - Update GridCanvas.render() to accept rowOrder, colOrder, commentCellKeys - Add Pass 5 to renderQuadrantCells to draw comment markers - Update all renderQuadrantCells call sites with new parameters - Wire Worksheet.renderSheet() to build comment key set via listThreads() and pass to GridCanvas The marker respects the 'resolved' filter and only renders on the active tab (via the Store's listThreads query). Marker style: 7×7 px yellow triangle (#fbbc04), positioned at cell's top-right corner to match Google Sheets parity. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Reusable plain-text input form for new threads and replies. Supports Cmd/Ctrl+Enter to submit, Escape to cancel, and disabled mode for unauthenticated users. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
- CommentPopover: threaded comment UI with reply/resolve/edit/delete actions, reusing CommentComposer; read-only mode for unauthenticated users - SheetView: tracks active cell via onSelectionChange, derives open threads from worksheet.comments, auto-opens popover when a cell with unresolved threads is selected, positions popover beside the active cell using getCellRect + getGridViewportRect, delegates mutations to YorkieStore (addThread, addReply, setThreadResolved, editComment, deleteComment) Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
Creates the CommentSidePanel React component (Open/Resolved tabs, per-thread rows). Wires it into DocumentLayout with a toolbar toggle button, aggregates threads from all tabs via the Yorkie doc root, and implements jump-to-cell by converting CommentAnchor → Sref and passing a commentJumpTarget prop to SheetView which calls focusCell. Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
Add docVersion state that bumps on local-change and remote-change events, fixing the stale allThreads useMemo that previously relied only on doc and activeTabId (neither of which change when threads are added/edited/resolved on the same tab). This ensures the comment side panel stays in sync with thread mutations.
- Right-click cell → "Insert comment" (Cmd/Ctrl+⌥+M hint) - Cmd/Ctrl+Alt+M opens the comment composer for the active cell - Cmd/Ctrl+Alt+Shift+M toggles the comments side panel - Toolbar IconMessage button was already wired in Task 11 Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
Four integration tests covering: concurrent thread creation on the same cell, concurrent replies to the same thread, row-delete cascading to orphan thread while the other client edits it, and concurrent resolve of the same thread. Tests are gated behind YORKIE_RPC_ADDR, matching the existing concurrency-test convention. Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
All 14 tasks complete. verify:fast passes (58 test files, 1265 tests). Visual tests skipped — no browser harness in repo; follow-up for phase C. Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
Three small post-review polishes: - CommentPopover renders comment bodies with whitespace-pre-wrap so user-typed newlines survive display, matching the spec's plain-text- with-newlines goal (would otherwise collapse to spaces). - handleJumpToCell in DocumentLayout now uses cellAnchorToSref instead of the older anchorToRef + toSref pair, eliminating the apparent unfinished-refactor smell flagged in final review. - listThreads JSDoc on the Store interface clarifies it returns a single tab's threads (defaulting to the active tab) — callers aggregating across tabs should walk Worksheet.comments directly. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Four UX issues surfaced during browser testing of the comments feature: 1. Comment body in CommentPopover was 14px while surrounding metadata was 12px — visually too big and clashing. Reduced body to text-xs. 2. Cell comment marker at 7×7 was too small to spot at typical zoom. Bumped MARKER_SIZE to 9 in both gridcanvas and the exported helper. 3. Side panel did not refresh after adding a comment. Replaced the manual docVersion state + subscribe-and-bump indirection with the reactive `root` value already provided by the Yorkie React SDK (useDocument().root), so the allThreads memo recomputes naturally on every CRDT mutation. 4. Marker rendered at the wrong position (one row above the target cell, horizontally shifted by RowHeaderWidth) and did not track scroll. The hand-rolled coord math in Pass 5 had the wrong sign on scroll and omitted the header offsets. Replaced with toCellRect, the canonical cell-rect helper used by Pass 1-3, which handles scroll, headers, and merge anchors correctly. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Iterative UX polish from browser testing. Builds on the previous Tier 1 work (avatars, relative time, hover-only edit/delete, resolve emphasis, outside-click dismiss, reply indentation) with the following follow-ups: - Drop the "Comments" header bar entirely. Outside-click and Escape both dismiss; the title was occupying vertical space without value. - Sync composer textarea size to the comment body (text-xs) so editing and reading feel like the same surface, not two different fonts. - Add a `compact` prop to CommentComposer used for the inline reply spot — slim 1-row textarea, submit button hidden until content typed. - Replace the [Reply] button → composer toggle with an always-visible inline composer at the bottom of every thread. - Move Resolve to a small ✓ icon button on the root comment header (right-aligned), reclaiming a full button row of vertical space. - Move Edit / Delete behind a MoreHorizontal "⋯" dropdown (revealed on hover/focus-within of the comment row). - Force focus into the composer on popover open: React's autoFocus prop silently no-ops when the textarea is mounted into an already-focused container, so call ref.current.focus() explicitly via useEffect. This makes "Insert comment" / Cmd+Alt+M put the caret in the input even on cells that already have threads. - Auto-grow the textarea to fit content (capped at 200px with internal scroll) so multi-line comments do not require manual resize. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
607acd5 to
1a9359c
Compare
Verification: verify:selfResult: ✅ PASS in 164.6s
Verification: verify:integrationResult: ✅ PASS |
Codecov Report❌ Patch coverage is 📢 Thoughts on this report? Let us know! |
- CommentPopover: use optional chaining (currentUser?.userId) so TypeScript does not assume !isReadOnly implies non-null currentUser - CommentSidePanel: replace aria-pressed with role="tab"/aria-selected inside a role="tablist" container per WAI-ARIA tabs pattern - sheet-view: handleCommentResolve now keeps the popover open when other unresolved threads remain on the same cell - yorkie-store addThread: reject calls whose anchor.tabId mismatches the store's own tabId to prevent silent cross-tab writes - yorkie-store listThreads: return structuredClone copies so callers cannot accidentally mutate live Yorkie proxy objects - memory.ts listThreads: same structuredClone guard for MemStore - CommentComposer: pass trimmed body to onSubmit (was raw) - sheet-context-menu: render full "⌘+⌥+M" / "Ctrl+Alt+M" per platform instead of mixing the ⌥ symbol into the non-Mac branch - render-comments + gridcanvas: fix JSDoc "7x7" → "9x9" to match the actual MARKER_SIZE = 9 constant - comments todo: add ## Tasks H2 before first ### Task to fix MD001 Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
Threaded comments anchored to spreadsheet cells, sourced from docs/design/sheets/comments.md. Phase B ships: * Multi-thread per cell with replies, edit/delete (author only), resolve/reopen (any collaborator). * Anchor stability via existing axis IDs (rowId/colId) — inserts and moves track for free; row/column delete cascades to threads in the same Yorkie transaction so undo restores them atomically. * Five UI surfaces: yellow canvas marker, cell-click popover, right-side panel with Open/Resolved tabs, context menu + Cmd/Ctrl+ Alt+M / +Shift+M shortcuts, and side-panel jump-to-cell. * Plain-text bodies with newlines, real-time sync via the existing Yorkie pipeline, read-only mode for unauthenticated viewers. Architecture is anchor-agnostic from day one — `CommentAnchor` is a discriminated union so a future `@wafflebase/comments` extraction (when Docs adds comments) is a move, not a rewrite. The sheets package owns the data model, pure helpers, and Canvas marker; the frontend owns the Yorkie boundary, React UI (popover, side panel, composer), and event wiring. UI polished against browser testing: avatars, relative time stamps, hover-revealed Edit/Delete behind a "⋯" menu, Resolve as a small ✓ icon button on root comment headers, always-visible inline reply composer, auto-grow textarea, outside-click and Escape dismiss, forced focus on popover open. Composer textarea sized to match the displayed comment body for visual continuity. Out of scope (phase C+): @user mentions, notifications, per-user unread state, range / row / column / sheet anchors, popover scroll-tracking. Tests: 1265 unit, 4 Yorkie two-client concurrency scenarios gated behind YORKIE_RPC_ADDR. Visual / interaction tests deferred — no Playwright harness in repo yet. Co-authored-by: Claude Opus 4.7 (1M context) <[email protected]>
Adds @wafflebase/slides as a third surface alongside Sheets and Docs, plus 53-shape library (Phase 1+2), adjustment handles for 9 pilot shapes (P3-A.1), live snap guides, align/distribute toolbar, themed authoring, and layout-change UI. Also ships Sheets cell comments (Phase B), docs peer-avatar caret jump, yorkie-js-sdk 0.7.8 upgrade, and CLI/REST API improvements (docs export imageFetcher, expired session refresh, REST docs split). Minor bump because slides is a new top-level package. Highlights: #184 #185 #186 #187 #188 #189 #190 #191 #192 #197 #198 #201 #202 #203 #204 #205 #206 #207 #209 #210
Summary
Cmd/Ctrl+Alt+Mshortcut, and side-panel jump-to-cell.CommentAnchoris a discriminated union from day one so a future@wafflebase/commentsextraction (when Docs adds comments) is a move, not a rewrite.Architecture
packages/sheets/src/comment/— pure TypeScript data model:Comment,Thread,CommentAnchor, plus pure helpers and anchor↔Sref conversion. Zero Yorkie dependency.Worksheet.comments?— optional Yorkie field, no migration needed.packages/frontend/src/app/spreadsheet/yorkie-worksheet-comments.ts— Yorkie-local mutations operating on plain object proxies.packages/sheets/src/view/render-comments.ts— canvas marker, drawn in the existing sheets render pipeline.deleteThreadsForAxisruns in the samedoc.update()transaction as row/column delete, so undo restores threads atomically.Spec & docs
docs/design/sheets/comments.mddocs/tasks/archive/2026/05/20260508-sheets-comments-todo.mdOut of scope (phase C+)
@usermentions, in-app/email notifications, per-user unread state.Test plan
pnpm verify:fast— 748/748 unit tests passpnpm sheets typecheck— cleanpnpm frontend lint— 0 warningspnpm frontend build— cleanpnpm verify:entropy— clean (knip / doc staleness)tests/app/spreadsheet/comments-concurrency.test.ts) runs only whenYORKIE_RPC_ADDRis set — verify in CICmd/Ctrl+Alt+Mopens composer on empty cell;Cmd/Ctrl+Alt+Shift+Mtoggles panel🤖 Generated with Claude Code
Summary by CodeRabbit
Release Notes