PINGBO · CASE-FILE (MATTER DETAIL) + RAW-THREAD EMAIL VIEW
Screen ⑦ — the case-file rendered · the only operating hub, one tap from anywhere · DESIGN-launch-uiux §Case-file anatomy
9:41􀙇 􀛨 􀋨
Settlement — Cacafly
Why this matter
Cacafly owes $48K on the terminated Q2 contract; recovering it clears the AR hole before the round closes.
📎 Owed: the signed release
With KWKen Wu ACCacafly · legal 2d
Now
To respond
Ken’s indemnity cap is under your floor
Because Ken Wu · Jul 7 ↗ — “Redlined clause 6; the indemnity cap is the one open item.”
Counter at $40K and offer to countersign this week.
Waiting on Allister
Risk-read on clause 6 is not back yet
Because You · Jul 8 ↗ — asked Allister to risk-read the cap before you counter; no reply since. nudge Jul 11
Timeline2 threads · 5 messages
Re: Settlement — CacaflyJul 7
Settlement drafted at $48K net-30; Cacafly’s counsel countered the indemnity cap at $25K against your $40K floor, and that gap is the only open item.
PL
Sent the settlement draft at $48K, net-30 from countersign.
Jun 24
View 2 earlier replies
KW
Redlined clause 6 — the indemnity cap is the one open item.
Jul 7
Risk-read: clause 6Jul 8
Allister was asked to risk-read the indemnity cap before you counter; nothing back yet.
PL
Asked Allister to risk-read clause 6 before countering.
Jul 8
Tell Pingbo, or ask about this Matter…
The Matter page, as it actually ships. Redrawn 2026-07-30 against MatterDetailView.swift — the previous drawing showed a Done-when section and a bordered Status card with an inline draft and Send / Change ▾, and none of those exist on the screen. What exists: the name at 21pt in the body (the nav bar carries no title), Why this matter, the Owed pill when the board projection has items, a With row whose chips scroll sideways with the age pinned at the trailing edge, Now, Timeline, Door.

Now is one card, not one per front. Fronts are separated by hairline dividers. Each opens with its state in mono — To respond for a needs-you front, Waiting on <person> only when the page resolved a counterpart (never a placeholder name in a sentence the user acts on) — then the summary at 16 semibold. The next step line renders for needs-you fronts only, and Review the draft reply → appears only when the server already wrote one for that thread; it routes through openSuggestedDraft, which loads reply context first, so reply-all recipients survive.

Timeline is grouped by thread, not a flat event list, and the fold (View N earlier replies) sits after the opening message — the first message is what everything after it answers. Section headers are 15pt semibold sentence case; the uppercase mono labels this mockup used exist nowhere on the screen. →
9:41􀙇 􀛨 􀋨
Settlement — Cacafly · whole thread · 4 messages
You → Ken Wu
to ken@cacafly.com
Jun 24
10:02

Ken — good talking. Sending the settlement draft as discussed: $48K, net-30 from countersign.

The full terms are in the doc; shout if anything reads off.

— sent from Pingbo
Ken Wu
ken@cacafly.com · to you
Jul 7
16:12

Thanks — mostly aligned. Our counsel redlined clause 6; the sticking point is the indemnity cap. We’d want it capped at $25K.

Revised doc attached. If the cap works, we can countersign this week. Details here: docs.cacafly.com/settlement ↗

> Sending the settlement draft as discussed: $48K, net-30 from countersign…
Ken Wu · General Counsel, Cacafly
PDFcontract-v3.pdf
240 KB
Email display = the raw thread, rendered in-app (WKWebView). Reached by tapping Open thread ↗ on a Timeline mail row. Real HTML body, chronological, attachment chips (📎 parsed), quoted replies. Nav title = the thread subject; the Matter title demotes to the sub row — one Matter can hold several threads, so the zoom view names the thread, not the matter. No "Open in Gmail" door (north star); auth / magic-links open in system Safari, never in-webview. Backend seam built (GET /v0/messages/{id}/thread) — it returns the Matter's messages flat, each row carrying its threadId; the client render filters to the tapped row's thread (or the seam grows ?threadId=). Both halves are gap #9.
9:41􀙇 􀛨 􀋨
Settlement — Cacafly · whole thread · 1 message
You → Allister Chen
to allister@harborcounsel.com
Jul 8
09:10

Allister — Ken’s counsel redlined clause 6: indemnity cap at $25K, our floor is $40K. Can you risk-read the exposure before I counter?

Redline attached; countersign target is this week.

> Our counsel redlined clause 6; the sticking point is the indemnity cap. We’d want it capped at $25K…
— sent from Pingbo
PDFcontract-v3.pdf
240 KB
Multi-thread, same Matter. A Matter links messages, never threads — threads are derived groupings (distinct threadId over the Matter's messages), so a second thread costs the model nothing. This one was born from the forward move (gallery below): one outbound to Allister, no reply yet — the Status "also out" row is its live face (quiet 1d), the Jul 8 Timeline row is its fact. Same ‹ back to the same case-file. The Timeline stays thread-blind; the fork only appears here, at zoom depth — Scene ② and Scene ③ are the two destinations behind two identical-looking Open thread ↗ rows.
9:41􀙇 􀛨 􀋨
Settlement — Cacafly
Why this matter
Cacafly owes $48K on the terminated Q2 contract; recovering it clears the AR hole before the round closes.
With KWKen Wu ACCacafly · legal 6d
Nothing outstanding
Looks finished
Every Thread in this Matter has been judged and none is outstanding — nothing is waiting on you, and nothing is waiting on anybody else.
The messages this rests on
Ken WuWire sent this morning; nothing further needed from your side.Jul 14
Cacafly · legalReturned the signed copy with the cap at $40K as agreed.Jul 12
Timeline2 threads · 6 messages
Re: Settlement — CacaflyJul 14
Settled at $40K on the cap: Cacafly countersigned on Jul 12 and the wire landed Jul 14. Nothing outstanding.
AC
Returned the signed copy with the cap at $40K as agreed.
Jul 12
View 3 earlier replies
KW
Confirmed the wire went out, value-dated today.
Jul 14
Tell Pingbo, or ask about this Matter…
Settled — the page proposes the end (pingbo#564 · #577 · #648). THE CARD NOW SHIPS. MatterDetailView.doneSuggestion renders it, and #648 added the messages it cites — so the card above is drawn from the code, not proposed. Its words are the code’s: Looks finished, then reviewReason verbatim, then Not yet · Mark done.

Why the citations are not decoration. reviewReason is a CONSTANT — REVIEW_REASON in settled-matter-proposal-runner.ts is one sentence reused on every proposal. Read alone it says the identical thing about every Matter, so evidenceMessageIds is the only content that makes the question answerable. An id that resolves to nothing is dropped, never stood in for; and the citations resolve against every message rather than against the Timeline, whose middle rows fold away — a cited message living in the fold is still real.

Still design, not built: the Nothing outstanding framing below, and Not yet opening the Door. Today the view has no such section header — with no fronts the Now block is simply absent — and declining just resolves the proposal and toasts “Kept open.”

Where it sits: above Now, in Now’s place. A settled Matter has no front to show, so the section header reads Nothing outstanding and the card carries the suggestion instead. The card is sage-bordered, never lime — lime is the pop that means a move is waiting on you, and this card’s whole claim is that none is. No Owed pill either: owed items are read from needs-you Nows, and there are none.

A suggestion, never a transition. done stays something the Matter earns from your accept. Not yet opens the Door and asks what still needs chasing (agent-door.html ⑤) — a decline is a conversation, not a dismissal, because the vaguest sentence in the product is "the threads are done but the work is not" and the reason is the missing Now. What you say rides reopenContext into a re-judgment that writes it as a Now on the thread it concerns, and that Now is what stops the re-asking: the bar below excludes any Matter with an outstanding Now, so the suggestion stops qualifying by the same condition that raised it. Contract: plan.md §The Done suggestion (2026-07-30). Note the earlier claim here was wrongstatus = 'pending' does not gate re-proposal: a declined row is not pending, so a reasonless decline re-proposes on the next sweep, which is a live defect in #577.

The bar is positive evidence. It appears only when every thread the Matter owns has been judged and none is outstanding — never on “no Now”, which is absence. Those were indistinguishable until thread_homes.last_judged_at existed (#559/#572). One unjudged thread ⇒ no suggestion at all. A Matter whose threads are all done qualifies vacuously and correctly; one owning no threads never does — empty, not settled.

The three citations are load-bearing: a suggestion you cannot check is an instruction. Measured on the dogfood account 2026-07-29, this is the majority state — 70 of 70 active Matters had no outstanding move, every one rendering in the board’s Waiting lane as though somebody were being waited on. The lane keeps its place; this card is what drains it.
The move — one scoped draft (the agent picks the scope)
Verified against the code 2026-07-30 — read the split before the cards.

Real: scope is a property of the draft, not a move type, and the three below are exactly the server’s draftDeliverySchema — a discriminated union on mode: reply (threadId + inReplyToMessageId), forward (sourceMessageId), new_message. The invariant a draft on this Matter stays on this Matter is enforced, not just intended: a reply draft’s thread must be one of the response’s own threads (contracts/mail.ts:382). The composer does carry a scope chip and a Send.

NOT built: Change ▾ does not exist anywhere in the client — zero matches. The card-level Send drawn below is not on the Matter page either; the shipped path is Review the draft reply →, which opens the composer, and Send lives there. “The user re-scopes freely in the composer” is a stub: tapping ComposerScopeChip raises a toast — “Scope switching arrives with multi-matter drafts.” And the real chip label is free-form (“Acme matter · to Raj”), not the ↩ Re: … · same thread form drawn here.

Only a genuinely new Matter becomes its own Matter — origination via People → *Pingbo, never this card.
reply
↩ Re: Settlement · same thread
“Thanks, Ken. $40K on the cap is firm for us — but we can countersign this week at that number…”
to Ken Wu
forward
↪ Fwd · loop someone in
“Allister — can you risk-read clause 6 (the indemnity cap) before I counter? Ken’s redline attached.”
to Allister · 📎 contract-v3.pdf
new thread · same Matter
✎ New thread · same matter
“Hi Dana — closing the loop on the Cacafly settlement wire. Once counsel countersigns this week, can your team release the $40K net-30?”
to Dana Okafor · CFO, Cacafly

What this pins

Anatomy order, as shipped (MatterDetailView.swift, verified 2026-07-30): title (21 bold, in the body — the nav bar carries none) → Why this matterOwed pill when the board projection has items → With (cast chips, scrolling, age pinned trailing) → Now (ONE card; fronts divided by hairlines) → Timeline + “N threads · N messages” → the docked Door. Headers are 15pt semibold sentence case. Guidelines are agent-internal — held and enforced at review/send/incoming, never a displayed section.
The ··· menu is real, and holds exactly one item: Mark done (ToolbarItem(placement: .primaryAction) → a Menu whose single Button is Mark done with a checkmark). So the verb ships — it just lives in the overflow, not under a Done-when quote.
Designed but NOT built — do not read these off the drawing as shipped: a Done-when section with an editable quote and a Mark-done that cites it; a bordered zone-variant Status card; the inline Send / Change ▾ pair; a side-ask row. The earlier version of this file drew all four as though they existed. What the screen actually offers for a prepared reply is Review the draft reply →, which opens the composer through openSuggestedDraft. The Done proposal in Scene ⑤ is in the same category: server-side real (#577), client-side unrendered.
DECISION 2026-07-30 — Timeline v3. Both changes REVERSE the 2026-07-21 redesign (pingbo docs/adr/matter-detail-redesign/plan.md §3, §4/§10), which is why they are written down rather than just drawn:
Each thread carries an up-to-date summary under its title. The redesign called this “the rejected digest” on the grounds that a thread’s what it’s about is already the Gmail subject titling the section. That reasoning holds for what it’s about and misses where it stands — which is what a reader of a 15-message thread actually needs, and what the fold hides. Source (v0): the newest message’s summary, projected at read time — no new column, no new model call, and up-to-date by construction because the newest message defines it. Falls back to the body’s opening where that summary is absent (it is on 550 of 853 Matter-homed rows today). Not v0: a distilled thread-state sentence — that needs its own model call per arriving message AND its own freshness watermark, the exact machinery #569 had to build for L1; worth doing only if the projected line reads thin in dogfood.
The message rows get their left rail back — node, connector, fixed mono time column, then the initial avatar. The redesign removed it (“no vertical rail”) reasoning that a thread is a conversation read forward, not a reply-pile, and that the section header already groups. True — but the rail is not doing grouping here, it is doing time: with a summary line now sitting above the rows, the rows need a scannable spine. Ported from that ADR’s own timeline-ab-mock.html variant A rather than reinvented, so the two drawings stay one language.
Who and when are ONE column, and the row is vertically centred. The initial icon sits directly over its own datetime instead of standing beside a separate time column — they are one fact about a message, so they read as one block, and it returns ~42px of width to the summary on a 376pt screen. The dot, the identity block and the summary share one centre line. This reverses an earlier top-aligned version whose stated worry was that a two-line summary would push the icon off the rail’s axis; centring the rail itself is what answers that — the connector became a full-height element behind the dot (clipped at the first and last dot), so the dot sits at the row’s middle however tall the summary grows, which top-alignment never gave. Narrows matter-activity-projection’s revived rule twice over: that ADR had the time in an “adjacent fixed column” with the avatar in the content row, and centred the node on the date/time block alone rather than on the whole row.
Every thread group collapses. Real disclosure, drawn in both states above (the second group is closed). What stays visible when closed is the point: title, date and the thread summary — collapsing is only safe BECAUSE ① exists, since a closed thread that showed only a subject would hide where it stands. Hidden: the message rows and the fold. Not built on the client: the shipped Timeline renders every section’s rows with no disclosure.
Every Now front says WHY it is on you, or why it is waiting — and this one needs no new AI field. Both members of the Thread Now union already carry evidenceMessageIds (it is in threadNowBaseShape), so the reason renders as the citation that produced the Now: sender · date · the line itself, with the trust-ladder . Unciteable by construction — a reason with no evidence cannot be drawn. needs-you cites the message that put the ball on you; waiting cites what YOU last did, why the other side owes a reply, and — free from nudgeAt, which only the waiting variant carries — when Pingbo will chase. The waiting front previously said nothing beyond its summary; a lane that asserts “waiting on Allister” while showing no basis is the same unevidenced claim #564 is about, one level down.
The row’s timestamp is ONE token from MailTimestamp, never a time-over-date stack. That formatter (PingboShared/MailTimestamp.swift) escalates by age and returns exactly one string: 1m under an hour (floored — a just-arrived item never reads “0m”) → localized time same-day (jm) → Yesterday → the weekday within the week (EEE) → MMM d same-year → MMM d, yyyy older — with a separate Chinese branch (N 分鐘 / 昨天). An earlier draft of this file drew 10:02 over Jun 24, a shape the app cannot produce: a Jun 24 message in the same year renders Jun 24 and no time at all. Consequence for layout: the column must fit the longest rung in the reader’s locale, not a fixed HH:mm. This supersedes matter-activity-projection’s revived rule that the time column always shows “localized system short time” sized for Jul 2, 2025 — the shipped formatter does not always show a time.
The rail spans exactly first-dot → last-dot — never above the oldest message or below the newest. It cannot be one element: the dots’ positions depend on how tall each summary wraps, which CSS cannot read. So each dot draws its own two segments (up to its row’s top edge, down to its bottom edge) and consecutive rows meet at the boundary; the first dot drops its upward segment and the last drops its downward one. The fold carries a full-height segment of its own, since it sits between two dots and would otherwise leave a gap where hidden messages are.
Spacing rules the render forced, each with a reason: the seam between the identity block and the summary is 15px while the seam inside it (dot→icon) is 9 — a block reads as one unit only when its internal spacing is tighter than its external. The datetime is anchored to the icon (absolutely, inside the centred row) rather than stacked after it: the icon is centred in a row as tall as the summary, so a row-relative datetime drifted further from the icon the more the summary wrapped. And the Why→Owed gap tightens from 24px to 12 — 24 is what ships (MatterDetailView’s .padding(.bottom, 24)) and it left the pill floating between two blocks; with no pill the 24 stands, because that IS a section break.
The section chevron is a hanging gutter. It sits in the group’s left padding, so the thread title, the thread summary and the message rail share one left edge. Inline, it indented the title alone and the header stopped reading as one column.
Unchanged by this decision: sections stay current-first, rows stay oldest→newest within a thread, the fold stays after the opening message, and there is still no CURRENT chip.
Timeline is grouped by thread, and its rows are messages only: a tappable subject + last-activity header, then avatar · one-sentence summary · timestamp per message, with View N earlier replies folded in after the OPENING message (the first message is what everything after it answers). Agent moves are not interleaved as rows today — an earlier version of this legend claimed they were. The row’s sentence is message.summary when the wire carries one and the body’s opening when it does not, which is visibly worse and known.
A prepared reply is a link, not a Send button (today): when the server already wrote a draft for a front’s thread, the front shows Review the draft reply → and the composer opens with reply context loaded — that is where reply-all recipients come from. The merged verdict brief with inline Send / Change ▾ remains the design (FEAT-verdict-brief §21), unbuilt on this screen.
Multi-front: a Matter with several open strands renders every front in the one Now card, separated by hairline dividers — not a critical-path card plus a “side-ask” row, which is what this legend used to claim. Each front carries its own state line and its own Open thread ↗, and maps to exactly one Timeline section by threadId.
Multi-thread: a line links messages, never threads — threads are derived groupings. The Timeline is thread-blind (a row never names its thread); Open thread ↗ zooms to that row's thread, and the zoom view's title is the thread subject with the Matter title in the sub row. The scope new thread · same matter (and forward) is how a Matter grows its second thread.
Colour tokens do NOT port across by name — match on hex. This file’s --accent is the lime (#CCEF33 light / #D4F038 dark), which is PingboColor.limelime500/lime450. But PingboColor.accent in the app is a legacy alias for primary, i.e. forest #1C251F — the token file says so outright: “equals primary (forest), NOT the lime pop.” Same word, opposite colour. The lime Send drawn in the gallery below IS correct — PingboActionButton(“Send →”, variant: .lime) (Composer.swift:187) fills with PingboColor.lime and labels with onLime = forest800 #162219, which is this file’s --on-accent exactly.
Trust ladder = three zoom levels, a concept — NOT labels in the UI: distilled (the Timeline row you read) → single original (that one raw email) → whole raw thread (its whole thread, WKWebView). The app never prints the words "distilled/original"; you just tap deeper. No “Open in Gmail” door in daily UI (north star); durable export/delete live in You.
The Door (orb + placeholder) is the ask/tell input, docked at the floor of every case-file.