5.1 Comments + @mentions
Comments turn an issue from a record into a conversation — the first Epic-5 collaboration surface, and the one the detail page has reserved a slot for since Story 2.4 (the Activity placeholder: "Comments coming in Epic 5"). This story ships the comment model + service, the thread UI in that slot, @mentions over workspace members in the rich-text editor (comments AND description — Jira-faithful), and the mention → email notification hook through the Story 1.6 job pipeline.
The Jira-verified shape (decision-ladder rung 1, checked against Atlassian sources at plan time — not memory). Comments are threaded, one level deep: Jira Cloud GA-ed comment replies (~April 2025) as single-level child comments — a reply to a reply attaches to the same thread (and pre-fills an @mention of that reply's author, Jira's auto-tag behaviour); threads never nest deeper. Ordering is oldest-first by default with a per-user newest/oldest toggle. An edited comment carries an "Edited" tag (only the latest version is kept — no edit history). Delete is a hard delete — no tombstone, no content retention; what remains is a work_item_revision row recording that a comment was deleted and by whom (the History trace Story 5.5's activity feed renders). Permissions map Jira's five comment permissions (Add / Edit own / Edit all / Delete own / Delete all) onto the shipped 6.4 role model: project admin/member (who can view the project) add comments; authors edit/delete their own; project admin + workspace admin/owner edit/delete any; the read-only viewer role cannot comment (rung 2 — the shipped viewer contract is read-only).
Mentions. Typing @ in the editor opens a member picker (the Combobox option-row vocabulary) over the members who can VIEW the issue — for open/limited projects the workspace membership, for private projects the project members (exactly the 6.4 assignableMembersService scoping; mention candidates reuse that read). Mentions serialize into the stored Markdown as a durable token — [@Display Name](mention:<userId>) — so the body stays plain Markdown (rung: one storage format, no parallel rich-text blob), renders as a user chip in MarkdownView, and is parseable server-side. The service is the authority on mentions: on every comment write it parses the body, validates each mentioned user is a workspace member who can view the issue (silently dropping the rest — the Jira rule: no view permission → no notification), and persists comment_mention rows in the same transaction — the queryable substrate ("comments mentioning me") Epic 6 search can filter on. Because the mention capability lives in the shared MarkdownEditor primitive (2.3.5), the description field gets mentions for free — and description-mention notifications ride the same parse helper on the work-item write path (Jira notifies description mentions too).
Notifications (email here; in-app is Story 5.7). Comment writes emit a channel-agnostic job event after the transaction commits (work-item/comment.created carrying workspace, issue, comment, author, and the mentioned-user ids; the description path emits work-item/mentioned). An Inngest job (the 1.6 defineJob harness) fans out mention emails via the shipped email pipeline: per mentioned user — never the author themselves — it re-validates view access at send time, renders a mentionNotification template (who mentioned you, the issue identifier + title, a plain-text excerpt, a deep link), and sends idempotently (one notification per comment × user, replay-safe). The event payload is shaped so Story 5.4 (watcher notifications) and Story 5.7 (the in-app bell) consume the SAME events later without reshaping.
Scale (finding #57 — never load-all). A years-old issue can hold hundreds of comments; Jira's issue view loads only the newest few and hides the rest behind "Show more comments". The list read is cursor-paginated from the most recent (page size 20) with a total count; the UI renders the newest page in the active sort direction plus a "Show more comments (N older)" affordance that extends backward — never a fetch-everything read. Replies load with their thread; long threads collapse their middle behind "Show more replies" (the Jira auto-collapse).
Completeness — the real-product states. Empty (no comments yet — an inviting, non-blank state), loading (skeleton rows in the Activity slot), error (ErrorState + retry), the composer's submitting/disabled states, the "Edited" tag, the delete confirm (naming the reply count when deleting a thread root — deleting a root deletes its replies, a deliberate decision recorded in 5.1.2 since the mirror behaviour is unverifiable), the viewer (read-only) state — thread visible, no composer — and a deleted/permission-denied mention target degrading gracefully. All drawn by 5.1.3, asserted in 5.1.7.
Out of scope (documented extension slots, each justified): per-comment visibility restriction (Jira's padlock — needs the company-managed role/group substrate; Jira's team-managed projects, the shape Motir mirrors for small teams, do not support it; Epic-6 admin territory); realtime live-updating comments (no realtime substrate in the codebase; a product-wide decision, not a story-local improvisation); the in-app notification center (Story 5.7 — added as a stub in this same planning pass); watcher notifications on every comment (Story 5.4 owns watchers; the 5.1.6 event payload already carries what it needs); comment reactions/emoji, edit history, and rendering field-change history in the Activity feed (Story 5.5). The Activity section ships with the comments stream and a documented seam where 5.5 adds the History filter.
Verification
- Pull the Story branch,
pnpm install,pnpm prisma migrate dev(applies the 5.1.1comment+comment_mentionmigration cleanly; a re-run reports "No difference detected" — both FKs are modelled as Prisma@relations, no drift),pnpm db:seed,pnpm dev. pnpm test:coverage— Vitest (real Postgres, no mocks exceptgetSession) overcommentsService(add/edit/delete/list/permissions/mention parsing) stays ≥90% per-file branch/fn/line on the new service/repo files (the CI coverage gate); every new repo method has a direct empty-input-guard test.- Comment flow: sign in as
zhuyue@motir.co/!QAZ1qaz, open any issue → the Activity section shows the comment thread + composer (matchingdesign/work-items/comments.mock.html). Add a comment → it appears with your avatar + relative time. Edit it → the "Edited" tag shows. Reply to it → the reply nests one level; replying to the reply attaches to the same thread with the author pre-mentioned. Delete the root → the confirm names the reply count; the thread disappears (hard delete) and the revision trail records the deletion. - Mention flow: type
@in the composer → the member picker opens over the project's viewable members; pickbophilips@motir.co→ the chip renders in the posted comment; the dev email console ([EMAIL]line) shows the mention notification to Bo (subject naming you and the issue identifier) — and NO email when you mention yourself. A mention in the issue DESCRIPTION (edit form) notifies the same way. - Permission checks: as
eikooc@motir.co(member) — can add, can edit/delete OWN comment only (no edit/delete affordance on others'); as a projectviewer— thread visible, no composer; as project admin / workspace owner — can delete anyone's comment. - Scale check (finding #57): seed an issue with 100+ comments (
pnpm db:seed:largeor the 5.1.7 fixture) → the detail page loads only the newest 20 with "Show more comments (N older)"; clicking extends backward; the network read is cursor-paged, never the full set; the sort toggle flips oldest/newest-first. pnpm test:e2e --grep comments— Playwright over the real stack: comment → mention → email assertion, edit/delete/reply, show-more at scale, sort toggle, viewer read-only.- a11y check: the comments surface passes the strict axe sweep (composer labelled, the mention picker keyboard-navigable with the Combobox listbox semantics, "Edited"/timestamps conveyed as text, focus returns after post/delete); colour via
--el-*, shape via element shape tokens.