Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/design/threading-surface-ruling.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
1. **Headline card.** A thread shows in the channel as one card at the root message's position: root author + first line, reply count, last-activity time, and the avatars of up to three participants. No reply bodies in the channel column.
2. **Inline expand, indented rail.** Opening a thread expands it *in place* under the headline card, replies indented along a rail on the left; the channel column does not scroll-jump. Collapse returns to the card, and **collapse state is persisted** per user per thread (55852 verbatim: "collapse state persisted") — a reload does not re-expand what you closed.
3. **No side panel.** There is no thread pane in the inspector and no route change. (Mobile at 390px is the reason this option lost: a side panel has no 390px form; inline expand does.)
4. **The composer stays one element.** Posting inside an expanded thread uses the same composer, re-targeted **to the thread, not to a message**: an ordinary in-thread post sets `thread_root_id` only and leaves `reply_to` null, so it is ambient and the wake-scoping test ("`thread_root_id` alone enqueues no `chat.mention`") holds. `reply_to` keeps its existing meaning — the explicit "reply to this person" gesture — and is set only when the user invokes it on a specific message; that post is addressed to that author (routed, as today) and ambient to everyone else. No second composer is mounted. Write-path consequence: an in-thread post carries `thread_root_id` explicitly from the client, validated server-side (root exists in this pod; the root's own `thread_root_id` is null — no nesting); derivation from `reply_to` remains for explicit replies and for the backfill. Explicit wins when present, derivation fills when absent, and a mismatch between the two is a 400 — **but agreement is not refused**: a reply edge and an explicit root that point at the same thread pass the resolver, so the composer's one-target-two-kinds exclusivity is the *enforcing* layer for "an in-thread post addresses nobody", not a convenience in front of a server that would catch it (#1153). That is why both send paths — text and upload — must carry the same target, and why each is pinned. And **a send consumes the target on every path**: after a successful text or image send the composer clears both kinds (pod 57483/57486) — the chip is a promise about the *next* send, and a chip left standing after the message it described has gone is a lie that costs more than one click to re-aim. *(Amended 2026-08-22 after pod-architect found the collision at 56871: the first wording set `reply_to` on every threaded reply, which made every thread post routed and skipped ambient delivery entirely.)*
4. **The composer stays one element.** A thread is aimed from any message, the headline card, or the expanded rail (#1169 — there is no privileged entry point), and posting into it uses the same composer, re-targeted **to the thread, not to a message**: an ordinary in-thread post sets `thread_root_id` only and leaves `reply_to` null, so it is ambient and the wake-scoping test ("`thread_root_id` alone enqueues no `chat.mention`") holds. `reply_to` keeps its existing meaning — the explicit "reply to this person" gesture — and is set only when the user invokes it on a specific message; that post is addressed to that author (routed, as today) and ambient to everyone else. No second composer is mounted. Write-path consequence: an in-thread post carries `thread_root_id` explicitly from the client, validated server-side (root exists in this pod; the root's own `thread_root_id` is null — no nesting); derivation from `reply_to` remains for explicit replies and for the backfill. Explicit wins when present, derivation fills when absent, and a mismatch between the two is a 400 — **but agreement is not refused**: a reply edge and an explicit root that point at the same thread pass the resolver, so the composer's one-target-two-kinds exclusivity is the *enforcing* layer for "an in-thread post addresses nobody", not a convenience in front of a server that would catch it (#1153). That is why both send paths — text and upload — must carry the same target, and why each is pinned. And **a send consumes the target on every path**: after a successful text or image send the composer clears both kinds (pod 57483/57486) — the chip is a promise about the *next* send, and a chip left standing after the message it described has gone is a lie that costs more than one click to re-aim. *(Amended 2026-08-22 after pod-architect found the collision at 56871: the first wording set `reply_to` on every threaded reply, which made every thread post routed and skipped ambient delivery entirely.)*
5. **The rail is the context; no quote block for the visible parent** (Sam, pod 57487). Quote-and-thread are independent mechanisms: a `reply_to` renders its quote block only when the quoted message is *not already visible* as the thread parent in the same expanded view. Independence is full, both ways (Sam, 57491): inside an expanded thread the quote mechanism stays entirely available — a participant may quote **any** message, in-thread or out, and that quote renders, because it adds information the rail does not show. What is suppressed is only the redundant case: the auto-carried quote of the thread's own root when the reply sits directly under it on the rail. Predicate: hide the quote iff `replyTo.id === thread_root_id` **and** the reply is rendering inside that root's expanded rail; every other quote shows.

**Scoping and follow (from the #1045 ruling)**
Expand Down
Loading