# peer-dm-thread.screen Single-screen breakdown for the **peer-DM thread** — a chat-home-shaped surface between two `colleague`-state providers, with every message round-tripped through both sides' `ai-copilot` per [Brief AE §AE3](./AE-provider-social-network.brief.md). Each side sees the other in *their own* preferred language, [AD-opaque](./AD-multilingual-opaque.brief.md) per AD3 + AD6, with the **only sanctioned opacity break** being a long-press "see what the peer literally wrote" verbatim expand on a recipient bubble (consented at connection time per AE3). Voice register: working by default, plain for blocks / K3 holds / vacation gates, hearth nowhere on this surface (peer-DM is not ambient). ## Constraints - Both peers must be `colleague`-connected (AE2). `follow` state does not unlock this surface. - Every outbound passes K3 PII gate + voice §V6 banned-phrase check + AD3 register-faithful re-authoring into recipient's preferred language. No "translated from…" annotation, ever (AD opacity invariant). - AD6 audit triad recorded per turn: original / canonical-EN / recipient-view / delivered. `turn_id` links sender outbound row to recipient ingest row in `agent_actions` (AE-Q3: peer_messages stores IDs + delivery state, canonical row lives in audit spine). - Verbatim-expand is the **only** AD opacity break in the product. Recipient-side only. Both peers consented at connection time. Sender's bytes shown read-only, side-by-side with the recipient-view rendering. Never auto-revealed; never annotated as "translation"; never narrated to the recipient that this affordance exists *for them* without an explicit settings nudge. - `@model-boss /translate` is the mediation router. On router unreachable → [M §M2a](./M-error-degraded-modes.brief.md) degraded-mode fallback per AD low-confidence rules below. - Multi-message debounce (chat-home §state 2b/3a) **applies here too**: peers type in bursts; the turn assembler coalesces within the 2.5s text / 1.5s voice silence window before firing through mediation. Coalesce boundary is per-side, not shared. - Out at P0: voice/audio call, file attachment. Photo paste is composer-side TBD (defer to AE-Q follow-up). ## Layout (iPhone 17 logical 393×852) ``` ┌─────────────────────────────────────────────────┐ │ ◄ status bar (system) │ 47pt ├─────────────────────────────────────────────────┤ │ [◀] Sarah-K [colleague] [⋯ menu] │ 56pt — peer header (handle + state badge) │ last active 12m · de-DE │ sub-line: presence + peer's locale (subtle) ├─────────────────────────────────────────────────┤ │ │ │ ┌───────────────────────────────────────────┐ │ recipient bubble (left, peer) │ │ Habe gestern die Tryst-Listings für │ │ ← Quinn sees this rendered in en-US: │ │ Berlin überarbeitet — bist du dran? │ │ │ │ · mediated 12:01│ │ subtle "·" dot = mediation indicator │ └───────────────────────────────────────────┘ │ long-press → verbatim-expand (state 9) │ │ │ Quinn: yeah, going through mine now │ user bubble (right-aligned, accent tint) │ 12:03 │ debounced — see state 3a │ │ │ ┌───────────────────────────────────────────┐ │ recipient bubble — streaming mediation │ │ Magst du mir deinen Bio-Diff schicken? │ │ ← Quinn sees: "want to share your bio diff?" │ │ · mediated · streaming… │ │ thin pulsing dot at end of bubble │ └───────────────────────────────────────────┘ │ │ │ ├─────────────────────────────────────────────────┤ │ ╭─────────────────────────────╮ [🎤] │ 56pt — composer │ │ Reply to Sarah-K… │ │ placeholder rotates per state │ ╰─────────────────────────────╯ ↗ send │ └─────────────────────────────────────────────────┘ │ 34pt — home indicator ``` ## Components (in z-order, top to bottom) | Component | Brief ref | Notes | |---|---|---| | Peer header | AE2, AE5 | Back chevron → previous surface (chat-home or peer-roster). Center: peer handle + state badge (`colleague` chip; `muted` / `blocked` chips in their states). Sub-line: presence ("last active 12m") + peer's locale code in subtle dimmed type (this is the only place locale ever appears — it disappears once you are inside the thread bytes per AD opacity). Right: `[⋯]` overflow → mute · block · unmute-verbatim-expand toggle · view peer profile · report (AE10) · end colleague-connection. | | Recipient bubble | A §states, AE3 | Left-aligned, no tint. Rendered in **recipient's preferred language** (recipient-view from AD6 triad). Footer: `· mediated` dot + timestamp. The dot is the *only* surface label for mediation; never reads "translated", never names a source language, never annotates confidence. Long-press → verbatim-expand sheet (state 9). | | User bubble | A §states | Right-aligned, accent-tinted. Plain user-authored bytes; never shows mediated text to the sender — Quinn sees what Quinn wrote, full stop. AD opacity holds even on sender side. Editable for 30s post-send per chat-home pattern. | | Mediation dot | AE3, voice §V6 | A 4pt subtle `·` glyph after bubble body. Tooltip-on-long-press reads exactly: "mediated". Never "translated". Never "AI-rewritten". Never names a language. This is the entire UI surface area for the AE3 invariant. | | Verbatim-expand sheet | AE3 (the only AD break) | Long-press recipient bubble → modal sheet, ¾-height. Two stacked panes: **top** = "Sarah-K wrote, in de-DE:" with the peer's raw bytes (read-only, monospace-ish weight, copy-disabled by default per AE10 risk surface); **bottom** = "You read:" with the rendered recipient-view bubble. No "diff", no inline highlights, no confidence number — just both, side-by-side. Single dismiss action. Audit row `agent_actions(verb='verbatim_expanded', turn_id=...)` written on open. | | Composer | A §multimodal, AE3 | Text field; mediation happens on send, not on type — Quinn types in her language and never sees a preview of the mediated outbound (AD opacity holds on sender side). `[🎤]` short-tap voice input; long-press disabled here (no cook-mode in peer-DM at P0). Send `↗` affordance per chat-home §state 3a appears when a multi-message turn is assembling. | | Multi-message debounce indicator | A §state 2b | Thin pulsing dot under the last user bubble while the silence window is open (2.5s text / 1.5s voice). "Send now" `↗` next to composer fires the turn immediately through the mediation pipeline. | | Connection-state banner (conditional) | AE2, AE10 | Full-width strip above composer when state ≠ `connected`: muted-by-you, muted-by-peer (invisible to muted side per AE10), blocked-mid-thread, downgraded-to-follow, peer-incognito-mid-thread. Plain register, single-line, single action ("undo mute" / "view block reason" / etc.). | ## States 1. **Empty — first DM** (working) — No prior history. Centered prompt: "First message to Sarah-K. Mediated — you write in yours, they read in theirs." Composer placeholder: "Say hello." Sub-line under prompt is a single low-emphasis link: "How mediation works" → opens AE3 explainer sheet (one-time; the *only* moment the system explains AD opacity to the user mid-flow). 2. **Drafting** — User typing in composer; no bubble yet committed. Standard. 3. **Multi-message debounce assembling** — Per A §state 2b: Quinn sent N messages within the silence window. Bubbles render tight-grouped (no timestamp between, single trailing `· mediated` dot on the *coalesced* outbound — Sarah-K will see one mediated turn, not N). "Send now" `↗` available on the composer to force-commit early. 4. **Streaming-mediation (inbound)** — Peer's `ai-copilot` is mid-stream rendering the recipient-view into Quinn's locale. Bubble shows partial tokens with stagger-fade; footer shows `· mediated · streaming…`. Composer remains active; sending mid-stream is fine (her outbound debounces independently). Cannot long-press a streaming bubble for verbatim-expand — affordance unlocks on stream complete. 5. **K3 PII blocked outbound** (plain) — Outbound contained K3c-1 govt name / K3f-2 precise location / K3h channel-vs-surface leak. User bubble is *not* committed; composer text is preserved. Inline plain-register receipt above composer: "Held that back — restricted content. See audit for the row." No naming of language, no naming of which K3 rule fired in the surface (the rule ID lives in audit only, per AD5 locale-invariance + AE3 opacity). 6. **AD low-confidence draft** (working, dialed soft) — `@model-boss /translate` returned confidence < 0.65 for the inbound mediation. Recipient bubble renders best-effort recipient-view; a single low-emphasis line below the `· mediated` dot reads: "I'm not fully sure I understood. Want to write back yourself?" *Never* "translation was uncertain". *Never* "low confidence". The hint reads as a working-register conversational cue, not a system error. Composer is normal. 7. **Peer muted (by you)** — Connection-state banner: "You muted Sarah-K. New messages won't notify you." Existing thread fully readable, composer fully usable (muting is recipient-side notification suppression per AE10, not a send-block). Tap banner → unmute. 8. **Peer blocked mid-thread** (plain) — Either side blocked the other while the thread was live. Connection-state banner: "Colleague connection ended. New messages can't be sent." Composer disabled. Existing bubbles remain readable to both sides (the audit spine survives blocks per AE10 + I). Header state-badge swaps `colleague` → `blocked`. `[⋯]` reduced to: report · view audit. 9. **Verbatim-expand (long-press)** — The only AD opacity break. Modal sheet renders peer's raw bytes (top) + recipient-view (bottom), per the Verbatim-expand-sheet component above. Audit row written on open. Sheet has no "translate", "diff", or "confidence" controls — just both panes, dismiss. Re-entry into the thread restores scroll position. 10. **Received-while-offline / catching up** — Returned from offline; backlog renders top-to-bottom oldest-first with a quiet "you missed 4 messages from Sarah-K" hairline above the first new bubble. No badge animation, no haptic; mediation already completed server-side, bubbles are static. 11. **Cross-locale steady state** — Default sustained state: Quinn writes en-US, Sarah-K writes de-DE; each sees the other in her own language; the `· mediated` dot is the only signal anywhere on the screen. Locale code under header is the only place "de-DE" appears in the UI; it does not surface on individual bubbles. 12. **Peer-DM during vacation mode** (plain, brief H §H1) — User on vacation. Inbound bubbles still mediate + arrive, but no push notification fires. Top hairline strip: "Vacation mode — peers see you as away. Replies hold until you return." Composer still usable if user chooses to break vacation. 13. **Peer-DM during peer's incognito flip** (working) — Peer flipped AE11 posture to incognito mid-thread. Past thread still readable to both (consent at connection persists per AE11). Connection-state banner: "Sarah-K stepped back. No new messages from her side until she's back." Composer disabled for *outbound* (per AE11: incognito stops surfacing, including receiving). Header state-badge unchanged (`colleague` still); presence sub-line removed. 14. **Connection downgraded follow mid-thread** (plain) — Peer downgraded `colleague` → `follow`. Thread becomes read-only for both sides. Connection-state banner: "Colleague connection ended — follow still active. No new DMs." Composer disabled. History preserved. 15. **Both peers incognito (frozen archive)** — Both AE11 incognito. Thread is fully sealed; even header presence sub-line is absent. Read-only to both. No `[⋯]` menu actions except "view audit" + "leave thread" (leaves the surface; data persists). 16. **Mediation router degraded** (plain, M §M2a) — `@model-boss /translate` unreachable. Inbound bubbles queue with a top banner: "Mediation paused. Messages will surface when it's back." No raw bytes leak to the recipient during the outage (AD opacity holds even under degradation). Composer accepts outbound but holds in a local queue with the same banner copy. ## Interactions / gestures - **Long-press recipient bubble** → verbatim-expand sheet (state 9). The ONLY AD-opacity break in the product. Audit row written. - **Long-press user (sender) bubble** → standard edit-within-30s affordance per chat-home; no verbatim affordance on sender side (sender already sees their own bytes). - **Swipe-left on any bubble** → reply-with-quote-stub. Composer pre-fills with a quoted excerpt of the swiped bubble (recipient-view bytes for received bubbles; sender bytes for sent). Replying does not unlock verbatim for the quoted excerpt — quote shows what *this side* read. - **Swipe-right on any bubble** → quote-only (inserts inline quote block into composer without focus-stealing). - **Tap header (handle / badge)** → opens `peer-profile.screen.md` (AE5). - **Tap `[⋯]` overflow** → action sheet (mute · block · view profile · report · end-connection · audit). - **Pull-to-refresh** → re-pulls thread state + peer presence from platform.api. - **Tap `· mediated` dot** → one-line tooltip "mediated". No further detail surfaced. (Verbatim is reached via long-press only.) - **Send `↗` (debounce assembling)** → forces immediate commit of the in-progress multi-message turn through the mediation pipeline. ## In-the-wild copy - (working, empty / first DM) "First message to Sarah-K. Mediated — you write in yours, they read in theirs." - (working, drafting placeholder) "Reply to Sarah-K…" - (working, AD low-confidence inline cue) "I'm not fully sure I understood. Want to write back yourself?" - (plain, K3 blocked outbound) "Held that back — restricted content. See audit for the row." - (plain, peer-blocked-mid-thread) "Colleague connection ended. New messages can't be sent." - (plain, mediation router degraded) "Mediation paused. Messages will surface when it's back." - (plain, vacation mode banner) "Vacation mode — peers see you as away. Replies hold until you return." - (working, peer incognito mid-thread) "Sarah-K stepped back. No new messages from her side until she's back." - (plain, downgraded to follow) "Colleague connection ended — follow still active. No new DMs." - (working, verbatim-expand sheet header — top pane) "Sarah-K wrote, in de-DE:" - (working, verbatim-expand sheet header — bottom pane) "You read:" - (tooltip on the mediation dot) "mediated" ## Edge cases - **K3 leak in a draft (composer text)** before send — pre-send scan flags + the send button is disabled with a single-line plain cue under composer: "Restricted content — adjust before sending." No naming of which K3 rule. No naming of language. Voice §V6 + AD5 locale-invariance jointly hold. - **Connection downgraded to follow mid-thread** — per state 14: thread freezes read-only, history preserved, composer disabled; header badge swaps. No notification fires to either side that the *thread* froze; only that the connection state changed (AE2 handles that surface separately). - **Both peers flip incognito (state 15)** — neither side can send; both retain read access; presence sub-line removed; header `[⋯]` reduced to audit + leave-thread. Audit spine intact for both. - **Vacation mode on Quinn's side, peer messages arriving** — per H §H1: bubbles still mediate + render in the thread for when she returns, no push notification per [notification-rich-preview](./notification-rich-preview.screen.md) state 4 (vacation gate). Composer remains usable if Quinn opens the thread voluntarily mid-vacation (no soft-block on the user's own surface). - **VoiceOver active** — reading order per [brief X](./X-accessibility.brief.md): header (handle → state badge → presence) → bubble cluster (chronological) → composer last. Within a bubble: timestamp → `· mediated` indicator → body. Verbatim-expand sheet: top pane label → top pane bytes → bottom pane label → bottom pane bytes → dismiss. Verbatim-expand affordance is announced as "long-press to see what {peer} wrote in their language" — and only when accessibility hints are enabled (otherwise the affordance discovery is intentionally quiet per AE3 opacity). - **Reduced motion** — debounce dot becomes static; streaming-mediation token reveal becomes single crossfade per bubble. - **Dynamic Type XXL** — recipient bubble `· mediated` footer wraps to a second line; verbatim-expand sheet panes become full-height-stacked-scroll instead of side-by-side-vertical. - **Right-to-left peer locale (per AD-Q4)** — recipient-view rendering flips per recipient's locale (Quinn always sees her own LTR/RTL preference); peer's raw bytes inside verbatim-expand render in peer's locale direction (the sheet is the one place the peer's locale direction is honored). Sender-view (Quinn's own bubbles) always renders in Quinn's locale direction. - **Mediation router degraded + multi-message debounce active** — debounced turn holds in the local queue; "send now" still functions but the outbound never leaves the device until the router recovers. Audit row written only on actual `@model-boss /translate` round-trip success. - **Peer's `ai-copilot` ingests a banned phrase (voice §V6) on inbound** — recipient bubble still renders (transparency per AE constraint: "the ingest path flags them on inbound for the recipient's information without redacting"). Flag surfaces as an unobtrusive icon next to the `· mediated` dot, single-tap explanation: "this contains language flagged by the safety list — shown anyway so you can decide." Plain register on the explanation; never blocks the inbound view. - **Quinn tries to copy from inside verbatim-expand top pane** — copy is disabled by default; affordance reads: "Verbatim view is for reading. Copy isn't enabled here." (AE10 risk surface: prevents trivial exfiltration of peer's raw bytes outside the consented context.) Toggle in settings (S §settings-ia) per-account if user wants to enable. ## Related - [Brief AE §AE3](./AE-provider-social-network.brief.md) — peer-DM mediation spec. - [Brief AE §AE7](./AE-provider-social-network.brief.md) — endorsement context (header state-badge derives from AE2; peer profile surfaces AE7 endorsements). - [Brief AE §AE2](./AE-provider-social-network.brief.md) — connection state machine (header badge + connection-state banner). - [Brief AD](./AD-multilingual-opaque.brief.md) — opacity invariant + AD6 audit triad + AD3 register-faithful re-authoring + AD5 locale-invariant K3 + AD-Q4 RTL. - [Brief A](./A-chat-surface.brief.md) — bubble + composer + multi-message debounce patterns this surface inherits. - [chat-home.screen](./chat-home.screen.md) — sibling surface; debounce (§state 2b / 3a) and streaming-reply (§state 4) patterns are reused here. - [approval-card.screen](./approval-card.screen.md) — sibling pattern (action bar shape, "Why" line discipline); peer-DM does not embed approval cards (peer-to-peer messaging is not an approval surface). - [notification-rich-preview.screen](./notification-rich-preview.screen.md) — push surface for inbound peer-DM (subject to vacation gate + DND). - [00-system-voice](./00-system-voice.md) §V2b / §V2c — working + plain register copy on this surface. - [Brief K §K3](./K-safety-blocklist.brief.md) — PII gate on outbound + K3c-1 / K3f-2 / K3h locale-invariance. - [Brief I](./I-audit-trust-replay.brief.md) — `turn_id` linkage + verbatim-expand audit row + connection-state-change audit rows. - [Brief M §M2a](./M-error-degraded-modes.brief.md) — `@model-boss /translate` degraded mode. - [Brief H §H1](./H-recurring-chores.brief.md) — vacation-mode notification suppression. - [Brief X](./X-accessibility.brief.md) — VoiceOver reading order + reduced motion + dynamic type. ## Out of scope - **Voice / audio call between peers** — P5+ at earliest; AE3 is text-mediated only at P0. - **File / image attachment in peer-DM** — P5+ (defer to AE-Q follow-up; current spec is text-mediated only). - **Group salon thread** — separate surface (AE4); see future `salon-thread.screen.md`. - **Org-attributed peer-DM** — AE6 + W §W4 personal-only at P0; org-attribution P5+. - **Raw-bytes peer-DM mode** (no mediation) — P5+ at earliest with extreme caveats (AE-Q4 lean); the verbatim-expand affordance is the only consented break, recipient-side only. ## Open questions - **DM-Q1** Typing indicator — show the peer that the other side is composing (chat-home §state 3a-style pulsing dot)? Across the mediation pipeline the "composing" signal would have to traverse `@model-boss` (or short-circuit it as a presence ping); either way it leaks burstiness. **Lean: opt-in per-connection at colleague-acceptance time; off by default; lives next to the verbatim-expand consent.** `[blocking]` - **DM-Q2** Read receipts — default on, default off, or opt-in per-connection? Receipts on peer-DM carry different social weight than client receipts (peers are operators, not subjects). **Lean: default off; opt-in per-connection at colleague-acceptance time; mutually-on or off (no asymmetric "I see your reads but you don't see mine").** `[blocking]` - **DM-Q3** Message retention window — peer-DM bubbles surface forever (audit retains regardless per brief I), or do they age off the UI surface after N days for privacy hygiene (data still in audit, just not scrolled into chat-home thread view)? Affects how much history a peer can long-press verbatim-expand. **Lean: surface-retain 90d default; per-peer overrideable in settings; audit retains forever per I append-only spine.** `[blocking]`