Repository navigation
feat(drafts): conversation rail (Rail-1) — draft-level un-anchored chat surface - #227
Merged
Merged
Conversation
Add the draft-level, un-anchored conversation surface — a flat per-draft
message log distinct from the anchored annotation threads. Near-mechanical
clone of the draft_annotation_message stack, minus the anchor.
- Migration 126: draft_message table (id, draft_id, workspace_id,
open-enum author_type, nullable author_user_id, body, created_at) +
(draft_id, created_at) index. Human-only for now; author_type/
author_user_id exist so Rail-2 (Aye) is a drop-in with no migration.
- Queries: ListDraftMessages / CreateDraftMessage (sqlc-generated).
- Handler draft_message.go: list + create, resolving the parent draft via
loadDraftForUser then using draft.ID; empty body -> 400; publishes
draft_message:created with the resolveActor actor.
- Extract resolveDraftAnnotationAuthor -> resolveDraftAuthor (it was
annotation-agnostic); reused by both surfaces.
- Routes GET/POST under /api/drafts/{id}/messages (workspace-member group).
- WS event EventDraftMessageCreated.
- Tests: create+list, empty-body 400, bad-draft-UUID guard, cross-owner
denial; annotation tests stay green after the rename.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Mirror the annotation-message TS stack for the flat draft_message rail. - Types: DraftMessage / ListDraftMessagesResponse / AddDraftMessageRequest; reuses the open author-type union for enum-drift safety. - Schemas: DraftMessageSchema + ListDraftMessagesResponseSchema (.loose, parseWithFallback) + EMPTY fallbacks + a malformed-response drift test (unknown author_type downgrades; wrong-typed/missing fields fail closed). - Client: listDraftMessages / addDraftMessage through parseWithFallback. - message-queries: draftMessageKeys keyed [wsId, draftId] + list options. - message-mutations: useAddDraftMessage with optimistic append + temp->real swap on success (mirrors the fixed annotation-message hook; flat shape). - message-ws-updaters: onDraftMessageCreated with de-dupe-by-id + temp-match replacement; unit-tested. - Wire draft_message:created into use-realtime-sync (specificEvents + ws.on + cleanup) and add it to WSEventType. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Extract the annotation thread's inline ThreadMessage into a shared common/message-row.tsx (avatar + sender + timestamp + Markdown). Both DraftAnnotationMessage and DraftMessage satisfy its props; the annotation panel imports it (No-Duplication Rule). Panel behavior unchanged — its tests stay green. - conversation/conversation-rail.tsx: scrollable message list (shared row) over a persistent bottom composer (Textarea + Send, Cmd/Ctrl+Enter), empty state; reads draftMessageListOptions, posts via useAddDraftMessage. - conversation/draft-side-panel.tsx: two-tab wrapper — Conversation (default) | Annotations (+ open-count badge). Local useState for the active tab (ephemeral). Owns the w-96 rail chrome; each tab renders embedded (no double chrome — AnnotationThreadPanel gains an `embedded` prop that drops its standalone width/border/title header). - Swap AnnotationThreadPanel -> DraftSidePanel in the editor, threading the annotation props to the Annotations tab + wsId/draftId to the rail. - i18n drafts.conversation.* (en + zh-Hans). - Tests: conversation-rail (renders rows, composer posts + clears, Cmd+Enter sends, empty guard) + draft-side-panel tab switch + badge wiring. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Builds Rail-1 of the Drafts "global germination rail": a draft-level, un-anchored conversation surface (chat-style), distinct from the anchored annotation threads. Humans only this slice —
author_type/author_user_idare modeled now so Rail-2 (Aye viamultica draft say) is a drop-in with no migration.Approach: a near-mechanical clone of the shipped
draft_annotation_messagestack, minus the anchor — a flat per-draft message log. Each layer mirrors the annotation surface 1:1.What's in it
Backend (
migration 126)draft_messagetable:id,draft_id(FK→draft CASCADE),workspace_id(denormalized FK→workspace), open-enumauthor_type(no CHECK), nullableauthor_user_id(FK→user SET NULL),body,created_at;(draft_id, created_at)index.ListDraftMessages/CreateDraftMessage(sqlc, no drift).draft_message.go: list + create, resolving the parent draft vialoadDraftForUserthen usingdraft.ID(UUID convention); empty body → 400; publishesdraft_message:createdwith theresolveActoractor.resolveDraftAnnotationAuthor→ sharedresolveDraftAuthor(it was annotation-agnostic) — reused by both surfaces.GET/POSTunder/api/drafts/{id}/messages(inherits the workspace-member middleware).EventDraftMessageCreated.Core TS —
DraftMessagetypes +indexexport;DraftMessageSchema/ListDraftMessagesResponseSchema(.loose(),parseWithFallback, EMPTY fallbacks) + a malformed-response drift test;listDraftMessages/addDraftMessageclient methods;draftMessageKeys(keyed[wsId, draftId]) + list options;useAddDraftMessage(optimistic append + temp→real swap);onDraftMessageCreatedWS updater (de-dupe + temp-match); wireddraft_message:createdintouse-realtime-sync+WSEventType.Frontend (
packages/views/drafts/)ThreadMessage→common/message-row.tsx(avatar + sender + timestamp +Markdown). Both message shapes satisfy its props; the annotation panel imports it (No-Duplication Rule).conversation/conversation-rail.tsx: scrollable list (shared row) over a persistent bottom composer (Cmd/Ctrl+Enter to send), empty state.conversation/draft-side-panel.tsx: two-tab wrapper — Conversation (default) | Annotations (+ open-count badge). LocaluseStatefor the active tab. Owns thew-96rail chrome; each tab renders embedded.AnnotationThreadPanel→DraftSidePanelin the editor (annotation props threaded to the Annotations tab +wsId/draftIdto the rail;w-96footprint + drag regions unchanged).drafts.conversation.*(en + zh-Hans).Annotation panel — unchanged behaviorally
AnnotationThreadPanelgained an opt-inembeddedprop (drops the standalone width/border/title header when inside the tab wrapper). The default — and everything its own tests render — is identical to before. Its 11 tests pass unchanged after theMessageRowextraction.Verification
make sqlcclean (no drift);go build ./...+go vet ./...clean; fullgo test ./internal/handler/green (DraftMessage create+list / empty-body 400 / bad-UUID guard / cross-owner denial, plus all existing Draft + annotation tests after theresolveDraftAuthorrename). Migrations 123–126 applied locally.pnpm typecheck(all 6 tasks incl. web + desktop),pnpm test(all packages, 866 views + 389 core),pnpm lint(0 errors), i18n parity test — all green.🤖 Generated with Claude Code