Hermes 284313f908
Some checks are pending
Bidi Control Character Guard / bidi-control-guard (push) Waiting to run
Circular Dependency Check / Check for new circular dependencies (push) Waiting to run
Citus Migration Smoke / Combined migrations on single-node Citus (push) Waiting to run
E2E Fresh Install Tests / fresh-install-e2e (push) Waiting to run
ext-v2 guardrails / Run ext-v2 guard and ESLint (push) Waiting to run
Integration Tests / Check for relevant changes (push) Waiting to run
Integration Tests / ${{ (github.event_name == 'schedule' || github.event.inputs.suite == 'full') && 'Full integration suite' || 'Tier-1 integration subset' }} (push) Blocked by required conditions
Mobile checks / Mobile lint + typecheck (push) Waiting to run
Mobile checks / Mobile unit tests (push) Waiting to run
Mobile checks / Mobile dependency audit (report) (push) Waiting to run
Mobile checks / Mobile reproducibility checks (push) Waiting to run
Secrets guard (env backups) / Ensure no tracked env backup files (push) Waiting to run
Temporal Readiness / fast-readiness (push) Waiting to run
Temporal Readiness / docker-parity (push) Waiting to run
TypeScript Type Check / Nx affected typecheck (push) Waiting to run
Unit Tests / Skipped-test budget (push) Waiting to run
Unit Tests / Nx affected unit tests (push) Waiting to run
Unit Tests / Server unit coverage (informational) (push) Waiting to run
Validate Tenant Management Schema / Check for relevant changes (push) Waiting to run
Validate Tenant Management Schema / Validate Tenant Management Schema (push) Blocked by required conditions
EE Workflows Build Guard / ee-workflows-build-guard (push) Waiting to run
Initial import of AlgaPSA codebase from PSA server
Excluded: .git, node_modules, secrets/, compose.env, assemblyscript tgz

Source: /opt/alga-psa on psa.joliet.tech
2026-06-22 16:12:17 -05:00

62 KiB

Scratchpad — Threaded Comment Responses

  • Plan slug: 2026-05-13-threaded-comments
  • Created: 2026-05-13

What This Is

Working notes for implementing nested/threaded comment responses on tickets and project tasks, plus the supporting first-class comment_threads entity that carries email-thread identity. See PRD.md for scope, features.json for the commit-sized work list, tests.json for the test plan.

Decisions

  • (2026-05-13) Thread as first-class entity. Every top-level comment is the head of a comment_threads row. Inbound/outbound email correlation happens at thread granularity, not ticket. New "Add Comment" always creates a new thread; existing flat comments backfill as single-comment threads.
  • (2026-05-13) F001 migration split. Created comment_threads in 20260513100000_create_comment_threads.cjs with only the base table, parent CHECK, and ticket/task FKs. Deferred indexes, comment-column FKs, and backfill to their own checklist migrations to preserve the PRD's staged rollout and make each commit reviewable.
  • (2026-05-13) Hybrid (Nested + collapsible drawer) is the only production reply model. Other modes from the prototype (Flat, Single-level only, Deep nesting only, Quote-reply, Side-panel only) are exploration-only and not shipping. The prototype's Tweaks panel is not part of production.
  • (2026-05-13) Scope: Tickets + project tasks together in one PR. Email integration is full bidirectional (inbound matching + outbound RFC headers + reply tokens) for tickets only — tasks don't accept inbound email today.
  • (2026-05-13) Backfill model: one thread per existing comment. Cleanest cut-over; visually identical to today (no thread bars on legacy comments).
  • (2026-05-13) Depth cap: visual indent capped at 4 levels (matches the design prototype); data has no cap so the model accommodates real-world deep threads.
  • (2026-05-13) Soft-delete only for roots with children. Leaf comments hard-delete as today. Soft-delete = deleted_at set + note = '[deleted]'. Keeps tree well-formed without orphaning children.
  • (2026-05-13) Visibility invariant: a thread's is_internal flag denormalizes from its root. Reply must be compatible with root (can't make a reply client-visible inside an internal thread, etc.). Inbound emails inherit thread visibility — clients can only reply to client-visible threads.
  • (2026-05-13) Inbound resolution precedence: reply-token > In-Reply-To > References (end→start) > provider thread id > ticket-fallback (new top-level thread). First match wins.

Discoveries / Constraints

  • (2026-05-13) F002 indexes. Added 20260513100500_add_comment_threads_indexes.cjs with comment_threads_ticket_idx, comment_threads_task_idx, and partial comment_threads_email_msgid_idx. The list indexes include last_activity_at DESC because thread ordering is by thread activity, not individual comment chronology.
  • (2026-05-13) F003 ticket comment columns. Added nullable comments.thread_id, comments.parent_comment_id, and comments.deleted_at in 20260513101000_add_threading_columns_to_comments.cjs. thread_id is intentionally nullable until the backfill and F007 enforcement migration. Added tenant-scoped FK to comment_threads plus self-FK for parent comments.
  • (2026-05-13) F004 project task comment columns. Added matching nullable threading columns to project_task_comments in 20260513101500_add_threading_columns_to_project_task_comments.cjs, with FK to comment_threads and a self-FK on (tenant, parent_comment_id).
  • (2026-05-13) F005 ticket comment backfill. Added chunked backfill in 20260513102000_backfill_comment_threads_for_comments.cjs. Legacy ticket comments use thread_id = comment_id, which makes reruns idempotent via ON CONFLICT (tenant, thread_id) DO NOTHING and avoids a temporary mapping table. email_message_id is populated from metadata->'email'->>'messageId'.
  • (2026-05-13) F006 task comment backfill. Added 20260513102500_backfill_comment_threads_for_project_task_comments.cjs using the same deterministic legacy ID pattern (thread_id = task_comment_id). Task threads set project_task_id, leave ticket_id null, and use is_internal=false.
  • (2026-05-13) F007 NOT NULL enforcement. Added 20260513103000_enforce_comment_thread_ids_not_null.cjs. It raises a clear migration exception if either comment table still has null thread_id values, then alters both columns to NOT NULL.
  • (2026-05-13) F008 email log linkage. Added nullable email_sending_logs.comment_thread_id in 20260513103500_add_comment_thread_id_to_email_sending_logs.cjs with tenant-scoped FK to comment_threads. Left existing email_sending_logs.thread_id untouched as the provider thread id and added a partial (tenant, comment_thread_id, created_at DESC) index for outbound latest-message lookup.
  • (2026-05-13) F009 type contract. Added packages/types/src/interfaces/commentThread.interface.ts with ICommentThread covering all comment_threads columns and exported it from packages/types/src/interfaces/index.ts. Verified with npx tsc -p packages/types/tsconfig.json --noEmit.
  • (2026-05-13) F010 ticket comment type fields. Extended IComment with thread_id, parent_comment_id, and deleted_at. Kept them optional at the interface boundary because IComment is currently used for both persisted rows and create payloads; runtime model code will guarantee thread_id for persisted reads after F012-F016. Verified with npx tsc -p packages/types/tsconfig.json --noEmit.
  • (2026-05-13) F011 task comment type fields. Extended IProjectTaskComment with camelCase threadId, parentCommentId, and deletedAt, matching the existing task-comment interface convention while mapping to DB columns thread_id, parent_comment_id, and deleted_at in actions/models. Kept fields optional for create payload compatibility. Verified with npx tsc -p packages/types/tsconfig.json --noEmit.
  • (2026-05-13) F012 top-level ticket comment creation. Updated packages/tickets/src/models/comment.ts so top-level inserts generate comment_id + thread_id, insert comment_threads first, then insert comments.thread_id in the same transaction. Also updated shared/models/ticketModel.ts direct comment creation for the same NOT NULL requirement; otherwise workflow/email paths would fail after F007. Reply parent resolution remains guarded for F013. Verified with npx tsc -p packages/types/tsconfig.json --noEmit and npx tsc -p packages/tickets/tsconfig.json --noEmit.
  • (2026-05-13) F013 ticket reply parent resolution. Updated Comment.insert to resolve parent_comment_id against the same tenant and ticket, reject soft-deleted parents, inherit the parent's thread_id, and default/validate is_internal against comment_threads.is_internal. Verified with npx tsc -p packages/tickets/tsconfig.json --noEmit.
  • (2026-05-13) F014 ticket reply counters. Comment.insert now increments comment_threads.reply_count and updates last_activity_at after successful reply insert, in the same transaction supplied by the action/model caller. Verified with npx tsc -p packages/tickets/tsconfig.json --noEmit.
  • (2026-05-13) F015 ticket comment delete semantics. Comment.delete now checks for children. Comments with children are soft-deleted with note/markdown_content = '[deleted]' and deleted_at set. Leaf replies hard-delete and decrement reply_count; leaf roots hard-delete and remove their now-empty comment_threads row. Verified with npx tsc -p packages/tickets/tsconfig.json --noEmit.
  • (2026-05-13) F016 ticket comment reads. No code change needed: Comment.getAllbyTicketId already selects comments.*, filters only by tenant/ticket, and orders by comments.created_at ASC, so the new thread_id, parent_comment_id, and deleted_at columns are returned and soft-deleted comments remain in the result.
  • (2026-05-13) F017 top-level task comment creation. Updated createTaskComment to validate the task first, generate task_comment_id + thread_id, insert comment_threads with project_task_id and is_internal=false, then insert project_task_comments.thread_id in the same transaction. Reply parent resolution remains guarded for F018. Verified with npx tsc -p packages/projects/tsconfig.json --noEmit.
  • (2026-05-13) F018 task reply parent resolution. createTaskComment now resolves parentCommentId, validates the parent is on the same task and not deleted, inherits thread_id, and inserts the reply with parent_comment_id. Verified with npx tsc -p packages/projects/tsconfig.json --noEmit.
  • (2026-05-13) F019 task reply counters. Task replies now increment comment_threads.reply_count and bump last_activity_at in the same transaction after successful insert. Verified with npx tsc -p packages/projects/tsconfig.json --noEmit.
  • (2026-05-13) F020 task comment delete semantics. deleteTaskComment now soft-deletes task comments with children, hard-deletes leaves, decrements reply_count for leaf replies, and removes empty root thread rows. Reactions are still explicitly deleted before hard-delete for Citus compatibility. Verified with npx tsc -p packages/projects/tsconfig.json --noEmit.
  • (2026-05-13) F021 task comment reads. getTaskComments now maps thread_id, parent_comment_id, and deleted_at from project_task_comments.* into threadId, parentCommentId, and deletedAt on IProjectTaskCommentWithUser. Verified with npx tsc -p packages/projects/tsconfig.json --noEmit.
  • (2026-05-13) F022 ticket create action pass-through. No code change needed: after F010, createComment(comment: Omit<IComment, 'tenant'>) accepts optional parent_comment_id; the action copies the payload into commentToInsert and passes it to Comment.insert, which resolves the parent in F013.
  • (2026-05-13) F023 ticket comment event payload. TICKET_COMMENT_ADDED now includes thread_id, parent_comment_id, and is_reply at the payload level and inside the legacy comment object. The action fetches the inserted comment after model insert so the event uses the resolved thread id. Verified with npx tsc -p packages/tickets/tsconfig.json --noEmit.
  • (2026-05-13) F024 ticket reply visibility enforcement. No extra action code needed: createComment already rejects internal comments from non-internal authors, and F013 model enforcement rejects any reply whose is_internal differs from comment_threads.is_internal (including client-visible reply inside an internal thread and internal reply inside a client thread).
  • (2026-05-13) F025 task create action parent payload. createTaskComment now accepts parent_comment_id in addition to camelCase parentCommentId and normalizes both to the same parent resolution path. Verified with npx tsc -p packages/projects/tsconfig.json --noEmit.
  • (2026-05-13) F026 task comment event payload. TASK_COMMENT_ADDED now includes both camelCase and snake_case thread fields: threadId/thread_id, parentCommentId/parent_comment_id, and isReply/is_reply. Verified with npx tsc -p packages/projects/tsconfig.json --noEmit.
  • (2026-05-13) F027 task reply ownership. No code change needed: task replies use the same project_task_comments.user_id ownership field as top-level comments, so assertOwnCommentOrInternalUser already allows internal users and the reply's owner while rejecting other client users.
  • (2026-05-13) F028 inbound reply-token thread routing. Added resolveReplyTargetFromComment in processInboundEmailInApp so a reply token tied to a comment_id resolves that comment's thread_id and uses the latest comment in that thread as parent_comment_id. Extended createCommentFromEmail and shared TicketModel.createComment to create replies when parent_comment_id is supplied, inheriting thread visibility and incrementing counters. Verified with npx tsc -p shared/tsconfig.json --noEmit.
  • (2026-05-13) F029 inbound In-Reply-To routing. Added resolveReplyTargetFromOutboundMessageId to look up email_sending_logs.rfc_message_id, read comment_thread_id, resolve the latest comment in that thread, and pass it as parent_comment_id for inbound replies. This runs before legacy ticket-level header matching. Verified with npx tsc -p shared/tsconfig.json --noEmit.
  • (2026-05-13) F030 inbound References routing. Added reverse-order References[] lookup using the same email_sending_logs.rfc_message_id resolver. If In-Reply-To is absent or does not resolve, the first matching reference from the end of the chain routes to that comment thread. Verified with npx tsc -p shared/tsconfig.json --noEmit.
  • (2026-05-13) F031 inbound provider thread routing. Added resolveReplyTargetFromProviderThreadId, which matches emailData.threadId to comment_threads.email_provider_thread_id for ticket threads and routes to the latest comment in that thread before falling back to legacy ticket-level matching. Verified with npx tsc -p shared/tsconfig.json --noEmit.
  • (2026-05-13) F032 inbound ticket fallback. No additional code needed after F028: legacy ticket-level header matching still calls createCommentFromEmail without parent_comment_id, and shared TicketModel.createComment now treats that as a new top-level thread. This preserves today's ticket-level fallback while avoiding accidental attachment to an existing comment thread.
  • (2026-05-13) F033 inbound comment creation. Thread-specific inbound resolution now always passes the latest comment in the resolved thread as parent_comment_id; TicketModel.createComment stores the inherited thread_id and parent_comment_id on the new comment. Top-level fallback omits parent_comment_id and creates a fresh thread.
  • (2026-05-13) F034 outbound top-level Message-ID. BaseEmailService now generates a RFC Message-ID header for comment emails when the caller did not provide one, returns/logs that RFC id, and updates comment_threads.email_message_id for successful sends where the associated comment is a top-level/root comment. No In-Reply-To header is added in this top-level path. Verified with npx tsc -p packages/email/tsconfig.json --noEmit.
  • (2026-05-13) T039 outbound top-level Message-ID test. Added server/src/test/unit/email/baseEmailServiceCommentThreading.test.ts with a mocked provider and tenant knex. It verifies a top-level comment email gets a generated RFC Message-ID, does not emit In-Reply-To/References, returns that RFC id, and stores it on comment_threads.email_message_id with the provider thread id. Verified with cd server && npx vitest run src/test/unit/email/baseEmailServiceCommentThreading.test.ts -t "T039".
  • (2026-05-13) F035 outbound reply In-Reply-To. Added best-effort header enrichment in BaseEmailService: when sending an email for a reply comment (comments.parent_comment_id set), it looks up the latest successful email_sending_logs.rfc_message_id for that comment_thread_id and sets In-Reply-To unless the caller already supplied one. Top-level comments remain unchanged. Verified with npx tsc -p packages/email/tsconfig.json --noEmit.
  • (2026-05-13) T040 outbound reply In-Reply-To test. Extended baseEmailServiceCommentThreading.test.ts to model a reply comment plus latest sent email_sending_logs.rfc_message_id; provider sees In-Reply-To set to that latest outbound RFC id and gets a new generated Message-ID for the outbound reply. Verified with cd server && npx vitest run src/test/unit/email/baseEmailServiceCommentThreading.test.ts -t "T040".
  • (2026-05-13) F036 outbound References chain. BaseEmailService now builds thread-specific References for reply comments from comment_threads.email_references + latest outbound rfc_message_id, emits that header with the email, and persists the deduped array back to comment_threads.email_references only after a successful send. Thread-specific headers intentionally override legacy ticket-level headers for comment replies. Verified with npx tsc -p packages/email/tsconfig.json --noEmit.
  • (2026-05-13) T041 outbound References test. Added unit coverage for reply emails with an existing comment_threads.email_references chain. The provider receives the stored chain plus the latest outbound RFC id as the References header, and BaseEmailService persists the same array back to comment_threads.email_references. Verified with cd server && npx vitest run src/test/unit/email/baseEmailServiceCommentThreading.test.ts -t "T041".
  • (2026-05-13) F037 outbound reply tokens. No code change needed: sendEventEmail already generates a fresh randomUUID() token when replyContext is present, embeds the ALGA-REPLY-TOKEN marker in HTML/text, and persists email_reply_tokens.comment_id plus recipient_email for the outbound comment. Confirmed in server/src/lib/notifications/sendEventEmail.ts.
  • (2026-05-13) T042 outbound reply-token persistence test. Tightened ticketEmailDelimiters.test.ts reply-marker coverage so the comment notification test now verifies the in-memory email_reply_tokens row includes tenant, ticket_id, comment_id, recipient_email, and ticket entity_type. Updated the @alga-psa/email mock to include StaticTemplateProcessor, matching the current sendEventEmail dependency. Verified with cd server && npx vitest run src/test/integration/ticketEmailDelimiters.test.ts -t "T042".
  • (2026-05-13) F038 outbound send log thread linkage. sendEventEmail now passes the generated reply token through to BaseEmailService so reply_token_hash/suffix are populated even when callers did not provide conversationToken. BaseEmailService.logEmailSendResult looks up the outbound comment's thread_id and writes it to email_sending_logs.comment_thread_id alongside provider/RFC IDs and token fields. Verified with npx tsc -p packages/email/tsconfig.json --noEmit and npx tsc -p server/tsconfig.json --noEmit.
  • (2026-05-13) T043 outbound email log linkage test. Added BaseEmailService unit coverage that waits for the best-effort email_sending_logs insert and verifies comment_thread_id, generated rfc_message_id, provider_message_id, thread_id (provider), comment_id, and reply-token hash/suffix are populated. Verified with cd server && npx vitest run src/test/unit/email/baseEmailServiceCommentThreading.test.ts -t "T043".
  • (2026-05-13) T044 outbound/inbound round-trip test. Added server/src/test/unit/email/outboundInboundThreadRoundTrip.test.ts, sharing an in-memory mocked DB between BaseEmailService and processInboundEmailInApp. The test sends a top-level comment email, captures the generated/logged RFC Message-ID, then processes an inbound email with matching In-Reply-To and verifies it creates a reply on the original ticket/thread parent. Verified with cd server && npx vitest run src/test/unit/email/outboundInboundThreadRoundTrip.test.ts -t "T044".
  • (2026-05-13) T045 thread grouping/sorting test. Added packages/ui/src/components/CommentThreadList.test.ts covering buildCommentThreadGroups: one group per threadId, root/child mapping, reply count, chronological in-thread comments, and oldest/newest sorting by thread last activity. Verified with cd server && npx vitest run ../packages/ui/src/components/CommentThreadList.test.ts -t "T045".
  • (2026-05-13) T046 recursive HybridThreadNode test. Added packages/ui/src/components/HybridThreadNode.test.tsx with root → reply → subreply data. It renders the recursive node tree and verifies depth 0 is not a sub-thread while depth 1+ nodes are marked as sub-threads via render context. Verified with cd server && npx vitest run ../packages/ui/src/components/HybridThreadNode.test.tsx -t "T046".
  • (2026-05-13) T047 collapse/expand bar test. Extended HybridThreadNode.test.tsx to click the root Collapse button, assert child comments are hidden, and verify the collapsed bar exposes Expand plus Open in drawer when onOpenPanel is available. Verified with cd server && npx vitest run ../packages/ui/src/components/HybridThreadNode.test.tsx -t "T047".
  • (2026-05-13) T048 open-in-drawer test. Extended HybridThreadNode.test.tsx with a harness that collapses the root thread, clicks Open in drawer, and verifies CommentThreadDrawer opens with root, reply, and subreply content. Mocked TextEditor to keep the drawer test focused. Verified with cd server && npx vitest run ../packages/ui/src/components/HybridThreadNode.test.tsx -t "T048".
  • (2026-05-13) T049 inline reply composer controls test. Added packages/ui/src/components/InlineReplyComposer.test.tsx to verify the inline reply composer shows Mark as Internal, omits Mark as Resolution, and submits the inherited initialInternal=true value with the parent comment id. Verified with cd server && npx vitest run ../packages/ui/src/components/InlineReplyComposer.test.tsx -t "T049".
  • (2026-05-13) T050 drawer reply submit test. Extended HybridThreadNode.test.tsx with a parent-managed drawer harness: submitting the drawer composer calls onSubmitReply with parentCommentId=root, appends the new reply to the flat comment state, closes the drawer, and the inline tree re-renders the new child. Verified with cd server && npx vitest run ../packages/ui/src/components/HybridThreadNode.test.tsx -t "T050".
  • (2026-05-13) T051 visual depth cap test. Extended HybridThreadNode.test.tsx with a six-comment root-to-depth-5 chain. The test verifies the depth-5 comment still renders with data depth 5 but visual depth 4, and that repeated .thread-children.depth-4 wrappers enforce the CSS cap. Verified with cd server && npx vitest run ../packages/ui/src/components/HybridThreadNode.test.tsx -t "T051".
  • (2026-05-13) T052 sub-thread bar style test. Extended HybridThreadNode.test.tsx to verify root bars render without comment-thread-bar-subthread, nested bars render with it at depth 1, and CommentThread.module.css maps that class to dashed border plus white background while base bars keep #f9fafb. Verified with cd server && npx vitest run ../packages/ui/src/components/HybridThreadNode.test.tsx -t "T052".
  • (2026-05-13) T053 ticket Reply action reveal test. Extended CommentItem.metadataDebug.test.tsx to render a ticket comment with onReply, click the accessible Reply to comment button, verify the callback receives the comment, and assert the button lives inside .c-actions. The same test checks shared CSS keeps .c-actions hidden by default and reveals it on hover/focus-within. Verified with cd server && npx vitest run ../packages/tickets/src/components/ticket/CommentItem.metadataDebug.test.tsx -t "T053".
  • (2026-05-13) T054 soft-deleted comment UI test. Added ticket and project-task coverage for deleted comments: ticket CommentItem and project TaskComment both render [deleted] with opacity-70 and suppress the Reply to comment action even when onReply is provided. Verified with cd server && npx vitest run ../packages/tickets/src/components/ticket/CommentItem.metadataDebug.test.tsx ../packages/projects/src/components/TaskComment.test.tsx -t "T054".
  • (2026-05-13) T055 thread-level tab counts test. Factored ticket tab bucketing into ticketConversationThreadTabs.ts and used its thread counts in the TicketConversation tab labels. Added ticketConversationThreadTabs.test.ts covering All/Client/Internal/Resolution counts by root thread visibility and any-resolution-in-thread behavior, including client portal internal-thread exclusion. Verified with cd server && npx vitest run ../packages/tickets/src/components/ticket/ticketConversationThreadTabs.test.ts -t "T055" and cd server && npx tsc -p ../packages/tickets/tsconfig.json --noEmit.
  • (2026-05-13) T056 ticket thread sort preference test. Extended TicketConversation.commentOrderPreference.test.tsx with threaded comment data where an older root has a later reply. The test verifies oldest-first renders the less-active thread first, toggling stores newestFirst=true, and newest-first reorders whole threads while keeping root/reply chronological inside the active thread. Added test harness mocks for DocumentsCrossFeatureContext and IntersectionObserver, and timestamped the legacy preference fixtures so thread sorting is deterministic. Verified with cd server && npx vitest run ../packages/tickets/src/components/ticket/TicketConversation.commentOrderPreference.test.tsx.
  • (2026-05-13) T057 ticket reply composer interaction test. Added TicketConversation.replyComposer.e2e.contract.test.tsx as a lightweight local e2e contract around TicketConversation with the real CommentItem and thread wiring. The test hovers the rendered comment, verifies the Reply to comment action is in the .c-actions row, clicks it, and asserts the inline reply composer plus editor and submit button render for the parent comment. Verified with cd server && npx vitest run ../packages/tickets/src/components/ticket/TicketConversation.replyComposer.e2e.contract.test.tsx -t "T057"; Vitest still emits the repository's localstorage-file warning and a React act warning from component state effects.
  • (2026-05-13) T058 ticket inline reply submit test. Extended TicketConversation.replyComposer.e2e.contract.test.tsx with a stateful conversation harness whose onAddReplyComment appends a child comment to the same thread. Submitting the inline composer now verifies the reply row renders under its parent, the default thread bar shows 1 reply with Collapse, and the reply sits inside .thread-children.depth-1. Verified with cd server && npx vitest run ../packages/tickets/src/components/ticket/TicketConversation.replyComposer.e2e.contract.test.tsx; Vitest still emits the repository's localstorage-file warning and React act warnings from component state effects.
  • (2026-05-13) T059 ticket collapse/expand interaction test. Extended TicketConversation.replyComposer.e2e.contract.test.tsx with an existing root + reply ticket thread. The test collapses the thread, verifies the child comment is hidden while the bar shows 1 reply, Expand, and Open in drawer, then expands and verifies the child returns with Collapse. Verified with cd server && npx vitest run ../packages/tickets/src/components/ticket/TicketConversation.replyComposer.e2e.contract.test.tsx -t "T059"; Vitest still emits the repository's localstorage-file warning and React act warnings from component state effects.
  • (2026-05-13) T060 ticket drawer open/close interaction test. Extended TicketConversation.replyComposer.e2e.contract.test.tsx to collapse an existing ticket thread, click Open in drawer, verify the drawer contains both root and reply comments, then close it and confirm the inline view remains collapsed with Expand visible. Verified with cd server && npx vitest run ../packages/tickets/src/components/ticket/TicketConversation.replyComposer.e2e.contract.test.tsx -t "T060"; jsdom still reports Radix focus/portal act warnings while the test exits successfully.
  • (2026-05-13) T061 ticket drawer reply reflection test. Extended TicketConversation.replyComposer.e2e.contract.test.tsx with a stateful drawer-reply harness. The first run exposed that the inline thread stayed collapsed after drawer submit, hiding the new reply. Fixed TicketConversation by keying each inline HybridThreadNode by threadId-replyCount, so a thread remounts expanded when its reply count changes while a no-op drawer close preserves collapse state. Verified with cd server && npx vitest run ../packages/tickets/src/components/ticket/TicketConversation.replyComposer.e2e.contract.test.tsx and npx tsc -p packages/tickets/tsconfig.json --noEmit.
  • (2026-05-13) T062 ticket nested reply sub-thread test. Extended TicketConversation.replyComposer.e2e.contract.test.tsx with a root + reply thread and a stateful nested reply submit. The test clicks Reply on the existing reply, submits, verifies the nested child renders, and asserts the nested bar has .comment-thread-bar-subthread.depth-1 with 1 reply while the child sits under .thread-children-subthread. Verified with cd server && npx vitest run ../packages/tickets/src/components/ticket/TicketConversation.replyComposer.e2e.contract.test.tsx.
  • (2026-05-13) T063 ticket internal tab interaction test. Extended the ticket reply contract test's CustomTabs mock to render tab buttons and active content by controlled value. Added a ticket tab harness with one client-rooted thread and one internal-rooted thread, verifying tab labels show thread-level counts (All Comments (2), Client (1), Internal (1)) and switching to Internal hides the client root + reply while keeping the internal thread visible. Verified with cd server && npx vitest run ../packages/tickets/src/components/ticket/TicketConversation.replyComposer.e2e.contract.test.tsx -t "T063".
  • (2026-05-13) T064 project task inline reply test. Added TaskCommentThread.threadedReplies.test.tsx with mocked project-task comment actions backed by an in-memory comment list. The test renders TaskCommentThread, opens Reply on the root task comment, submits through TaskCommentForm, reloads via getTaskComments, and verifies the child comment renders under .thread-children.depth-1 with a 1 reply bar. Verified with cd server && npx vitest run ../packages/projects/src/components/TaskCommentThread.threadedReplies.test.tsx -t "T064" and npx tsc -p packages/projects/tsconfig.json --noEmit.
  • (2026-05-13) T065 project task collapse/drawer test. Extended TaskCommentThread.threadedReplies.test.tsx with a preloaded root + reply task thread. The test collapses the thread, verifies the reply hides and Expand + Open in drawer appear, opens the drawer with root + reply content, closes it, and verifies the inline thread remains collapsed. Verified with cd server && npx vitest run ../packages/projects/src/components/TaskCommentThread.threadedReplies.test.tsx.
  • (2026-05-13) T066 project task reply visibility-control test. Extended TaskCommentThread.threadedReplies.test.tsx to open the inline task reply composer and assert the editor appears without any Mark as Internal toggle, matching the task-only internal model. Verified with cd server && npx vitest run ../packages/projects/src/components/TaskCommentThread.threadedReplies.test.tsx -t "T066".
  • (2026-05-13) T067 legacy ticket render regression test. Extended TicketConversation.replyComposer.e2e.contract.test.tsx with two legacy flat ticket comments that omit thread_id and parent_comment_id. The test verifies both comments render with no .comment-thread-bar and no .thread-children indentation wrapper, preserving the pre-threading layout for single-comment legacy threads. Verified with cd server && npx vitest run ../packages/tickets/src/components/ticket/TicketConversation.replyComposer.e2e.contract.test.tsx -t "T067".
  • (2026-05-13) T068 legacy project task render regression test. Extended TaskCommentThread.threadedReplies.test.tsx with two legacy flat task comments that omit threadId and parentCommentId. The test verifies both comments render without a thread bar or .thread-children wrapper, preserving the old flat task comment layout. Verified with cd server && npx vitest run ../packages/projects/src/components/TaskCommentThread.threadedReplies.test.tsx -t "T068".
  • (2026-05-13) T069 legacy ticket edit regression test. Extended CommentItem.metadataDebug.test.tsx to render a legacy comment in edit mode, click Save, and verify the save payload includes normal note/internal/resolution fields without thread_id or parent_comment_id. The same test reads packages/tickets/src/models/comment.ts to assert the update path still stamps updated_at: new Date().toISOString(). Verified with cd server && npx vitest run ../packages/tickets/src/components/ticket/CommentItem.metadataDebug.test.tsx -t "T069".
  • (2026-05-13) T070 reply reaction regression test. Mocked ReactionDisplay in CommentItem.metadataDebug.test.tsx and added a reply comment with thread_id + parent_comment_id. Clicking the reaction verifies CommentItem calls onToggleReaction with the reply's own comment_id, so reactions attach per comment rather than per root thread. Verified with cd server && npx vitest run ../packages/tickets/src/components/ticket/CommentItem.metadataDebug.test.tsx -t "T070".
  • (2026-05-13) T071 thread list render performance test. Added CommentThreadList.performance.test.tsx with 100 synthetic threads and five replies each. It renders through CommentThreadList + HybridThreadNode, verifies all 600 comment nodes and 100 thread bars render, and asserts the component render stays under a conservative 1500ms budget. Verified with cd server && npx vitest run ../packages/ui/src/components/CommentThreadList.performance.test.tsx -t "T071" (observed test body around 133ms in this local run).
  • (2026-05-13) T072 ticket cascade integration test. Extended server/src/test/integration/commentThreadsMigration.integration.test.ts with a rollback-only transaction that creates an isolated ticket + comment_threads row, deletes the ticket, and verifies the thread row is removed by the comment_threads_ticket_fk cascade. An initial attempt against a shared fixture ticket failed because existing comments rows block ticket deletion via their older FK, so the final test creates its own no-comment ticket. Verified with cd server && npx vitest run src/test/integration/commentThreadsMigration.integration.test.ts -t "T072"; the suite still prints unrelated seed warnings about generate-system-email-workflow.cjs.
  • (2026-05-13) T073 project-task cascade integration test. Extended commentThreadsMigration.integration.test.ts with a rollback-only transaction that creates an isolated project task + comment_threads row, deletes the task, and verifies the thread row is removed by the comment_threads_project_task_fk cascade. Verified with cd server && npx vitest run src/test/integration/commentThreadsMigration.integration.test.ts -t "T073"; the suite still prints unrelated seed warnings about generate-system-email-workflow.cjs.
  • (2026-05-13) T074 comment_threads parent CHECK integration test. Extended commentThreadsMigration.integration.test.ts to attempt inserting a thread with both parent IDs null and another with both ticket_id and project_task_id set. Both inserts reject via comment_threads_exactly_one_parent_check, proving the polymorphic parent invariant is enforced by the database. Verified with cd server && npx vitest run src/test/integration/commentThreadsMigration.integration.test.ts -t "T074"; the suite still prints unrelated seed warnings about generate-system-email-workflow.cjs.
  • (2026-05-13) T075 inbound email idempotency test. Extended unifiedInboundEmailQueueJobProcessor.fetch.test.ts with a duplicate Microsoft message carrying In-Reply-To and References headers. The simulated email_processed_messages unique violation returns a deduped/skipped result before processInboundEmailInApp runs, proving thread-routing paths cannot create a duplicate comment for replayed provider messages. Verified with cd server && npx vitest run src/test/unit/unifiedInboundEmailQueueJobProcessor.fetch.test.ts -t "T075"; Vitest still prints the existing duplicate on mock warning and coverage table.
  • (2026-05-13) T076 multiple-recipient reply token test. Extended ticketEmailDelimiters.test.ts so the ticket-comment notification path sends to a contact and an internal assignee/resource address, then verifies email_reply_tokens receives two rows for the outbound comment with distinct tokens and recipient-specific recipient_email values. Added lightweight mocks for Knex .modify(), comments as cm, and tenant_settings needed by the current subscriber path. Verified with cd server && npx vitest run src/test/integration/ticketEmailDelimiters.test.ts -t "T076"; the run still prints an existing auth-session warning from notification gating but passes.
  • (2026-05-13) T077 concurrent reply counter test. Extended commentModelThreading.integration.test.ts with two parallel Comment.insert calls replying to the same root comment. Both inserts complete, both child rows inherit the root thread, and comment_threads.reply_count ends at 2, covering the atomic reply_count + 1 update against lost increments. Verified with cd server && npx vitest run src/test/integration/commentModelThreading.integration.test.ts -t "T077"; the integration setup still prints normal migration/seed noise and coverage output.
  • (2026-05-13) T078 client reply RBAC test and enforcement. Added assertClientCanCreateComment to createComment so client users can only post their own non-internal comments on tickets whose client_id matches their session/contact client. Extended commentActionsThreading.integration.test.ts to verify a client can reply on their client-visible thread, cannot reply to an internal thread, and cannot reply on another client's ticket. Verified with cd server && npx vitest run src/test/integration/commentActionsThreading.integration.test.ts -t "T078" and npx tsc -p packages/tickets/tsconfig.json --noEmit.
  • (2026-05-13) F039 shared thread grouping UI. Added generic CommentThreadList and buildCommentThreadGroups in packages/ui/src/components/CommentThreadList.tsx. It accepts flat comments plus accessors, groups by thread_id (falling back to comment id for legacy rows), builds childrenByParentId, keeps replies chronological, derives lastActivityAt, and renders one group per thread in oldest/newest order. Exported from packages/ui/src/components/index.ts. Verified with npx tsc -p packages/ui/tsconfig.json --noEmit.
  • (2026-05-13) F040 recursive HybridThreadNode. Added generic HybridThreadNode in packages/ui/src/components/HybridThreadNode.tsx. It renders a comment, a thread bar when children exist, and recursive child nodes from childrenByParentId; depth class emission caps at depth-4 while recursion continues for unlimited data depth. Exported from packages/ui/src/components/index.ts. Verified with npx tsc -p packages/ui/tsconfig.json --noEmit.
  • (2026-05-13) F041 per-node collapse state. HybridThreadNode now owns expanded/collapsed state for each node with children. The default thread bar shows Collapse while expanded and Expand plus Open in drawer while collapsed; collapsed nodes hide only their child subtree. Verified with npx tsc -p packages/ui/tsconfig.json --noEmit.
  • (2026-05-13) F042 open-panel callback. No additional code needed after F041: HybridThreadNode exposes onOpenPanel?: (commentId) => void, passes it recursively, and the default collapsed thread bar calls it from Open in drawer.
  • (2026-05-13) F043 inline reply composer. Added InlineReplyComposer in packages/ui/src/components/InlineReplyComposer.tsx. It uses BlockNote TextEditor, defaults internal visibility from the parent, exposes only the Mark-as-Internal switch (no resolution control), and submits { parentCommentId, content, isInternal }. Exported from packages/ui/src/components/index.ts. Verified with npx tsc -p packages/ui/tsconfig.json --noEmit.
  • (2026-05-13) F044 comment thread drawer. Added generic CommentThreadDrawer in packages/ui/src/components/CommentThreadDrawer.tsx. It wraps the existing Radix-backed Drawer at 480px, renders the root thread tree via HybridThreadNode, and shows an InlineReplyComposer at the bottom. Exported from packages/ui/src/components/index.ts. Verified with npx tsc -p packages/ui/tsconfig.json --noEmit.
  • (2026-05-13) F045 shared comment thread CSS. Added packages/ui/src/components/CommentThread.module.css and imported it from HybridThreadNode. It defines global selectors for the left rail, thread-bar pill, dashed sub-thread bars, depth classes through depth-4, drawer spacing, inline composer spacing, and .c-actions hover/focus reveal. Verified with npx tsc -p packages/ui/tsconfig.json --noEmit.
  • (2026-05-13) F046 ticket Reply action. CommentItem now accepts optional onReply(comment), renders a CornerUpLeft reply button in the .c-actions hover/focus row ahead of edit/delete, and keeps existing call sites working when no reply handler is supplied. Verified with npx tsc -p packages/tickets/tsconfig.json --noEmit.
  • (2026-05-13) F047 ticket conversation thread renderer. Replaced TicketConversation's flat comment mapping with CommentThreadList<IComment> + HybridThreadNode<IComment>, preserving the existing top-level composer, tabs, sort toggle, edit/delete, upload session, metadata debug, and reactions through the existing CommentItem renderer. Verified with npx tsc -p packages/tickets/tsconfig.json --noEmit.
  • (2026-05-13) F048 ticket thread-level tabs. TicketConversation now derives thread groups before tab filtering. All/Client/Internal select threads by root visibility, Resolution selects threads containing any resolution comment, and the rendered list includes all comments in matching threads so children stay attached. Client portal mode still excludes internal-rooted threads. Verified with npx tsc -p packages/tickets/tsconfig.json --noEmit.
  • (2026-05-13) F049 ticket thread sort. No additional code needed after F039/F047: CommentThreadList receives the existing reverseOrder preference as newestFirst, sorts thread groups by derived lastActivityAt, and keeps comments inside each thread chronological.
  • (2026-05-13) F050 ticket drawer state. TicketConversation now tracks openPanelCommentId, passes onOpenPanel through HybridThreadNode, resolves the containing thread group for that comment, and renders CommentThreadDrawer with the selected comment as the drawer reply parent. Reply submission is intentionally still a placeholder for F053. Verified with npx tsc -p packages/tickets/tsconfig.json --noEmit.
  • (2026-05-13) F051 ticket inline reply open. TicketConversation now tracks replyingToCommentId, passes onReply into CommentItem, and renders InlineReplyComposer directly beneath the selected comment. The composer inherits the parent comment's is_internal value and hides the internal switch in client portal mode. Submission is intentionally still a placeholder for F052. Verified with npx tsc -p packages/tickets/tsconfig.json --noEmit.
  • (2026-05-13) F052 ticket inline reply submit. Added onAddReplyComment from TicketDetails into TicketConversation. Inline submit calls createComment with parent_comment_id, is_resolution=false, and the chosen visibility, then refreshes findCommentsByTicketId into local state and closes the composer on success. Verified with npx tsc -p packages/tickets/tsconfig.json --noEmit and npx tsc -p packages/client-portal/tsconfig.json --noEmit.
  • (2026-05-13) F053 ticket drawer reply submit. CommentThreadDrawer in TicketConversation now calls the same onAddReplyComment path with parent_comment_id, then closes the drawer after a successful local refresh. Verified with npx tsc -p packages/tickets/tsconfig.json --noEmit.
  • (2026-05-13) F054 task Reply action. TaskComment now accepts optional onReply(comment), renders a CornerUpLeft reply button in the .c-actions hover/focus row ahead of edit/delete, and preserves existing callers when no reply handler is supplied. Verified with npx tsc -p packages/projects/tsconfig.json --noEmit.
  • (2026-05-13) F055 task thread renderer. TaskCommentThread now renders CommentThreadList<IProjectTaskCommentWithUser> + HybridThreadNode<IProjectTaskCommentWithUser> instead of a flat sorted map, preserving existing add-comment form, sort toggle, edit/delete, and reactions through TaskComment. Verified with npx tsc -p packages/projects/tsconfig.json --noEmit.
  • (2026-05-13) F056 task inline reply submit. TaskCommentForm now accepts parentCommentId and sends it as parent_comment_id to createTaskComment. TaskCommentThread tracks replyingToCommentId, renders the form under the selected task comment, refreshes comments on success, and has no internal/client visibility toggle because task comments are internal-only. Verified with npx tsc -p packages/projects/tsconfig.json --noEmit.
  • (2026-05-13) F057 task drawer flow. TaskCommentThread now tracks openPanelCommentId, passes onOpenPanel to HybridThreadNode, resolves the selected thread group, and renders CommentThreadDrawer. Drawer replies call createTaskComment with parent_comment_id, refresh comments, and hide the internal toggle. Verified with npx tsc -p packages/projects/tsconfig.json --noEmit.
  • (2026-05-13) F058 soft-deleted comment UI. Ticket CommentItem and project TaskComment now render deleted rows as reduced-opacity [deleted] placeholders, preserve the node in the tree, and suppress Reply/Edit/Delete actions for deleted comments. Verified with npx tsc -p packages/tickets/tsconfig.json --noEmit and npx tsc -p packages/projects/tsconfig.json --noEmit.
  • (2026-05-13) F059 depth cap. No additional code needed after F040/F045: HybridThreadNode caps emitted visual depth classes at depth-4 while continuing recursion, and CommentThread.module.css keeps thread-children.depth-4 at the capped indent.
  • (2026-05-13) F060 sub-thread bar styling. No additional code needed after F040/F045: HybridThreadNode marks depth > 0 bars with comment-thread-bar-subthread, and CommentThread.module.css styles those bars with dashed border, white background, and indigo action/count color.
  • (2026-05-13) F061 sort preference persistence. No additional code needed after F039/F047/F049: TicketConversation still reads/writes the existing ticketConversationOrderPreference, and its reverseOrder value is now consumed by CommentThreadList as thread-level newestFirst.
  • (2026-05-13) F062 dev seed compatibility. T001 migration integration test exposed that server/seeds/dev/15_comments.cjs inserted direct comment rows after comments.thread_id became NOT NULL. Updated the seed to create one comment_threads row per seeded comment and insert comments.thread_id, matching the legacy backfill shape.
  • (2026-05-13) T002 migration index test. Extended commentThreadsMigration.integration.test.ts to assert the ticket/task list indexes and partial email_message_id index via pg_indexes. This covers the thread lookup/index requirements against the migrated schema.
  • (2026-05-13) T003 comments column/FK test. Added migrated-schema coverage for comments.thread_id, parent_comment_id, deleted_at, plus comments_thread_fk and comments_parent_comment_fk. Because the integration DB runs all migrations, thread_id is no longer nullable by this point; T009 separately verifies the final NOT NULL behavior.
  • (2026-05-13) T004 task comments column/FK test. Added the same migrated-schema coverage for project_task_comments.thread_id, parent_comment_id, deleted_at, project_task_comments_thread_fk, and project_task_comments_parent_comment_fk.
  • (2026-05-13) T005 ticket backfill test. Added a controlled legacy-row test that temporarily relaxes comments.thread_id, inserts a null-thread ticket comment, runs 20260513102000_backfill_comment_threads_for_comments.cjs, verifies thread_id = comment_id and the single-comment comment_threads row, then cleans up and restores NOT NULL.
  • (2026-05-13) T006 ticket email metadata backfill test. Added a second controlled legacy-row case with metadata.email.messageId and verified the backfill writes it to comment_threads.email_message_id.
  • (2026-05-13) T007 backfill idempotency test. Added a rerun check for both ticket and task comment backfill migrations; with all rows already threaded, comment_threads count must stay unchanged.
  • (2026-05-13) T008 task backfill test. Added a controlled legacy-row test for project_task_comments that temporarily relaxes thread_id, runs 20260513102500_backfill_comment_threads_for_project_task_comments.cjs, verifies thread_id = task_comment_id and the project_task_id thread row, then restores NOT NULL.
  • (2026-05-13) T009 NOT NULL enforcement test. Added final-schema assertions that direct inserts with thread_id = NULL fail for both comments and project_task_comments.
  • (2026-05-13) T010 email log linkage test. Added migrated-schema coverage for email_sending_logs.comment_thread_id, the tenant-scoped FK to comment_threads, and the partial lookup index used for latest outbound message lookup.
  • (2026-05-13) T011 comment thread type test. Added packages/types/src/commentThread.typecheck.test.ts using expectTypeOf to verify ICommentThread is exported from @alga-psa/types with all persisted comment_threads fields.
  • (2026-05-13) T012 ticket comment type test. Extended the typecheck test to verify IComment.thread_id, parent_comment_id, and deleted_at are present with create/read-compatible optional types.
  • (2026-05-13) T013 task comment type test. Extended the same typecheck test to verify IProjectTaskComment.threadId, parentCommentId, and deletedAt are present with the camelCase task-comment interface convention.
  • (2026-05-13) T014 ticket top-level model test. Added server/src/test/integration/commentModelThreading.integration.test.ts to call Comment.insert for a top-level ticket comment and verify the inserted comment's thread_id points at a new comment_threads row whose root_comment_id is the new comment.
  • (2026-05-13) T015 ticket reply inheritance test. Extended the ticket comment model integration test to insert a root and a reply, then verify the reply's thread_id matches the parent/root thread and parent_comment_id stores the parent id.
  • (2026-05-13) T016 ticket reply same-ticket guard. Added a model integration test that creates a parent on one ticket and verifies Comment.insert rejects a reply payload targeting a different ticket with Parent comment must belong to the same ticket.
  • (2026-05-13) T017 deleted-parent guard. Added a model integration test that soft-deletes a parent comment and verifies Comment.insert rejects replies with Cannot reply to a deleted comment.
  • (2026-05-13) T018 reply visibility invariant. Added model integration coverage for both forbidden directions: internal reply on a client-visible thread and client-visible reply on an internal thread. Both must fail with Reply visibility must match the thread root visibility.
  • (2026-05-13) T019 reply counter/activity test. Added ticket model integration coverage that inserts a reply and verifies comment_threads.reply_count increments by one and last_activity_at advances/stays current.
  • (2026-05-13) T020 leaf delete counter test. Added ticket model integration coverage that deletes a leaf reply through Comment.delete, then verifies the reply row is gone and the owning thread's reply_count decrements to zero.
  • (2026-05-13) T021 root soft-delete test. Added ticket model integration coverage that deletes a root comment with a child, verifies the root remains as [deleted] with deleted_at set, and confirms the child reply still points at that root.
  • (2026-05-13) T022 ticket read contract test. Added ticket model integration coverage that soft-deletes a root with a child and verifies Comment.getAllbyTicketId returns both rows with thread_id, parent_comment_id, and deleted_at preserved for tree rendering.
  • (2026-05-13) T023 task top-level model test. Added server/src/test/integration/projectTaskCommentThreading.integration.test.ts with mocked auth/tenant resolution around the real createTaskComment action, verifying a top-level task comment creates a comment_threads row with project_task_id set and is_internal=false.
  • (2026-05-13) T024 task reply inheritance/counter test. Extended the project task comment integration test to create a reply through createTaskComment, verify it inherits the root thread, stores parent_comment_id, increments reply_count, and bumps last_activity_at.
  • (2026-05-13) T025 task delete semantics test. Extended the project task comment integration test to call deleteTaskComment for both a leaf reply and a root with children, verifying leaf hard-delete/decrement and root [deleted] soft-delete while preserving the child.
  • (2026-05-13) T026 task read contract test. Extended the project task comment integration test to soft-delete a root with a child and verify getTaskComments returns threadId, parentCommentId, and deletedAt for both root and reply.
  • (2026-05-13) T027 ticket action parent pass-through test. Added server/src/test/integration/commentActionsThreading.integration.test.ts with mocked auth/tenant resolution around the real createComment action, verifying an action-level reply stores parent_comment_id and inherits the root thread.
  • (2026-05-13) T028 ticket comment event payload test. Extended the ticket action integration test to create a reply and inspect the mocked publishEvent call, verifying TICKET_COMMENT_ADDED includes thread_id, parent_comment_id, and is_reply at the top level and inside the legacy comment object.
  • (2026-05-13) T029 client internal-reply rejection test. Extended the ticket action integration test with a client user attempting to create an internal reply. createComment rejects with the action-level failure and no invalid internal child row is persisted.
  • (2026-05-13) T030 task action parent pass-through coverage. No new test code needed: T024 already calls the real projectTaskCommentActions.createTaskComment action with parent_comment_id and verifies the reply row inherits the root thread and stores the parent id.
  • (2026-05-13) T031 task comment event payload test. Extended the project task comment integration test to inspect the mocked publishEvent call for a reply and verify TASK_COMMENT_ADDED includes both camelCase and snake_case threading fields plus reply booleans.
  • (2026-05-13) T032 task reply ownership test. Extended the project task comment integration test with two temporary client-owned replies. The real deleteTaskComment action allows the current client to delete their own reply and rejects deleting another client's reply via assertOwnCommentOrInternalUser.
  • (2026-05-13) T033 inbound reply-token thread routing test. Added a mocked inbound-email unit test where findTicketByReplyToken returns a commentId; processInboundEmailInApp resolves that comment's thread, looks up the latest comment in the thread, and forwards that latest id as parent_comment_id to createCommentFromEmail.
  • (2026-05-13) T034 inbound In-Reply-To routing test. Extended the inbound-email threading unit test so emailData.inReplyTo matches email_sending_logs.rfc_message_id; the processor resolves comment_thread_id, loads the thread's latest comment, and sends that id as parent_comment_id with matchedBy: thread_headers.
  • (2026-05-13) T035 inbound References routing test. Added a unit test proving References[] is walked from newest to oldest: the newest reference misses, the next one matches email_sending_logs.rfc_message_id, and older references are not queried once a thread target is found.
  • (2026-05-13) T036 inbound provider-thread routing test. Added unit coverage for provider thread id fallback: emailData.threadId matches comment_threads.email_provider_thread_id, then the processor resolves the ticket thread and forwards the latest comment as parent_comment_id.
  • (2026-05-13) T037 inbound ticket fallback test. Added unit coverage for the legacy ticket-level match after all thread-specific strategies miss. The resulting createCommentFromEmail call intentionally omits parent_comment_id, so shared ticket comment creation creates a new top-level thread on the matched ticket.
  • (2026-05-13) T038 inbound precedence test. Added unit coverage where a reply token and In-Reply-To are both present. The token path wins, the email log header lookup is never queried, and the inbound comment is parented from the token-derived thread.
  • (2026-05-13) T079 drawer focus management. Added focus restoration in TicketConversation by remembering the element that opened the comment-thread drawer and refocusing it after close. Reinforced Drawer content focus on open so focus moves inside the Radix drawer instead of remaining on the collapsed thread bar. Extended the ticket conversation contract test to verify focus enters the drawer and returns to the Open in drawer button after close. Verified with cd server && npx vitest run ../packages/tickets/src/components/ticket/TicketConversation.replyComposer.e2e.contract.test.tsx -t "T079" and npx tsc -p packages/tickets/tsconfig.json --noEmit. npx tsc -p packages/ui/tsconfig.json --noEmit is still blocked by pre-existing jest-dom matcher type errors in HybridThreadNode.test.tsx and InlineReplyComposer.test.tsx.
  • (2026-05-13) T080 top-level add contract test. Extended the ticket conversation contract test with a stateful top-level composer flow on a conversation that already has a root + reply. Submitting Add Comment appends a new root thread, verifies the reply callback was not called, confirms the new root is not rendered inside .thread-children, and leaves the existing reply indented under its original root. Verified with cd server && npx vitest run ../packages/tickets/src/components/ticket/TicketConversation.replyComposer.e2e.contract.test.tsx -t "T080" and npx tsc -p packages/tickets/tsconfig.json --noEmit.
  • (2026-05-13) No existing threading columns. comments and project_task_comments are flat today; no parent_id, reply_to_id, or thread_id. Confirmed by reading packages/types/src/interfaces/comment.interface.ts (line 38).
  • (2026-05-13) Separate tables. Tickets use comments; project tasks use project_task_comments (migration 20251118140000_create_project_task_comments.cjs). No sharing — keep comment_threads polymorphic across both.
  • (2026-05-13) Today the ticket IS the email thread. tickets.email_metadata (jsonb) stores messageId, threadId, inReplyTo, references for inbound matching. All inbound emails for a ticket collapse to flat comments. This is what the thread-as-entity refactor unblocks.
  • (2026-05-13) Outbound currently uses reply tokens, not RFC headers. email_reply_tokens maps token → (ticket_id | comment_id | project_id). The token marker [ALGA-REPLY-TOKEN:...] is embedded in the body. New work: ALSO set proper In-Reply-To/References so mail clients thread our messages, AND keep issuing tokens scoped to the new outbound comment.
  • (2026-05-13) email_sending_logs already has a thread_id column (migration 20260331110000_add_email_threading_diagnostics_columns.cjs). It looks like it refers to the provider's threadId. We will add a separate comment_thread_id column rather than overload — clearer and avoids semantic drift. If safe, rename the existing one to email_provider_thread_id in the same migration.
  • (2026-05-13) Drawer is a Radix Dialog. Per MEMORY.md, the existing packages/ui Drawer uses Radix's Dialog internally with modal={true}. Nested dialogs are already handled by InsideDialogContext in ModalityContext.tsx — no extra wiring needed for the comment drawer.
  • (2026-05-13) Project task comments are internal-only today. author_type is hardcoded 'internal' in IProjectTaskComment. So the task inline reply composer should NOT show a Mark-as-Internal toggle (different from ticket flow).
  • (2026-05-13) Reactions already attach via commentReactionActions; nothing to change for replies — same per-comment reaction model applies.
  • (2026-05-13) Existing ticketConversationOrderPreference stores newest-first preference per user. Reused as-is; applied to thread ordering after this change.

Commands / Runbooks

  • (2026-05-13) T001 migration test:
    cd server && npx vitest run src/test/integration/commentThreadsMigration.integration.test.ts
    
    Result after F062 seed fix: passed (1 passed).
  • (2026-05-13) Run migrations locally:
    cd server && npm run db:migrate:latest
    
  • (2026-05-13) Dev server (Docker, builds from current worktree per the alga-dev-env-manager skill):
    alga dev up
    alga dev rebuild server   # after schema or types change
    
  • (2026-05-13) Run a single migration backwards (for iterating on backfill logic):
    cd server && npx knex migrate:down
    
  • (2026-05-13) Playwright e2e — see the playwright-testing skill conventions; tests live under server/src/__tests__/e2e/.
  • (2026-05-13) Email loop test (manual): use shared/services/email/processInboundEmailInApp.ts test fixture path; or trigger via the IMAP/MS Graph mock.
  • Design handoff bundle (extracted): /tmp/design_extract/comment-responses/
    • README.md — handoff instructions
    • chats/chat1.md — user's design conversation; the source of intent
    • project/Comment responses.html — entry HTML
    • project/conversation.jsx, comment.jsx, data.jsx, app.jsx — prototype implementation we're recreating in real components
    • project/styles.css — pixel-level visual reference for thread bars, drawer, indent rail, depth cap
  • Implementation plan: /Users/natalliabukhtsik/.claude/plans/immutable-foraging-wirth.md
  • Key existing files:
    • packages/tickets/src/components/ticket/TicketConversation.tsx
    • packages/tickets/src/components/ticket/CommentItem.tsx
    • packages/tickets/src/components/ticket/TicketDetails.module.css
    • packages/tickets/src/models/comment.ts
    • packages/tickets/src/actions/comment-actions/commentActions.ts
    • packages/projects/src/components/TaskComment.tsx
    • packages/projects/src/components/TaskCommentThread.tsx
    • packages/projects/src/components/TaskCommentForm.tsx
    • packages/projects/src/models/projectTaskComment.ts
    • packages/projects/src/actions/projectTaskCommentActions.ts
    • packages/types/src/interfaces/comment.interface.ts
    • packages/types/src/interfaces/projectTaskComment.interface.ts
    • shared/services/email/processInboundEmailInApp.ts
    • shared/workflow/actions/emailWorkflowActions.ts
    • server/migrations/202409071803_initial_schema.cjs (comments table origin)
    • server/migrations/20251118140000_create_project_task_comments.cjs (project_task_comments origin)
    • server/migrations/20260331110000_add_email_threading_diagnostics_columns.cjs (email_sending_logs thread fields)

Open Questions

  • Exact rename/co-existence strategy for email_sending_logs.thread_id (provider) vs new comment_thread_id. Default plan: add new column, leave the old one alone; revisit if it causes confusion.
  • Should comment_threads.email_references be capped in length (mail clients can emit 50+ References entries)? Default plan: no cap initially; revisit if storage growth is an issue.
  • Whether project-task comment soft-delete needs the same "[deleted]" UX text or a different copy (tasks have less context). Default plan: same text, revisit during UI review.