How built Comment editing using Claude CodeHere's the process I follow to add new features to the app using CC.

    Since we are all about improving our process, I will share the process I followed to built Comment editing into the app.

    Feature Overview

      You can see the PR here

      Since we already had comment editing support in the daemon, I wanted all of us to use the feature too. I actually had two bad comments I wanted to edit and I couldn't. so that's why I created the feature.

      Also I saw how GitHub handles comment editing and I thought it was a good UI/UX solution.

    The Process

      Initial Prompt

        here's what I said to claud in a complete new worktree/session:

        the daemon API lets us Edit comments (only the author can edit comments). can you make a plan to implement comment editing? (I mean create an edit on a published comment).
        - we need to add a “edited” label to comments that has being edited
        - we should let readers see past versions of comments
        - we should only let comment authors edit its own comments
        - we have delegated keys, so all delegated keys should act as one, so any of those accounts should be able to edit any of the comments created by all the delegated accounts.
        
        please add tests too

        I also attached this two images:

      Claude code in action

        CC asked me to clarify this things:

        and here's my answers:

        For "For the version history UI, what style do you want?", I selected:
        - Popover dropdown (Recommended)
        
        For "For delegation: on desktop, device linking copies the account key to the linked device, so both devices sign with the same key. Do you have cases where DIFFERENT keys create comments that should be editable by each other, or is same-key-on-all-devices sufficient?": I’m not sure about this one. what I know is that a delegated key I have on one site should be able to edit or delete comments made by that same key and also the key is linked.
        
        For "Should editing be available on both desktop and web, or desktop only?", I selected:
        - Both desktop and web

      Initial Plan

        # Comment Editing with Version History
        
        ## Context
        
        Users need to edit published comments. The daemon API already supports `UpdateComment` (creates a new blob with same TSID), but there's no frontend UI, no "(edited)" indicator, no version history, and the delegation auth check is missing. The goal: full edit flow with "(edited)" label, GitHub-style version history popover, and delegation-aware auth.
        
        ## Architecture Summary
        
        - **Comment identity**: `{authority}/{tsid}` — authority = signer's principal, TSID = timestamp+hash
        - **Editing model**: New blob with same TSID, different CID. `createTime` (from TSID) stays constant, `updateTime` (blob ts) changes → reliable edit detection
        - **Client-side publish**: Frontend creates blobs, signs them, publishes via `client.publish()`. Same flow for create/edit/delete
        - **API routing**: `SeedClient.request(key)` → HTTP `/api/{key}``APIQueries[key].getData(grpcClient)` → gRPC to daemon
        - **Delegation constraint**: Comment blobs are keyed by `(author, tsid)` in the DB. A different signer creates a different record. True cross-key delegation requires the gRPC `UpdateComment` endpoint (not client-side blobs). Phase 1 handles same-key editing; delegation is Phase 2.
        
        ---
        
        ## Phase 1: Core Editing (same-key)
        
        ### 1.1 Proto Changes
        **File**: `proto/documents/v3alpha/comments.proto`
        - Add `bool is_edited = 16;` to `Comment` message
        - Add `ListCommentVersions` RPC + request/response messages
        
        Then run: `./dev gen //proto/...`
        
        ### 1.2 Backend: `is_edited` flag
        **File**: `backend/api/documents/v3alpha/comments.go` — `commentToProto()` (line ~430)
        - Set `pb.IsEdited = cmt.Ts.Sub(createTime) > time.Second`
        - 1-second threshold avoids false positives from clock jitter on initial creation
        
        ### 1.3 Backend: `ListCommentVersions` endpoint
        **File**: `backend/api/documents/v3alpha/comments.go`
        - New SQL query `qListCommentVersions`: same as `qGetCommentByID` but without `LIMIT 1`, returns all versions ordered by `ts DESC`
        - Filter out tombstones (empty body). If latest version is tombstone, return NotFound
        - Implement `ListCommentVersions(ctx, req)` method on Server
        
        ### 1.4 Client: `updateComment` function
        **File**: `frontend/packages/client/src/comment.ts`
        - New `UpdateCommentInput` type: `{ commentId, targetAccount, targetPath, targetVersion, content, visibility?, replyParent?, threadRoot? }`
        - New `updateComment(input, signer)` function following `deleteComment` pattern:
          1. Extract TSID from `commentId.split('/')[1]`
          2. Build unsigned comment with `id: tsid`, `body: blocksToPublishable(content)`, signer, etc.
          3. Sign → CBOR encode → `toPublishInput(encoded, blobs)`
        - Export from `index.ts`
        
        ### 1.5 Frontend Types
        **File**: `frontend/packages/client/src/hm-types.ts`
        - Add `isEdited: z.boolean().optional()` to `HMCommentSchema`
        - Add `HMListCommentVersionsInputSchema`, `HMListCommentVersionsOutputSchema`, `HMListCommentVersionsRequestSchema`
        - Add to `HMGetRequestSchema` and `HMRequestSchema` discriminated unions
        
        ### 1.6 API Layer
        **File**: `frontend/packages/shared/src/api-comments.ts`
        - Add `ListCommentVersions` implementation: `grpcClient.comments.listCommentVersions({id})` → transform response to match schema
        **File**: `frontend/packages/shared/src/api.ts`
        - Import and register `ListCommentVersions` in `APIQueries`
        
        ### 1.7 Service Hooks
        **File**: `frontend/packages/shared/src/comments-service-provider.tsx`
        - Add `useUpdateComment()` mutation hook (follows `useDeleteComment` pattern)
          - Takes `{comment, newContent, signingAccountId}`
          - Creates update blob via `updateComment()`, signs, publishes
          - Invalidates comment query keys on success
        - Add `useCommentVersions(commentId)` query hook
          - Calls `client.request('ListCommentVersions', {id})`
          - Stale time 60s, enabled when commentId is truthy
        
        ### 1.8 Comment UI
        **File**: `frontend/packages/ui/src/comments.tsx`
        
        **"(edited)" label** — After `<CommentDate>` (line ~639):
        ```tsx
        {comment.isEdited && <span className="text-muted-foreground text-xs">(edited)</span>}
        ```
        
        **"Edit" menu item** — In options array (line ~551), before Delete, when `currentAccountId == comment.author`:
        ```tsx
        options.unshift({
          icon: <Pencil className="size-4" />,
          label: 'Edit',
          onClick: () => setIsEditing(true),
          key: 'edit',
        })
        ```
        
        **Inline edit mode** — New state `isEditing`. When true, replace `<CommentContent>` with `<CommentEditor>`:
        - Pass `initialBlocks={comment.content}` to pre-populate
        - Custom submit button: "Save" / "Cancel"
        - On save: call `updateCommentMutation.mutate(...)`, set `isEditing(false)` on success
        - Import `CommentEditor` from `@shm/editor/comment-editor`
        
        **Version history popover** — Make "(edited)" clickable, shows Popover with:
        - "Edited N times" header
        - List of versions from `useCommentVersions(comment.id)` — each shows relative timestamp
        - Clicking a version shows its content in the popover (read-only `CommentContent`)
        - Use Radix Popover, fetch on open
        
        ### 1.9 Backend Tests
        **File**: `backend/api/documents/v3alpha/comments_test.go`
        
        | Test | Verifies |
        |------|----------|
        | `TestCommentIsEdited` | `IsEdited=false` on create, `IsEdited=true` after update, `UpdateTime > CreateTime` |
        | `TestListCommentVersions` | Returns all versions (3 after 2 edits), newest first, same ID, different CIDs |
        | `TestListCommentVersions_NotFound` | Error for non-existent comment |
        | `TestListCommentVersions_DeletedComment` | Returns NotFound for deleted comment |
        
        ### 1.10 Frontend Tests
        **File**: `frontend/packages/client/__tests__/comment.test.ts`
        - `updateComment` produces valid blob with correct TSID and content
        
        ---
        
        ## Phase 2: Delegation (follow-up)
        
        For different keys that share delegation to edit each other's comments:
        
        ### Backend
        - Add `areDelegatesOfSameAccount(conn, principal1, principal2)` helper in `comments.go`
          - SQL: Check if both principals are delegates of the same account via capabilities (`type='Capability'`, `extra_attrs->>'del'`, `extra_attrs->>'role'`)
          - Reuse `qIsValidAgentKey` pattern from `backend/blob/index.go:807`
        - Relax auth in `UpdateComment` and `DeleteComment`: after exact match fails, call delegation check
        
        ### Frontend
        - Add `useMyAccountIds()` hook:
          - Desktop: calls `grpcClient.daemon.listKeys()` → extract principals
          - Web: calls `ListCapabilitiesForDelegate` with current key → extract issuer accounts
        - Replace `currentAccountId == comment.author` with `myAccountIds.includes(comment.author)`
        
        ### Backend Tests
        - `TestUpdateComment_WithDelegation`: delegate key can update comment of linked account
        - `TestDeleteComment_WithDelegation`: delegate key can delete comment of linked account
        
        ---
        
        ## Critical Files
        
        | File | Changes |
        |------|---------|
        | `proto/documents/v3alpha/comments.proto` | `is_edited` field, `ListCommentVersions` RPC |
        | `backend/api/documents/v3alpha/comments.go` | `commentToProto` edit detection, `ListCommentVersions` impl |
        | `backend/api/documents/v3alpha/comments_test.go` | New tests |
        | `frontend/packages/client/src/comment.ts` | `updateComment()` function |
        | `frontend/packages/client/src/hm-types.ts` | `isEdited` field, version schemas |
        | `frontend/packages/shared/src/api-comments.ts` | `ListCommentVersions` API handler |
        | `frontend/packages/shared/src/api.ts` | Register in `APIQueries` |
        | `frontend/packages/shared/src/comments-service-provider.tsx` | `useUpdateComment`, `useCommentVersions` hooks |
        | `frontend/packages/ui/src/comments.tsx` | Edit button, inline editor, "(edited)" label, version popover |
        
        ## Reusable Code
        
        - `deleteComment()` in `comment.ts:395` — pattern for `updateComment()` (TSID extraction, blob signing)
        - `CommentEditor` in `@shm/editor/comment-editor` — reuse for inline editing with `initialBlocks`
        - `useDeleteComment()` in `comments-service-provider.tsx:144` — pattern for `useUpdateComment()` mutation
        - `qGetCommentByID` in `comments.go:375` — base for `qListCommentVersions` SQL
        - `qIsValidAgentKey` in `blob/index.go:807` — pattern for delegation check SQL
        
        ## Verification
        
        1. **Backend**: `go test ./backend/api/documents/v3alpha/ -run TestComment -v`
        2. **Proto gen**: `./dev gen //proto/...` must succeed
        3. **Frontend typecheck**: `pnpm typecheck` from repo root
        4. **Frontend tests**: `pnpm test` from repo root
        5. **Manual test**: Create comment → edit it → verify "(edited)" appears → click to see version history → verify old content shown
        6. **Auth test**: Log in as different account → verify edit button NOT shown on others' comments
        
        ---
        
        ## Phase 3: Edited/Deleted Comment Links
        
        ### Context
        
        Comment URLs use record IDs (`UID/TSID`), not CIDs. They always resolve to the latest version. When a comment has been edited, the link still works but the reader has no indication (in embeds) that the content changed. When a comment has been deleted, navigation fails with a generic "Page not found" and embeds show `DeletedEmbedBanner` without content.
        
        Goal: Links to edited comments show "(edited)" in embeds. Deleted comments show "deleted" message instead of generic error on navigation.
        
        ### 3.1 "(edited)" indicator on comment embeds
        
        **File**: `frontend/packages/ui/src/blocks-content.tsx``CommentEmbedHeader` (~line 1848)
        
        After the date display, add "(edited)" text:
        ```tsx
        {comment.createTime ? (
          <SizableText size="sm" color="muted">
            {formattedDateMedium(comment.createTime)}
          </SizableText>
        ) : null}
        {comment.isEdited ? (
          <SizableText size="xs" className="text-muted-foreground">(edited)</SizableText>
        ) : null}
        ```
        
        This is a 3-line addition. The `comment` object already has `isEdited` available.
        
        ### 3.2 Deleted comment navigation — graceful handling
        
        **File**: `frontend/packages/ui/src/resource-page-common.tsx``CommentResourcePage` (~line 367)
        
        Currently, when a comment is deleted (tombstone), the resource loader fails before `CommentResourcePage` renders, and the page shows generic "Page not found". This is handled at the route/loader level:
        
        - **Web** (`frontend/apps/web/app/loaders.ts`): The server loader fetches the comment resource. If deleted/not-found, it should return a flag indicating deletion rather than a hard error.
        - **Desktop**: Resource loading happens via `useResource` hook which returns `data.type === 'tombstone'`.
        
        For the web route handler, when the comment resource returns tombstone/not-found, pass a `commentDeleted: true` flag to the page component. The page should render a "This comment has been deleted" message with a link to the target document (if the target doc info was available in previous version data).
        
        However, this is tricky because tombstoned comments return no data — we don't know the target document. Two options:
        
        **Option A (Simple)**: Show "This comment has been deleted" with no link. The user sees a clear message instead of generic 404.
        
        **Option B (Better UX)**: Add a backend endpoint or modify `GetResource` to return `CommentDeleted` status with minimal metadata (target doc ID) even for tombstoned comments. This requires backend changes.
        
        **Recommended: Option A** for now — it's a meaningful improvement over the current "Page not found" with no backend changes.
        
        **Implementation**: In the web loader (`loaders.ts`), when `loadResourceForComment` gets a not-found/error, check if the ID looks like a comment (has TSID path segment). If so, return `{commentDeleted: true}` instead of throwing. In the page component, show:
        ```tsx
        <div className="flex flex-col items-center justify-center gap-4 py-16">
          <AlertCircle className="size-8 text-amber-500" />
          <SizableText className="text-muted-foreground">This comment has been deleted</SizableText>
        </div>
        ```
        
        ### Critical Files for Phase 3
        
        | File | Changes |
        |------|---------|
        | `frontend/packages/ui/src/blocks-content.tsx` | Add "(edited)" to `CommentEmbedHeader` |
        | `frontend/apps/web/app/loaders.ts` | Handle deleted comments gracefully |
        | `frontend/packages/ui/src/resource-page-common.tsx` | Optional: deleted comment page state |
        
        ### Verification for Phase 3
        
        1. Create a comment → edit it → embed it in another document → verify "(edited)" shows in the embed
        2. Delete a comment → navigate to its URL → verify "This comment has been deleted" message (not generic 404)
        3. Delete a comment → embed its URL → verify `DeletedEmbedBanner` shows (already works)
        
        ## Open Questions
        
        None — Phase 2 (delegation) is clearly scoped as follow-up work. Phase 3 Option B (returning target doc metadata for deleted comments) can be revisited later.

        It looked correct to me so I approve it. (YOLO)

      First result

        after the first result, I got this:

        So I asked to make some changes:

        great. the only thing I would change is where the content of the previous versions of the comments is shown. What GitHub does is to show the previous content on a dialog. but what if we show the content of the previews versions in the same place the current comment is shown but with a different background? maybe a warning/yellow background and a button to remove it and show the current comment back?

        I also added this image:

      CC Iteration

        here are other prompts I send based on the results I was seeing:

        right now the current version item inside the edit dropdown has the “current version” label. I think it would be better to make the versions numbered (“Version X”) and add a tag-like label on the side to the current version. also the current version item should not have a click handler, because we are already looking at the current version by default
        the problem now is that if I get a link to a deleted comment, I either get document not found or I can’t embed it. Both opening and embedding should be possible, but with proper labelling:
        - if I paste a link to a block in a deleted comment, then I should go to the place where the new comment version is, and show it as if I click the previous version item in the edited dropdown.
        - if I paste a link to a block in a deleted comment to embed, I should be able to embed it but the comment embed should show that there’s a new version of the comment.
        
        Basically links to edited comments should work as normal comments, BUT the reader should also see that de document was either deleted or edited.
        
        can you make this changes please?
        is there a way we can show the deleted content? specially on desktop because the content might be in the local node. on web we can leave it like that because the owner’s decision might be to remove/edit the comment.
        I still can’t either embed or open an older version of a comment. did you try to do something about that?
        currently this is a URL of a block in a edited comment. can you check with it? https://gabo.es/:comments/z6Mkfm8befyW32tingJymhfqjX6nhLwoBkQxdSmqg9DnqmiH/z6FukAddvDAUpc#kwfkgLKk+
        inside the comment embed of a previous version we are not showing that is an old comment. do we have access to this information inside the comment embed? If so please add the same edited dropdown or similar and make sure is clear that the reader understands the current embedded comment is old or has a newer version.
        now editing a comment throws an error:
        
        comments.tsx:735 Error: Non-base58btc character
            at updateComment (comment.ts:467:59)
            at async Object.mutationFn (comments-service-provider.tsx:208:28)
        on web I’m not able to edit my comments. is that correct?
        I have the new account created with the vault on web and I just created a comment and I don’t see the delete/edit buttons in it. maybe the delegated account (old link keys) and new vault accounts are a bit different? we need to support both, but the vault account should be the default way of handling with accounts specially on web.
        I don’t see the “(edited)” dropdown in comments on web. was that intentional?
        I was able to edit on web. but I don’t see the edited dropdown to see all the comment versions. not even after reloading the page with the edited comment

        Here's where Alex Burdiyan told me about the backend changes he suggested and I said this:

        I think the `is_edited` boolean us unnecessary. because we can derive this data by comparing create_time and update_time. if those are not the same then we know was edited right?

    Conclusion

      That's it. this is the process I follow for most features. I need to be honest, this initial prompt was very small compared to others i've done. but the job was done and the way I wanted to!.

      Hope this helps understand how I'm doing things, so we can maybe improve the planning phase collectively and ship code faster/collaboratively! (something Gabo H Beaumont just mentioned to me here at the office).