fix(activity): make the page match the ruled canvas, not just its tokens - #1522
Conversation
Phase A (#1518) painted the new tokens over the old layout, so the live page kept the big composer heading, a bordered textarea with a 44px ink block beside it, 19px section headings with eyebrows, circle marks, and a reply box squeezed into the right column. Sam: not close to the preview. This is the canvas's after-side as structure: - composer is one radius-lg card: uppercase 11px label + pod chip on top, borderless textarea, a foot row with the hint and a 30px ink Send; focus is the halo on the card - section headings are 15/20 with an ink count badge and the description as muted meta on the same line; eyebrows gone - rows: name + meta on the top line, body in ink, the reply line UNDER the message (input 32px, ink Send, quiet Acknowledge / Open thread); square marks; pending cards radius-lg with the strong ring - the producer now stores actorName (resolved from the User row when a PG message carries only user_id), so a mention row is headed by who said it instead of 'Someone mentioned you' Frontend: 86 tests green (Activity, invariants, i18n keys), tsc clean. Backend: attentionItemService 9/9 incl. the author-lookup case. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UHfcrzjN6MpeuCCAap5Qnb
…ts the card At 390 the mobile block stacked the section heading as a column with align-items: stretch, which turned the count badge into a full-width bar, and the pod picker overflowed the composer card. Row + wrap + baseline for the heading; the chip label stays on one line and the select shrinks. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UHfcrzjN6MpeuCCAap5Qnb
lilyshen0722
left a comment
There was a problem hiding this comment.
SR-GATE: APPROVED @ 2b91fb08 — with one sequencing note that matters before the backfill runs.
The backend half of this PR is not scope creep, it is a real defect fix, and it is one I missed on #1517. recordMentionedUsers derived the author as message?.username || message?.userId?.username || 'Someone'. On the PostgreSQL path — the default for chat messages — Message.create returns INSERT … RETURNING *, and config/schema.sql:42 shows the messages table has user_id and no username column, so both fallbacks are undefined and every live mention rendered "Someone mentioned you." I reviewed that exact line on #1517 and checked the podId / userId / content field-name fallbacks without checking this one.
Sequencing: the fix reaches new mentions only, unless the backfill runs after it
recordForRecipients writes under $setOnInsert, which is correct and deliberate — it is what stops a retried source write from resurrecting a resolved card. The consequence here is that actorName and the corrected title are never applied to an AttentionItem that already exists. So:
- every row materialized between #1517 shipping and this PR keeps
"Someone mentioned you"permanently; - and if the attention-items backfill is applied before this lands, every backfilled row is written with
"Someone"too, also permanently.
@sam this bears directly on your 17:37 plan — dry-run the backfill, bring you the counts, apply only on your say-so. Land #1522 first and the backfill writes real names; apply the backfill first and it bakes in "Someone" for the whole 14-day window.
What I verified
The #1518 invariants still hold, which was the live risk in a 227-line CSS restructure that ships no change to v2-layout-invariants.test.ts. Re-derived each assertion against the new sheet rather than waiting on CI:
ok .v2-activity__queue-row <- border: 1px solid transparent
ok .v2-activity__queue-row <- border-radius: var(--v2-radius)
ok .v2-root .v2-activity__queue-actions button <- background: var(--v2-ink)
ok .v2-root .v2-activity__compose button <- var(--v2-ink)
ok .v2-activity__queue-row--settled <- box-shadow: none
ok .v2-pane--main <- border-radius: var(--v2-content-radius)
row+row divider rule present? False
outline: 2px in any .v2-activity focus-visible rule? False
The sheet parses. Served over http and read in Chromium: 1234 → 1243 cssRules, same last rule. No silent collapse from the restructure.
Non-blocking
resolveAuthorName'scatchreturns'Someone'— the same value the no-author path returns, so a User lookup failure is indistinguishable from a genuinely unknown author, and it is unlogged. Small, but it is the exact shape TASK-099 spent the day removing, re-entering in a new function two hours later.recordApprovalsetsactorName: agentName || undefinedwhile itstitlestill falls back to'Approval requested'. Consistent, but it means an approval with noagentMetadata.agentNamehas no actor for the UI's new topline to use; worth confirming the fallback renders.
Scope limits
Test & Coverage and E2E Tests are pending. I did not run any frontend suite — no frontend/node_modules in this checkout — so the component restructure, the seven locale strings and the CSS are read plus the two measurements above, not executed. The invariant re-derivation is my own reimplementation of those assertions; CI running the real file is still the authority.
Why
Phase A (#1518) painted the new tokens over the old layout. Live, the page kept the big composer heading, a bordered textarea with a 44px ink block beside it, 19px section headings with eyebrows, circle marks, and the reply box squeezed into the right column. Sam, on seeing it: not close to the preview, and the dark button does not align with the style. He is right — the canvas after-side is a structure, not a palette.
What
The after-side of the Shell Parity canvas (board 3, the ruling surface) as structure:
AttentionItem.actorName, resolved from the User row when a PG message carries onlyuser_id— every live mention read Someone mentioned you. The row is now headed by who said it.Proof
V2ActivityPage,v2-layout-invariants,translationKeys— 86/86;tsc --noEmitclean.attentionItemService9/9 including the new author-lookup case.🤖 Generated with Claude Code
https://claude.ai/code/session_01UHfcrzjN6MpeuCCAap5Qnb