feat(comments): files on a plan's comments, kept in the shared attachment table

Merged
DA
5d ago
+16367-74

Asked for: attachment support in plan comments, kept in a shared attachment database.

What changes

  • Storage. attachment (the shared table that chat, inbox, plan, and conversation files already live in) gains one more owner: a nullable plan_comment_id column that references plan_comment with ON DELETE CASCADE. It is added to ownerColumns, so attachment_one_owner, ownedByTaskAlone, and the attachment_unsaved index all count it. A comment's file therefore never counts as a plan picture, toward the plan's picture bound, or in the unsaved-upload sweep.
    • A new check, attachment_plan_comment_file, requires each comment file to name its task and workspace, a name, and a position, and holds it to a file's bound (3 MB).
    • The migration 20261020224552_attachment_add_plan_comment is idempotent. Its lock order and the reasoning behind it are commented at the top.
  • Wire.
    • A start and a reply can carry attachments in the coding agent's file shape (PNG, JPEG, WebP, PDF, or text), held to the same bounds as a message to an agent. The field is optional, so older clients send what they always did.
    • Body caps grow by the files' bound.
    • Each entry names its files as { attachmentId, name, mediaType }.
  • Store. A comment's files are written in the same transaction as the comment, carrying the plan's id and workspace, so they move with the task the way inbox files do. readPlanCommentFile reads one back for a member of the workspace.
  • Serving. /api/plans/{id}/attachments/{attachment} first looks for a plan picture and, if none has that id, serves a comment file of the same plan. Membership is gated the same way and the bytes headers are the same. Nothing new is routed. The host's plan-attachment read now accepts any type a coding agent's file may be.
  • Window.
    • A comment or reply field has a + picker, takes pasted pictures, and accepts dropped files. Held files show as chips above the words.
    • A sent comment draws its files under its words as chips: pictures as thumbnails, other files by type icon. Each chip opens in the viewer.
  • Agents. read_comments gives the names and types of each comment's files.

Words are still required on a comment that carries files.

Screenshot

A comment with a picture and a PDF under its words, and a reply field holding a text file. Rendered from the panel's own component tree with the panel's stylesheet:

comment with files

Tests

  • packages/database/.../plan-comments.test.ts: a comment's files are named on its entry in order, read by members only, are not plan pictures, and go with the comment.
  • apps/web/tests/plan-attachments.test.ts: a comment's file is served at the plan's attachment path, with its type and the sandboxing headers, to members only.
  • packages/panel/.../plan-comments.test.ts: a picked file is sent with the reply, and a comment's files are drawn and read through the plan's attachment read.
  • apps/web/tests/plan-comments.test.ts: the reply-cap test was raised to match the new cap.

Not in this PR

  • An agent sees a comment file's name, not its contents.
  • A thread handed to an agent by an @mention carries the comment's words, not its files.

Made with Luke


Devin Review