162 lines
7.5 KiB
Markdown
162 lines
7.5 KiB
Markdown
---
|
|
status: current
|
|
system: ui
|
|
requirements:
|
|
- REQ-UI-PLAN-COMMENT-DRAFTS-001
|
|
---
|
|
|
|
# Plan Comment Drafts System Design
|
|
|
|
## Purpose and boundaries
|
|
|
|
This design keeps session-scoped pending plan comments safe while the shared
|
|
task Plan editor changes its visible comment projection. The UI system owns the
|
|
browser-local comment store, Tiptap marks, session-selection response, and
|
|
composer context. The task system continues to own plan content, revisions,
|
|
sessions, and message delivery.
|
|
|
|
No backend, API, WebSocket, task-plan schema, or comment data-model change is
|
|
required. `PlanComment.sessionId` remains the delivery and persistence owner.
|
|
|
|
## Root cause and evidence
|
|
|
|
`useTaskPlan(taskId)` correctly treats the plan document as task-scoped.
|
|
`TaskPlanPanel` separately calls `usePlanComments(activeSessionId)`, so selecting
|
|
a sibling Agent session replaces the editor's `comments` prop with that
|
|
session's projection.
|
|
|
|
Before this repair, `rehydrateCommentMarks` removed every Tiptap `commentMark`
|
|
absent from the new projection without recording why. That programmatic
|
|
document transaction reached the `CommentMark` orphan detector. The detector
|
|
interpreted the removed mark as a destructive edit and called
|
|
`onOrphanedComments`. `TaskPlanPanel.handleCommentDeleted` then removed the
|
|
comment from the Zustand store. `persistSessionComments` removed the owning
|
|
session's `sessionStorage` key when no comments remained, making the loss
|
|
permanent.
|
|
|
|
A focused temporary Vitest reproduction rendered one comment, rerendered the
|
|
same editor with an empty comment projection, and observed
|
|
`onCommentDeleted([commentId])`. It passed against the current code. The test
|
|
was removed after diagnosis; the implementation work order replaces it with a
|
|
permanent failing-first regression.
|
|
|
|
## Requirement mapping
|
|
|
|
| Requirement | Design section |
|
|
| --- | --- |
|
|
| `REQ-UI-PLAN-COMMENT-DRAFTS-001` | [State and persistence](#state-and-persistence), [Transaction provenance](#transaction-provenance), [Session-switch flow](#session-switch-flow), [Verification](#verification) |
|
|
|
|
## Components and responsibilities
|
|
|
|
- `useCommentsStore` remains the authoritative browser-local comment state and
|
|
persists each session under `kandev.comments.<sessionId>`.
|
|
- `usePlanComments` selects only `PlanComment` records owned by the selected
|
|
session and creates new comments for that session.
|
|
- `TaskPlanPanel` projects the selected session's comments into the shared Plan
|
|
editor and clears transient selection/edit state when session identity
|
|
changes.
|
|
- `TipTapPlanEditor` asks `rehydrateCommentMarks` to reconcile rendered marks
|
|
with the supplied projection.
|
|
- `CommentMark` distinguishes projection-reconciliation transactions from
|
|
user-authored document transactions before reporting orphaned comments.
|
|
- Task chat continues to include and clear only the active session's pending
|
|
plan comments after successful delivery.
|
|
|
|
## State and persistence
|
|
|
|
The existing `CommentBase.sessionId`, `CommentsState.bySession`, and
|
|
`kandev.comments.<sessionId>` storage keys remain unchanged. Switching sessions
|
|
does not migrate or clone comments. The selected session controls which comment
|
|
IDs become editor marks and composer context.
|
|
|
|
An empty projection means no plan-comment marks are rendered for the selected
|
|
session. It does not mean comments belonging to another session were deleted.
|
|
Returning to the owning session reads its unchanged store entries and
|
|
rehydrates their marks from saved positions or selected-text fallback search.
|
|
|
|
## Transaction provenance
|
|
|
|
`rehydrateCommentMarks` shall tag the transaction it dispatches with a private
|
|
comment-projection reconciliation marker. The `CommentMark` orphan-detection
|
|
plugin shall ignore mark removals performed by tagged transactions.
|
|
|
|
Untagged user document transactions retain current orphan cleanup. For every
|
|
untagged document-changing transaction, the plugin compares comment IDs before
|
|
and after that transaction, then reports only IDs still absent from the final
|
|
document state. This keeps a real destructive edit authoritative without
|
|
mistaking projection changes for user intent.
|
|
|
|
Explicit deletion remains safe: the UI removes the rendered mark and store
|
|
entry as one user action, and any deferred orphan cleanup is idempotent. Tagged
|
|
additions and removals from projection changes never affect store state.
|
|
|
|
## Session-switch flow
|
|
|
|
1. The user adds a plan comment while session A is selected. Store and
|
|
`sessionStorage` record session A as owner.
|
|
2. The user selects session B. `usePlanComments` publishes session B's comment
|
|
projection.
|
|
3. Tiptap dispatches one tagged reconciliation transaction that removes A's
|
|
visible marks and adds B's marks. Orphan cleanup ignores those presentation
|
|
changes.
|
|
4. `TaskPlanPanel` closes any open comment editor and clears global
|
|
`editingCommentId` plus local text-selection state for the old session.
|
|
5. Selecting session A publishes its unchanged comments. Tagged reconciliation
|
|
restores their marks, badges, and editable feedback; task chat restores the
|
|
same session's context chip.
|
|
|
|
## Failure and recovery
|
|
|
|
- If mark position metadata is stale after plan editing, existing selected-text
|
|
fallback search remains responsible for finding the range.
|
|
- If a destructive user edit removes a mark, untagged orphan detection removes
|
|
the associated pending comment as before.
|
|
- A failed direct, queued, or passthrough send leaves comment state unchanged.
|
|
- A session switch during an open editor closes the editor rather than applying
|
|
its draft to another session.
|
|
- Comments already deleted by the old path have no backend copy and cannot be
|
|
reconstructed by this repair.
|
|
|
|
## Responsive behavior
|
|
|
|
Desktop and mobile reuse the same comment store, Plan editor, mark extension,
|
|
and session identity. The repair changes no composition, overlay geometry,
|
|
scroll owner, safe-area handling, or touch target. Existing desktop Popover and
|
|
mobile Drawer presentation remain unchanged.
|
|
|
|
Because this is state and transaction normalization inside the shared editor,
|
|
focused unit coverage plus one desktop multi-session E2E flow provides the
|
|
cross-viewport logic evidence. No new mobile Playwright scenario is required;
|
|
existing mobile Plan comment coverage continues to own touch presentation.
|
|
|
|
## Verification
|
|
|
|
- `tiptap-plan-editor.test.tsx` changes the `comments` projection and proves the
|
|
previous comment is visually unmounted without invoking deletion. A paired
|
|
case performs a real destructive editor transaction and proves orphan cleanup
|
|
still fires.
|
|
- A focused `TaskPlanPanel` test changes active session identity while a comment
|
|
editor is open and proves transient editing state closes without mutating the
|
|
old comment.
|
|
- `multi-session-ux.spec.ts` creates a plan and two sessions, adds a pending
|
|
comment in the primary session, selects the sibling, returns to the primary,
|
|
and proves the badge and feedback text are restored.
|
|
|
|
No production telemetry is added. The failure is deterministic and covered at
|
|
the transaction, component, and user-flow boundaries.
|
|
|
|
## Alternatives considered
|
|
|
|
- Keying Tiptap editor instances by session would avoid the callback by
|
|
destroying the editor, but it would add expensive remounts and focus/selection
|
|
churn to a task-scoped document.
|
|
- Guarding deletion only in `TaskPlanPanel` would leave transaction provenance
|
|
ambiguous and remain vulnerable to deferred callback races.
|
|
- Making plan comments task-scoped would change which Agent receives pending
|
|
prompt context and is outside this repair.
|
|
|
|
## Related decisions
|
|
|
|
None. The design separates programmatic reconciliation from existing user
|
|
intent without creating a new public or architectural boundary.
|