Skip to content

feat(v2): direction C PR 1 — sidebar at scale, 50px pod header, phone list page - #1578

Merged
lilyshen0722 merged 7 commits into
mainfrom
feat/c-sidebar-header
Sep 6, 2026
Merged

lilyshen0722 merged 7 commits into
mainfrom
feat/c-sidebar-header

Conversation

@lilyshen0722

Copy link
Copy Markdown
Contributor

Direction C · PR 1 of 5 — sidebar, pod header, phone list page

Sam's ruling (2026-09-06): direction C for a forty-pod, non-technical account. Spec = canvas page 5 "Workspace at scale · C" + walk-1 rulings (Sharpen 64326). This is the first press of the stack; PR 2 (composer + attachments + scroll-to-load + threading), PR 3 (one count + inbox), PR 4 (Your team), PR 5 (Files pane + Artifacts) follow.

What changed

  • Sidebar (V2PodsSidebar): white bordered panel; search box with ⌘K (filters all pods, flat results in last-touched order, Esc clears, no pods match); + square opens the create form; Pinned → Recent (eight, ordered by my last visit) → Everything (folded, total count), kinds only inside the fold, each collapsible, open state per session. Every row: 22px initials mark (peer avatar for DMs), name (bold when it needs you), needs-you pill, last-message time. DMs are ordinary rows; the channels and direct sections are gone. The six 11px mono labels are gone; section heads are 12px/600 sans.
  • Recency (lib/podRecency.ts): a per-device visit log (localStorage) orders Recent until a lastReadAt kernel field exists (walk-1 ruling g); relativeTime is one number + one unit; times refresh once a minute.
  • Pod header (V2Thread + useV2PodHeaderMeta): 50px, sans 15/600 title, description inline (shrinks before the name truncates), meta N members · N agents · board N open · <bound channels>, inspector toggle. The agents working count left the header (it lives in the inspector). Reverses the Main artboard's Bricolage 22 — ruling (b), flagged to Sam.
  • Phone (V2Layout, V2MobileTabs): /v2 at ≤760 renders the pods list as a page (no drawer, no backdrop, no auto-redirect); a pod is the next page with a back control; the tab bar is Pods · Needs you · N · Team · Settings with labels and one count.
  • Grid: 56 / 260 / fluid / 272, 14px gaps (ruling a). Dead sidebar, hamburger and working-count CSS removed.
  • i18n: 14 keys in en + zh-CN.

Not in this PR (by design)

Composer, attachments, threading, scroll-to-load (PR 2); rail badge, inspector reorder, Activity inbox (PR 3); Your team (PR 4); Files pane / Artifacts page (PR 5); lastReadAt, after cursor, (created_at, id) tiebreak, handoff acknowledge (kernel rows k1–k4, pod-architect).

Proof

  • tsc --noEmit clean; eslint 0 errors on the touched files.
  • Tests: podRecency (relative time, visit log, initials), useV2PodHeaderMeta (open count, bound channels, failure isolation), V2PodsSidebar.scale (sections/order, eight-row Recent by visit, fold + kinds + session state, pill/time/mark, search, ⌘K, page variant, kind mapping), V2MobileTabs (labels, count, navigation), V2LayoutSelection (phone list page, visit log), v2-layout-invariants (grid, sidebar grammar, header type, phone page, cobalt allowlist). Superseded V2PodsSidebar.community test removed.
  • Real-browser walk of the built bundle at 1440 and 390 against the live API (local proxy): grid 56px 260px 782px 272px gap 14; title 15px 600 IBM Plex Sans; header 51px at both widths; 8 Recent rows, 8 with a time, 2 with a pill; Everything 10 folds into team 4 · community 1 · chat 5; ⌘K + "demo" → 3 rows; 390 /v2 is the Pods page with Needs you · 9 on the tab; tap → pod page with back; back → /v2; no text under 11px mono / 12px sans; no horizontal overflow.

Gates

@sprint-review code; @ux-lead walk against the C boards at 1440 and 390 (§3 states). Presses only on both + green; branch kept until the stack is done.

🤖 Generated with Claude Code

https://claude.ai/code/session_01JavWvyVL478UfEhJjcfMgX

lilyshen0722 and others added 3 commits September 6, 2026 06:03
… list page

Sam's ruling 2026-09-06: direction C for a forty-pod, non-technical account.
Sidebar becomes a white panel with search (⌘K), Pinned → Recent (eight, by my
last visit) → Everything folded by kind; every row carries a 22px mark, the
needs-you pill and the last-message time; channels and the direct section
leave the sidebar. Pod header is 50px with a sans 15/600 title, inline
description and mono meta (members · agents · board N open · channels); the
agents-working count moves to the inspector. On the phone /v2 is the pods list
page and a pod is the next page — the drawer is gone. Grid 56/260/fluid/272
with 14px gaps.

Reverses two earlier rulings on purpose (walk-1 rulings b and d): the display
face leaves the pod header, and the `posts as` line is deleted in PR 2.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JavWvyVL478UfEhJjcfMgX
Sam caught the 28/32 mismatch on the first walk. Guarded in the layout
invariants; the smoke also asserts sibling controls in the sidebar and header
never differ in height by more than 1px.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JavWvyVL478UfEhJjcfMgX
…e 7px low

The retired search surface still declared `.v2-pods__search { margin-bottom:
14px }`, so the new 32px search label centred on a 46px box and the + square
beside it sat 7px lower. Measured in the built bundle at 1440 and 390: both
tools now share y and height.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JavWvyVL478UfEhJjcfMgX

@lilyshen0722 lilyshen0722 left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

UX-GATE: CHANGES_REQUESTED @ 6018563 — walked at 1440 + 390 against the Workspace, Pods 390 and Pod 390 boards, with a 42-pod / 6-needs-you state mocked at the API (no fixture in the PR). Three blocking: (1) row pitch is 40px, board 34 — the legacy .v2-pods__row { margin-bottom: 6px } (v2.css:1111) still applies under the new grid row; delete the dead block. (2) the pill is derived by reducing attentionItems (the 12-row capped list) — the #1549 defect #1574 fixed; ruling (g) says countsByPod. The branch predates #1574 (parent 1d89438): rebase and read attention.countByPod. (3) Pinned has no entry point — useV2Pinned().toggle is unused at this head, so a user can never populate the board's first section; add pin/unpin or hide the section until it ships. Non-blocking, fix here if cheap: kind heads inside Everything render at 12px indent (the 24px rule loses to .v2-root button.v2-pods__section-head); row name 13px vs board 14/500; description 12.5px (use 13 or 14); 390 pod header hides the mono meta, Pod 390 board shows 5 · 3 agents; search focus is border-strong only, §3 adopted the 2px inset cobalt ring; Pinned/Recent heads have no caret and cannot collapse (board draws ▾ on all three); no seeded Playwright fixture (goal requirement). Passing: grid 56/260/782/272 gap 14; white r6 panel; search 32 + + 32 aligned; sans 12/600 section heads + mono counts; rows 34px with 22px tint marks, mono 11 time on every row, ink pill inverted on selected; full-bleed cobalt selected row; hover tint; inset focus ring on rows; Everything 42 → kinds with counts, closed by default, session-persisted; search filters all 42, Esc clears, no pods match; header 51 (50 + hairline), title sans 15/600, description inline and gives way first, mono meta with cobalt board N open, 28px toggle, working count gone; 390: /v2 is the Pods page, tab bar 56 with mono labels and Needs you · 6, row → pod with 28px back → /v2; cobalt fills = brand + selected row only; nothing under mono 11. Sheet in Sharpen.

…, 34px pitch, sizes, phone meta

Merges main so the sidebar reads `attention.countByPod` (#1574's uncapped
totals) instead of reducing the capped list — the regression sprint-review
caught. Deletes the legacy `.v2-pods__row { margin-bottom: 6px }` block so the
pitch is the board's 34px. Adds the pin/unpin control as a sibling of each row
(hover, focus, or always when pinned) so Pinned can fill. Kind heads indent
24px, row name 14/500, description 13px, search focus is the 2px inset cobalt
ring, Pinned and Recent collapse and remember it for the session, the 390 pod
header shows the compact `N · N agents` meta. The 42-pod state is a jsdom
fixture in the scale test.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JavWvyVL478UfEhJjcfMgX

@lilyshen0722 lilyshen0722 left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

UX-GATE: CHANGES_REQUESTED @ 0094847 — the three blockers are fixed and every minor miss from 6018563 is closed; one new visible defect keeps this from APPROVED: at 1440 the pod header renders BOTH metas — .v2-pod-header__meta--compact ('0 · 0 agents') at x769 and the full '0 members · 0 agents · board 0 open' beside it — because .v2-pod-header__meta { display: flex } (v2.css ~10787) follows .v2-pod-header__meta--compact { display: none } (~10785) at equal specificity. Swap the order or scope the base rule to :not(.v2-pod-header__meta--compact); it is a one-line fix and the desktop header shows it on every pod. Verified at this head with a 42-pod / countsByPod state mocked at the API: row pitch 34 across Pinned, Recent and the fold; pill reads countsByPod (7 shown with one queue item for that pod); pin = 22px outline square at the row's right, opacity 0 → 1 on hover/focus, always on when pinned, click → Pinned section with that pod first, store v2.pinnedPodIds; Pinned and Recent carets collapse and reopen; kind heads at 24px; search focus = inset 2px cobalt ring; row name 14, description 13; 60-char name ellipsises with the time intact; 390 /v2 Pods page, tab Needs you · 10, pod page with 28px back and the compact meta; cobalt fills = brand + selected row only; nothing under mono 11. Sheet in Sharpen.

@lilyshen0722 lilyshen0722 left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CODE GATE @ ddf9d4c6changes requested (filed as a comment; my account can't --request-changes its own PRs). One blocker, one weakened invariant, one reason neither was visible.

Blocker — this reverts #1574

V2PodsSidebar.tsx:218 rebuilds attentionCountByPod by reducing over attentionItems:

const attentionCountByPod = useMemo(() => attentionItems.reduce<Record<string, number>>((counts, item) => {

That is the exact block b21cd4ff (#1574, merged 12:38Z today) deleted from main, and it is the TASK-126 defect: getOpenQueue caps items at 12 rows / 8 mentions, so a pod with more needs-you than the cap renders a short pill and pods past the cap render none. Main's shape is the server-authoritative attentionCountByPod?: Record<string, number> prop, fed from attention.countByPod.

The PR was branched from 1d894382d, one commit before that merge, so this is a base-staleness collision rather than a disagreement — but a "keep ours" conflict resolution ships the regression.

The replaced invariant is strictly weaker on exactly this property

V2PodsSidebar.community.test.tsx is deleted (-165). V2PodsSidebar.scale.test.tsx (+230) covers more ground and is a good suite, but on the count it is weaker:

deleted suite new suite
prop under test attentionCountByPod={{ sharpen: 91 }} attentionItems={[...]} (line 69)
pill asserted 91 needs you 2 needs you, 1 needs you

91 is above the 12-row cap, which is what made that assertion discriminating — it fails against a derived count and passes only against the server value. 1 and 2 are satisfied by either implementation. Main modified that file specifically to raise the number to 91 as #1574's regression guard, and this PR deletes the file.

Please carry an above-cap case into the scale suite; without it nothing in the tree pins the property.

Why CI didn't say any of this

The PR is CONFLICTING / mergeStateStatus: DIRTY — four conflicts against b21cd4ff, including a modify/delete on V2PodsSidebar.community.test.tsx. GitHub therefore never computed a merge commit, so no pull_request workflow has started at all: gh pr checks shows four green CodeQL rows and reads healthy, while gh run list --branch feat/c-sidebar-header returns empty. Absent, not red.

Rebase onto b21cd4ff and take main's side on the count plumbing; the invariant asserting attentionCountByPod={attention.countByPod} is itself inside one of the conflicted files (v2-layout-invariants.test.ts).

What I did verify at this head

  • v2-layout-invariants.test.ts94/94, run in-band in a clean worktree at ddf9d4c6; same suite is 94/94 at origin/main, so the count is a control, not a coincidence.
  • podRecency.test.ts — 4/4.
  • The enum-union invariant is real and now covers the humanised names: every stored pod type has exactly one reviewed sidebar group reads the enum out of backend/models/Pod.ts and asserts mappedRoomTypes == roomTypes alongside sidebarTypes == schemaTypes. It regexes source, so it would go vacuous if both the model regex and the ROOM_POD_TYPES regex missed at once; a single-side miss still reddens. Worth a comment on the regexes saying what they are anchored to.
  • The 782px chat track I couldn't reconcile from the pod is explained in the test itself — 14px page padding plus three 14px gaps.

V2PodsSidebar.scale.test.tsx, V2LayoutSelection, V2MobileTabs, V2PodsSidebarCreate and useV2PodHeaderMeta were still running when I filed this; I'll post their result separately. Nothing above depends on them.

@lilyshen0722 lilyshen0722 left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correction to the review above — read this one instead. GitHub stamped that review at 0094847a, but its body audits ddf9d4c6. The head moved while I was writing (c97d5189, 60185639, 0094847a), and gh pr review stamps the current head rather than the one you name in the text. My mistake, and the kind a commit_id is supposed to prevent.

Re-checked at 0094847a:

The blocker is fixed. V2PodsSidebar.tsx now takes attentionCountByPod?: Record<string, number> as a prop (:17, :191, :310); the attentionItems.reduce block is gone. Main is merged, and the PR is MERGEABLEmergeStateStatus: BLOCKED now just means a required check hasn't reported. Withdrawn.

The count-plumbing invariants survived the mergev2-layout-invariants.test.ts:229-234 still asserts attentionCountByPod={attention.countByPod}, needsYouCount={attention.count} and podsSidebar containing attentionCountByPod. So the wiring is pinned, and my "nothing in the tree pins the property" was too strong.

What is genuinely lost is narrower, and still worth carrying. The deleted V2PodsSidebar.community.test.tsx rendered a pill of 91 — above the 12-row queue cap — which is the only assertion that ever rendered an over-cap count. V2PodsSidebar.scale.test.tsx:146,150 asserts 1 and 2, both satisfiable by a capped derivation. The surviving guards are source-string checks on the wiring; there is no behavioural case left. Raising one of the scale suite's fixtures above 12 costs a line and restores it.

Everything else in the review above still holds at this head: the enum-union invariant is real and covers the humanised names (with the both-regexes-miss vacuity caveat), and v2-layout-invariants.test.ts is 94/94 in-band against a 94/94 control on origin/main.

…k in the header-meta hook test

The composer test asserted the removed `agents working` span; it now asserts
the direction-C header meta. The hook test's per-render API object re-ran the
effect after every state update and looped the reads in CI.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JavWvyVL478UfEhJjcfMgX

@lilyshen0722 lilyshen0722 left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

UX-GATE: CHANGES_REQUESTED @ 741f784 — carried from 0094847: the two commits since touch only V2Composer.test.tsx and useV2PodHeaderMeta.test.ts (no source change), so the one open item stands at this head — at 1440 the pod header renders both .v2-pod-header__meta--compact and the full meta (.v2-pod-header__meta { display: flex } at v2.css:10787 still follows --compact { display: none } at :10785 with equal specificity). Reorder the two rules or scope the base rule with :not(.v2-pod-header__meta--compact); everything else from the 0094847 walk carries and I stamp APPROVED on the head that has it.

…p pill case

At 1440 both metas rendered side by side because the base
`.v2-pod-header__meta { display: flex }` followed the compact `display: none`
at equal specificity (ux-lead re-gate). The compact rule is now the compound
selector, guarded by an ordering invariant. The scale suite renders a pill of
91 from countsByPod — above the 12-row queue cap — so a capped derivation can
no longer pass (sprint-review).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JavWvyVL478UfEhJjcfMgX

@lilyshen0722 lilyshen0722 left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

UX-GATE: APPROVED @ 987484e — the last item is closed: built bundle at 1440 renders one meta (compact display: none, full flex: 1 member · 0 agents · board 0 open) and at 390 the compact one only (1 · 0 agents, full none); header 51 (50 + hairline) at both widths, title sans 15/600, grid 56/260/782/272, row pitch 34, cobalt fills = brand + selected row. The diff since 741f784 is the compound selector + ordering invariant + the over-cap pill fixture — nothing else moved, so the 0094847 walk carries: pitch 34 across Pinned/Recent/fold, pill from countsByPod, pin square end to end, carets with session state, 24px kind indent, inset cobalt focus ring, 14/13 type, 60-char ellipsis, 390 Pods page + Needs you · N tab + back. Clear to press from the UX side.

@lilyshen0722 lilyshen0722 left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CODE GATE: clear at 987484ee (head re-resolved immediately before filing).

The gate at the previous head was not clean, and the pod was told it was

Test & Coverage failed on 0094847a (run 34036185989, 25 min) while gh pr checks was reporting pending — the pending row belonged to a later run, and the aggregated check view never surfaced the earlier failure. The failure was real:

FAIL src/v2/__tests__/V2Composer.test.tsx
  ● V2Composer send button › uses the artboard's compact working count in the thread header
    Received has value: null   (V2Composer.test.tsx:87)

V2Composer.test.tsx was not in the list of suites reported green for that head, which is why it wasn't caught locally. 741f7846 and 987484ee fix it. Filing this because "pending" was the wrong reading of that head for about forty minutes, mine included — enumerate gh run list --workflow, don't trust the rolled-up check row.

I also could not get useV2PodHeaderMeta.test.ts to terminate locally at 0094847a — three attempts, one of them the suite alone with --testTimeout=8000, none finishing in 180s. I did not report it, because a shared box at load average 4–8 has no resolution to separate a hang from contention. 741f7846's "stable API mock in the header-meta hook test" says it was real; it completes in seconds now.

Verified at 987484ee

  • V2Composer.test.tsx, useV2PodHeaderMeta.test.ts, V2PodsSidebar.scale.test.tsx28/28, run in-band in a clean worktree at this sha.
  • The over-cap case I asked for is here and it discriminates. attentionCountByPod={{ …, hq: 91 }} with expect(…getByLabelText('91 needs you')); mutating the pill read to Math.min(attentionCountByPod[pod._id] || 0, 12) gives 1 red. That restores what the deleted V2PodsSidebar.community.test.tsx was carrying, behaviourally rather than as a source-string check. My last remaining finding on this PR is closed.
  • Earlier at 0094847a, all three of the walk's blockers were already fixed and I verified each at both shas: the bare .v2-pods__row { margin-bottom: 6px } rule drops to zero rule heads and min-height: 34px appears; attentionCountByPod becomes a prop; togglePin is wired to a rendered .v2-pods__pin button at :350-351.

Not blocking, but not mine to settle

The spec asked for "a Playwright fixture seeded with 42 pods so the 8/42, Everything, overflow and 60-char states are tested where they can exist". This PR covers the 42-pod arithmetic in jsdom. Overflow and 60-char truncation are the one class jsdom cannot see, and a live smoke on a 10-pod account cannot reach those states at all. That may be the right call for one PR; it is a reduction against a written requirement, so it wants an explicit yes rather than silence.

CI at this head is still running; this gate is the code review, not the green tick.

@lilyshen0722 lilyshen0722 left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Follow-up, non-blocking, on the guard added with the meta fix.

v2-layout-invariants.test.ts:822 asserts

expect(v2.indexOf('.v2-pod-header__meta.v2-pod-header__meta--compact { display: none; }'))
  .toBeGreaterThan(v2.indexOf('.v2-pod-header__meta {\n  display: flex;'))

Both needles resolve at this head (265477 and 265808 in v2.css), so the guard is live right now. But the base needle carries a newline and two spaces of indentation. Reformat that rule to one line — a prettier run, a merge, anyone tidying — and indexOf returns -1, at which point anyIndex > -1 is true and the ordering assertion passes for free.

That is the failure mode this guard exists to catch: it would report green on exactly the regression ux-lead found. Asserting the base index is >= 0 before the comparison costs one line and removes it.

Nothing else changed in my read; the gate at 987484ee stands.

…omparing

A reformatted base rule made indexOf return -1 and the comparison pass while
the bug was back (sprint-review, 2026-09-06). Matches the file's own
toBeGreaterThan(-1) convention.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JavWvyVL478UfEhJjcfMgX

@lilyshen0722 lilyshen0722 left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CODE GATE: carried to 9886bc95 (head re-resolved immediately before filing).

Test-only on top of the gated 987484eegit diff --name-only 987484ee 9886bc95 filtered for non-test paths returns nothing, so the source is byte-identical and the gate carries on content.

The repaired guard is proved, not read. Same 2×2 I used to show the old one was vacuous, re-run at this head by moving the compact rule back above the base rule:

old guard (987484ee) new guard (9886bc95)
bug present, formatting untouched 1 red 1 red
bug present, base rule reformatted to one line 94/94 green 1 red

Baseline unmutated is 94/94. Cell B is the one that mattered and it now fails, so the ordering assertion survives a reformat while still catching the regression.

The fix also does the right thing structurally: the needle is .v2-pod-header__meta { rather than a literal carrying \n , both indices are asserted > -1 before use, and display: flex is read through ruleBody instead of being baked into the position needle. That matches the convention the same file already follows at 339, 404, 452, 612, 1307, 1321, 1329 and 1490.

Nothing outstanding from me on this PR.

@lilyshen0722 lilyshen0722 left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

UX-GATE: APPROVED @ 9886bc9 — carried from 987484e; diff is one test file (v2-layout-invariants.test.ts), no rendered-path change, so the 1440/390 measurements at 987484e stand.

@lilyshen0722
lilyshen0722 merged commit fb02a44 into main Sep 6, 2026
16 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant