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
WithKWKen WuACCacafly · legal2d
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.
Review the draft reply →
Open thread ↗
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
Open thread ↗
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
‹Re: Settlement
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.
> 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
‹Fwd: Settlement
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.
WithKWKen WuACCacafly · legal6d
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
wrong — status = '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 alldone
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 matter → Owed 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.htmlvariant 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 supersedesmatter-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.lime —
lime500/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.