feat(v2): rebuild workspace thread, decisions, and composer - #1556
Conversation
lilyshen0722
left a comment
There was a problem hiding this comment.
UX-GATE: CHANGES_REQUESTED @ 639a9ea — built and walked at 1440 + 390 beside Workspace · Main, the after-pick frame and Workspace · 390; sheet in Sprint HQ. Passing: rows with no bubbles, composer as one bordered box with mono footer and ink Send at 1440, decision card CSS with the 2px cobalt ring and cobalt primary, old bubble/inspector selectors gone, no UA controls, scroll width clean. Fixes: (1) header — pod name Bricolage Grotesque 22/28; working line as mono 11 cobalt text '● N agents working' (muted at 0), not a tint pill; a separate 28px outline-square inspector toggle at the right at 1440 AND 390 (today .v2-thread__working is the toggle and is display:none at 390, so the phone cannot open the sheet); (2) rows — avatar 28px square r4 (built v2-avatar--md 30px 50%), time mono 12 muted (built Plex Sans 11), tag slot after the time for '· telegram' / '· ruled', hover action bar r4 no shadow (built r8 + shadow); (3) card — 'Other…' as a cobalt text link without the dashed border, drop the muted hint span unless it is a count, options stack full-width 44px at 390, and a ruling unmounts the card leaving a plain human row tagged '· ruled' with the option label as body (built: settled card + status line); (4) composer at 390 — no footer, box + 44px ink square Send with the glyph; (5) 390 frame — no rail, page white edge to edge, four-tile tab bar with the cobalt needs-you badge, and opening the drawer closes the sheet. Note for the page-level cobalt invariant: the rail brand mark is a cobalt fill in the artboard too — exempt .v2-rail__brand-icon. Not walkable on the smoke account: live card, ruled row, telegram tag, working > 0.
lilyshen0722
left a comment
There was a problem hiding this comment.
Code gate at ad64d81837a59b6b82ab793713ba8580d8c0c1c0.
The card rule is in and it discriminates. The page-level invariant ruling 3 asked for is not written, and writing it as ruled fails today on exactly one element.
What passes
the pending human decision is the sole cobalt-filled action exception: one primary choice, bordered alternatives, and a text-only Other action is real. Mutation: change .v2-root button.v2-decision-card__choice--primary from var(--v2-accent) to var(--v2-ink) → 1 red, that test. 92/92 across v2-layout-invariants, V2DecisionCard and V2ThreadDecisionCard at this head.
The retirement is genuine, not a flag: V2PodChat.tsx, V2MessageBubble.tsx and V2PodInspector.tsx are absent from the tree, and :227, :228, :349 assert their absence with fs.existsSync(...) rather than a grep — that is the right instrument for "deleted, not kept behind a flag".
The ruled page-level invariant is missing
63938 ruled: "cobalt fills under .v2-root are exactly the selected room and the primary option of an open decision card, nothing else; the sidebar-scoped rule stays as well." This head has the sidebar-scoped rule and the card rule. It has no rule about the page.
That is not a formality, because the page changed underneath it. Accent backgrounds in v2.css went 29 → 33 across this stack, and on the workspace route the set is now:
| selector | size | fill or mark |
|---|---|---|
.v2-pods__row--selected |
full-width row | fill — ruled |
.v2-root button.v2-decision-card__choice--primary |
button | fill — ruled |
.v2-rail__brand-icon |
28×28 block | fill — unruled |
.v2-workspace-inspector__state--needs-you |
6×6 | mark |
.v2-workspace-inspector__state--working |
6×6 | mark |
.v2-thread-card__dot |
6×6 | mark |
So written literally, the invariant fails today on one element: the rail brand mark. That is the item ux-lead's own section B parked as "artboard decides… flagged for @sam" and it is still unruled — I raised it at 12:58 and nothing has answered it.
The three 6px indicators are marks and correctly stay cobalt, which means the invariant wants a role discriminator rather than an exception list: cobalt as a background on anything larger than a mark (say ≥ 16px in either axis, or simply "not a dot/indicator class") is exactly the selected room and the card's primary choice. Written that way it passes today if the brand mark is ruled ink, and fails loudly if a fourth block appears.
Two things needed before it can be written, both one-liners from a human: is the rail brand mark cobalt or ink, and does "fill" mean any accent background or only a ground-covering block.
DoD 2, for the record
V2Thread.tsx is 50,281 B at this head — 77,673 as V2PodChat, 58,349 at 02daee55, 49,042 at 6bca87dc, now back up 1.2 KB as the 1440/390 corrections landed. V2MessageRow.tsx is 29,759 B. The trend is real but has flattened well above "small"; V2Thread remains the largest component in v2.
Instrument note
My first run of this file reported two failures at :228 and :349. That was my own working tree: git checkout <ref> -- frontend/ adds the PR's files but does not delete files the PR removed, so the three retired components were still on disk and their existsSync assertions correctly said true. Removing them gives 92/92. Worth recording because those two invariants are precisely the ones designed to catch a leftover file, and a reviewer's stale checkout makes them fire on a correct PR.
lilyshen0722
left a comment
There was a problem hiding this comment.
UX-GATE: CHANGES_REQUESTED @ ad64d81 — re-walked at 1440 + 390 beside the Workspace frames (sheet in Sprint HQ). Closed since 639a9ea: separate 28px outline inspector toggle at both widths (the phone opens the sheet again), working line as mono text with the cobalt dot, name 22/28 with the description inline, no rail at 390, white page at 390, four-tile tab bar with the ink active tile, phone composer = box + 44px ink square Send, Other… as cobalt text, ruling unmounts the card and leaves a '· ruled' row. Remaining: (1) .v2-thread__title h1 uses var(--v2-font) — the artboard face is Bricolage, use var(--v2-font-display); (2) row avatars are V2Avatar size=md = 30px circles with a white ring + shadow — 28px square r4, no chrome, as the inspector/sidebar already do; (3) .v2-msg__time is 11px Plex Sans (legacy rule ~v2.css:2980) — mono 12 muted; author 14/600; '· telegram' in the same tag slot as '· ruled' when a message carries a channel source (if no source field exists, say so in the body and it's a follow-up); (4) .v2-msg__actions r8 + shadow (legacy ~v2.css:2864) — r4, 1px border, no shadow; (5) no 760px rule stacks .v2-decision-card__options — full-width 44px options at 390; drop the muted hint span unless it is a count; (6) opening the drawer while the sheet is open leaves both up — each closes the other; (7) the drawer toggle is a borderless glyph — the artboard draws the same 28px outline square as the inspector toggle. Not walkable on smoke: live card, ruled row, telegram tag, working > 0, tab badge.
ad64d81 to
c6c782f
Compare
74e24f9 to
199e905
Compare
c6c782f to
089c990
Compare
lilyshen0722
left a comment
There was a problem hiding this comment.
Joint code gate: #1549 82fa5ef8 · #1555 199e9054 · #1556 089c9902.
The cobalt role allowlist is in and it discriminates. One finding, and it is invisible to both gates as currently defined.
The allowlist is better than what I proposed
I suggested a hand-written selector list. This is stronger: activeAccentBackgroundSelectors derives the active class set from the route's own components (navRail, podsSidebar, thread, threadCard, workspaceInspector, decisionCard, composer, messageRow) and intersects it with the stylesheet, so a class that gains an accent background joins the set automatically instead of hiding in a stale rule. The two generated inspector state classes are added explicitly, with a comment saying why.
It also corrects my count. I enumerated six selectors and missed .v2-pods__channel-dot--live; the real set is seven — two blocks (.v2-pods__row--selected, .v2-root button.v2-decision-card__choice--primary) and five marks. Their enumeration was right and mine was the incomplete one.
Verified rather than read. Baseline 93/93. Mutation: add .v2-composer__send-extra { background: var(--v2-accent) } and put that class on the composer's send button — a realistic eighth fill on a class the route actually uses. Result 1 red, that test. It fails on arrival, which is the property ruling 3 wanted.
Also confirmed the rail geometry survived the resize: --v2-rail-w is still 56px and only .v2-rail__brand-icon went 28 → 32. "The rail is now 32px" in the pod reads like a column change; it isn't one.
Finding: the column tokens inflate as the window narrows
The desktop values were updated to the ruled grid. The breakpoint overrides were not, and they still carry the pre-Signal numbers:
| context | --v2-rail-w |
--v2-pods-w |
--v2-inspector-w |
|---|---|---|---|
| root | 56 | 232 | 300 |
@media (max-width: 1279px) |
72 | 248 | 300 |
@media (max-width: 1023px) |
64 | 236 | — |
So between 1024px and 1279px the rail is 72px — wider than at 1440 — and the sidebar is 248px against the ruled 232. The layout drifts back toward the geometry TASK-129 exists to replace, and it gets wider as the viewport shrinks, which is the opposite of what a responsive step should do.
Nothing catches it. #1549's grid invariant asserts cssVariable(v2, '--v2-rail-w') === '56px', and cssVariable matches the first declaration, so the root value satisfies it while two overrides sit 5,500 lines below. The UX walk is 1440 and 390, and both breakpoints fall between them.
Two lines fix it — delete the three stale declarations, or restate them deliberately if the rail is meant to change at tablet width. The invariant then wants to assert the count of --v2-rail-w declarations, the same shape as the .v2-shell one-head cleanup already agreed for this pass.
Carried
DoD 2: V2Thread.tsx is the number to re-read at the final head; it was 50,281 B at ad64d818, from 77,673 as V2PodChat.
Instrument note: git checkout <ref> -- frontend/ does not delete files the PR removed, so the three retired components must be deleted from the working tree before running — otherwise their existsSync invariants fire on a correct PR. That cost me a false red earlier today and it will cost the next reviewer the same.
lilyshen0722
left a comment
There was a problem hiding this comment.
UX-GATE: CHANGES_REQUESTED @ 089c990 — re-walked at 1440 + 390 (sheet in Sprint HQ). Closed since ad64d81: pod name in Bricolage Grotesque Variable 22/28 (font loads), drawer toggle as a 28px outline square, sheet and drawer mutually exclusive both ways, 760px rule stacks the card options full-width at 44px, cobalt allowlist pinned. Still open — the message rows did not move: (1) V2MessageRow still renders V2Avatar size=md → 30px, border-radius 50%, 2px white ring; the artboard row avatar is 28px square r4 with no chrome (as the inspector now does); (2) .v2-msg__time is still 11px Plex Sans (legacy rule ~v2.css:2983) → mono 12 muted; author .v2-msg__author-btn 13/700 → 14/600; (3) .v2-msg__actions still r8 + shadow (legacy ~v2.css:2867) → r4, 1px border, no shadow. Three CSS/props changes; I re-walk on the next head. Non-blocking: option.description renders as a muted 11px span beside each option — data-driven, fine; '· telegram' tag when a source field exists stays a follow-up unless the row already carries one.
199e905 to
a8f9449
Compare
lilyshen0722
left a comment
There was a problem hiding this comment.
Code re-gate at 43243593. The binding guard works, and I swept for the same failure elsewhere — the blast radius is one.
The guard discriminates
93/93 at this head. Mutation: drop v2-thread from the root's className="v2-chat v2-thread" — the exact bug this commit fixes — gives 1 red, on workspace thread rows retain the artboard grammar at desktop and phone widths. So the binding is pinned, not just the rules.
The interesting part is what the bug says about every other invariant here
This file's guards read v2.css as text. A rule can be present, correct, and reach nothing, and the guard still passes — which is what happened: the .v2-thread rules existed and the element never carried the class, so legacy row styles won and every .v2-thread … assertion stayed green through three UX walks. That is the same shape as the two 2026-08-24 CSS incidents, in a new place.
So I swept for it rather than assuming it was unique. Extracted every descendant selector asserted via ruleBody/selectorRuleBody/lastRuleBody, took the scoping class of each — 13 of them — and checked each against the class names v2 components actually emit.
Ten are literal className attributes. Three looked absent and are not:
v2-activity__queue-row--decision— built by interpolation,V2ActivityPage.tsx:420`…queue-row--${item.kind}…`v2-connector-row__dot--live—V2ConnectorsPage.tsx:486`…dot--${row.dot}…`v2-thread-card--addressed—V2ThreadCard.tsx:80, literal inside a template
All three are genuinely applied; my extractor could not resolve the interpolation. Reporting them as unbound would have been the same false-absence error the sweep exists to catch, so: no other asserted scoping class is unbound at this head. The .v2-thread case was the only one.
The standing weakness, for whoever adds the next invariant
The new guard is hand-written and per-class. Nothing detects the general case, and the general case is cheap to hit: add a scoped rule block, forget the class on the element, and the invariant passes forever while the page renders the old thing.
The mechanical version of what I just did by hand would close it — for every asserted descendant selector, require its scoping class to appear in a component's class output, with interpolated names handled by matching the prefix before ${. Worth considering when the .v2-shell and duplicate-rule cleanup lands, since that pass is already touching this file's helpers.
Carried
V2Thread.tsx size and the five stale breakpoint declarations are unchanged at this head; both are tracked in my earlier reviews.
4324359 to
cc519cc
Compare
lilyshen0722
left a comment
There was a problem hiding this comment.
UX-GATE: CHANGES_REQUESTED @ 4324359 — rows re-walked at 1440 + 1200 + 390 (sheet in Sprint HQ). The three row items are closed in kind: avatar 28px square r4 with no ring or shadow, time in IBM Plex Mono, author 600, hover bar r4 with a 1px border and no shadow (the .v2-thread binding did it). Two left: (1) desktop row meta is one size under the artboard — time mono 11 → 12 muted, author 13 → 14/600 (the phone already renders 12 and 15); (2) the stale breakpoint tokens agreed in 64009/64011 are still present, so at 1200 the grid is 72 / 248 / 516 / 300 instead of 56 / 232 / 776 / 300 — delete --v2-rail-w: 72px; --v2-pods-w: 248px (≤1279) and --v2-rail-w: 64px; --v2-pods-w: 236px (≤1023). Both are CSS-only; I re-stamp on the next head after a 1440 + 1200 + 390 recapture.
a8f9449 to
a6f10d4
Compare
cc519cc to
255c6a0
Compare
a6f10d4 to
b6adfa3
Compare
255c6a0 to
e76651c
Compare
lilyshen0722
left a comment
There was a problem hiding this comment.
UX-GATE: CHANGES_REQUESTED @ 255c6a0 — one line left. Re-walked at 1440, 1200, 1024 and 390 (sheet in Sprint HQ): grid fixed at 56 / 232 / 300 down to 1024 with the chat absorbing the width; rows are 28px square r4 avatars with no ring, mono time, 600 author, r4 bordered hover bar with no shadow; everything else unchanged from 089c990 and matching. Still under the artboard on desktop only: .v2-msg__time mono 11 → 12 muted, .v2-msg__author-btn 13/600 → 14/600 (the phone already renders 12 and 15). That is the last visible delta I have; I re-stamp APPROVED on the head that carries it.
lilyshen0722
left a comment
There was a problem hiding this comment.
Code gate at 7ef104e3 — note this is #1556's head as GitHub reports it now; e76651c0 in the pod message is already superseded, the fourth sha drift in this stack today. Content is what was described, so this is a gate on the right thing under a different name.
The guard now catches the case that defeated its predecessor.
--v2-rail-w, --v2-pods-w and --v2-inspector-w are each declared exactly once. Baseline 94/94.
Mutation, the one that mattered: re-add --v2-rail-w: 72px; --v2-pods-w: 248px inside @media (max-width: 940px) — outside the old 29-line slice, which is where the previous guard let it through green. Result 1 red, on the workspace keeps each artboard column width declared exactly once. The guard is now positionless and catches a re-add anywhere in the file.
One consequence of the routing, stated once
The deletion and the guard now live in #1556 only. #1549 stays at 82fa5ef8, which carries the 56/232 roots and the five stale overrides. That is a deliberate choice and it preserves ux-lead's approvals, which is what I suggested — but it makes merge order load-bearing rather than cosmetic:
- #1549 alone on main = the inversion I reported, live.
- #1549 + #1555 = same, since
b6adfa35also carries 8 declarations. - All three = correct.
Under the ruled press — three within an hour, one deploy — that is fine. It stops being fine if #1556 is held for any reason after the first two land, and #1556 is the PR most likely to be held, since it is the one still taking UX rounds.
Cheapest insurance if that risk is unwelcome: cherry-pick the five deletions into #1549 as well. They are idempotent with the guard, and #1556 would then be a no-op for that file. Not asking for it — flagging it so the order is chosen rather than assumed.
Standing items, unchanged
V2Thread.tsx against DoD 2's "small components"; the .v2-shell duplicate rule heads and the two-accessor situation in the invariants file; and the mechanical class-binding sweep that would generalise the .v2-thread fix. All three are follow-ups, none blocks this head.
lilyshen0722
left a comment
There was a problem hiding this comment.
UX-GATE: APPROVED @ 7ef104e — built the stack top and walked at 1440, 1200 and 390 (sheet in Sprint HQ). The last item is closed: desktop row meta now author 14/600 and time IBM Plex Mono 12 muted (phone 15 / 12). Grid 56 / 232 / 788 / 300 at 1440 and 56 / 232 / 548 / 300 at 1200 with the stale tablet tokens deleted here; rows 28px square r4 avatars with no ring, r4 bordered hover bar; Bricolage name 22/28 with inline description; mono working line with the cobalt dot; 28px outline toggles at both widths; no rail, white page, four-tile tab bar and box + 44px ink square Send at 390; sheet and drawer exclusive; cobalt fills = brand + selected room (+ card primary when open). Not walkable on the smoke account: live decision card, ruled row, telegram tag, working > 0, tab badge — Sam's whole-workspace look needs Sharpen. Clear to press with #1549 82fa5ef and #1555 b6adfa3 within the hour.
lilyshen0722
left a comment
There was a problem hiding this comment.
UX-GATE: APPROVED @ b7dd542 — diff read against the walked 7ef104e (lily-shen's three pre-press deltas, no new walk per 64030). (1) Phone: inspectorCollapsed initialises true under (max-width: 760px) and a matchMedia change listener re-collapses it, so the sheet starts closed and opens only from the control. (2) Inspector label: shortRoomName() takes the leading segment before · — – : |, lowercased; .v2-workspace-inspector__label is one line (nowrap, overflow hidden, text-overflow clip) in mono 11. (3) Working dot: ::before muted by default, cobalt only with .v2-thread__working--active when onlineAgentCount > 0; guards added for all three. One non-blocking note: at ≤760 the mobile working TEXT stays cobalt (.v2-thread__working--mobile color: var(--v2-accent-text)) while the dot goes grey at 0 — if you want the phone line fully quiet at zero, move that colour under --active too. Stack stands approved: #1549 82fa5ef, #1555 b6adfa3, #1556 b7dd542.
lilyshen0722
left a comment
There was a problem hiding this comment.
Code gate at b7dd5429. Both behavioural guards discriminate. One observation about the phone fix that no test covers.
Baseline 114/114 across v2-layout-invariants, V2LayoutSelection, V2Inspector and V2ThreadDeliveryHint.
Delta 3 — the working dot
Mutation: force workingClass to always be ' v2-thread__working--active', i.e. cobalt at zero agents, the exact defect. 2 red — the thread header is the artboard's name, muted description, and working count and V2Composer send button › uses the artboard's compact working count in the thread header. Two independent readers, one on the stylesheet and one on the rendered header. Good.
Delta 1 — the phone sheet, and a redundancy worth knowing about
a phone starts with its inspector sheet closed even when desktop left it open is real, but it took two mutations to characterise:
- Revert only the lazy initializer (
isPhoneViewport() ? true : readInspectorCollapsed()) → 7 passed. - Revert the initializer and the
closeInspectorOnPhone()effect → 1 red, that test.
So the property is enforced twice and the test pins the property, not either mechanism. That is not a vacuous guard — after the first mutation the behaviour is still correct, so green is the right answer.
The gap is what the two mechanisms do differently. The effect closes the sheet after first render; the initializer prevents it opening at all. With only the effect, a phone entering the route with v2.inspectorCollapsed = '0' renders one frame with the sheet up, then it snaps closed. The test cannot see that — RTL asserts the settled DOM — so if someone later removes the initializer as "redundant", the suite stays green and the flash returns.
Same shape as the v2 dark-flash and the login-payload badge: correct end state, wrong first frame, invisible to the tier that guards it. Not asking for a fix — the initializer is there and correct. Worth a one-line comment on it saying it exists to prevent the first-frame flash and is not redundant with the effect, since that is the note that stops the next reader deleting it.
Delta 2
uses the short lowercase room name in the single-line agents label is a straightforward render assertion and covered.
Confirmation requested
No invariant moved: the stylesheet guards are unchanged in count and content apart from the two additions above, and the token, cobalt-allowlist and class-binding guards all still pass at this head.
b6adfa3 to
bcb58d7
Compare
1d9b274 to
f8721f9
Compare
lilyshen0722
left a comment
There was a problem hiding this comment.
UX-GATE: APPROVED @ f8721f9 — restack of the approved 1d9b274 onto #1555 bcb58d7: the PR's non-test patch against its base is line-identical to the approved one (the sidebar-attention conflict resolved to the same lines). Thread / card / composer walk from 7ef104e and the b7dd542 diff-read stand. Stack clear: #1549 43e237c · #1555 bcb58d7 · #1556 f8721f9.
f8721f9 to
fe5358f
Compare
lilyshen0722
left a comment
There was a problem hiding this comment.
UX-GATE: APPROVED @ fe5358f — carried from f8721f9: rebased onto main b0b4759 (#1555 merged); the PR's patch against its base is line-identical to the approved one, both with tests excluded (nothing that renders changed) and with all files included (nothing at all changed). Clear to press 3; I walk commonly.me live at 1440 + 390 after the single Deploy Dev.
TASK-129 PR 3 — stacked on #1555 / #1549
Replaces the legacy workspace chat and bubble route with the thread artboard surface.
Verification
UX 1440/390 walk remains required. This is draft pending that review and the stacked cohesive cutover.