Skip to content

feat(app): pin sessions to a Pinned section above Recents - #189

Merged
quickbeard merged 1 commit into
mainfrom
pinned-sessions
Aug 11, 2026
Merged

quickbeard merged 1 commit into
mainfrom
pinned-sessions

Conversation

@quickbeard

@quickbeard quickbeard commented Aug 11, 2026 •

Copy link
Copy Markdown
Owner

Pin leads every session's ⋮ / ⋯ menu and reads Unpin once set. Pinned sessions move out of Recents into a Pinned section above it, and the View all list grows a matching Pinned group above Today.

Projects                     Rename                  Pin
  my-project                 Share...                ─────────────
                     →       Archive          →      Rename
Pinned                       ─────────────           Share...
  Fix scroll state           Delete                  Archive
                                                     ─────────────
Recents                                              Delete
  Session B
  View all

Pinned is a second SidebarSection, not a new one

It is the same component Recents renders, passed name="pinned" — the header toggle, chevron, aria-expanded, keyboard operability and 28px folded height all come along unduplicated. Its collapse state slots into the same Persist.global("sidebar.sections") record, which was already keyed by section name for exactly this: no new stored key, no migration, and the two fold independently.

The one addition to the shared component is a grow?: boolean. It hard-coded flex-1 when open, which was right while Recents was the only section but would have had two open sections halve the column. Pinned passes grow={false} and sizes to its rows, scrolling past five, so Recents can never be squeezed out.

Where the pins live

A fork-only context/pinned-sessions.tsx, not state inside createHomeModel(). The sidebar and the home screen each hold their own model, so two persisted stores over one key would write the same storage without sharing memory — a pin in the sidebar would not reach View all until a reload. Same reason the project selection already lives in useLayout().

Ids are keyed by server (session ids are only unique within one), ordered newest-pin-first so a pinned row does not reshuffle when its session is touched, and resolved through the records list — which drops pins whose session was deleted elsewhere and keeps the stored list self-healing. Deleting a session drops the pins of its whole subtree.

The Pinned list is keyed by session id, matching what #185 did for the other two lists: it renders the same rebuilt record wrappers, so a reference-keyed <For> would have given its rows the first-click defect that PR removed.

Tests

Unit tests for orderPinnedRecords / togglePinnedIDs (pin order over recency, unresolvable ids dropped, no duplicates, no mutation), and e2e for: pin moves the row and persists across reload, the label swaps to Unpin and unpinning restores it, Pin leads the menu, Pinned hugs its rows instead of halving the column, the View all group, and both sections folding independently.

All 8 new or changed assertions were run against the pre-change source and fail there, with the pre-existing collapse tests still passing.

Divergences

Four upstream files change — home.tsx (the pinned group), message-timeline.tsx (header menu), layout-new.tsx (provider) and the en/es dicts — under one DIVERGENCES.md row, filed beside the two existing home.tsx rows. Its on-conflict note flags the one thing that would silently break: the header must resolve the pin's server key the same way the sidebar's selection does, or one user's pins split in two.

Rebased onto main

#185, #186 and #188 have since landed. Rebased with four conflicts resolved:

Both earlier caveats are now moot — the clipping guard is on main, and pin/unpin opens on the first click.

Verification (rebased onto #190)

  • bun typecheck (packages/app) — clean
  • bun test (packages/app) — 638 pass
  • bun test:e2e:local — 130 passed, 0 failed. The two remote-session-settings failures every earlier run reported are gone: fix(app): press the settings shortcut the modifier the page expects #190 fixed them, and this branch now sits on top of it.
  • bun lint — 4746 warnings + 1 error, identical to main's baseline

🤖 Generated with Claude Code

@quickbeard
quickbeard force-pushed the pinned-sessions branch 3 times, most recently from f015828 to 66e1efe Compare August 11, 2026 07:01
Pin leads every session's ⋮ / ⋯ menu — sidebar rows, the View all list
and the session header — and reads Unpin once set. Pinned sessions move
out of Recents into a Pinned section above it, and the View all list
grows a matching Pinned group above Today.

Pinned is a second instance of the SidebarSection Recents already uses,
so it folds, persists and reads identically; its collapse state slots
into the same `sidebar.sections` record, which was keyed by section name
for exactly this. The one addition is a `grow` prop: only one open
section may claim the column's free height, or the two halve it. Pinned
sizes to its rows and scrolls past five, so Recents cannot be squeezed
out.

The ids live in a fork-only context rather than in createHomeModel: the
sidebar and the home screen each hold their own model, so two persisted
stores over one key would write the same storage without sharing memory
and a pin would not cross between them until a reload.

Also ships fitsRecentsRow. Drawing "View all" into a list too short to
hold a whole row has always clipped it, but only under a ~380px window;
Pinned takes a fixed 60px off the column, which lifts that into heights
people actually use, so the guard belongs with it.

Every new assertion was run against the pre-change source and fails
there — 8 of them, with the 3 pre-existing collapse tests still passing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@quickbeard
quickbeard merged commit 5c7be21 into main Aug 11, 2026
0 of 5 checks passed
@quickbeard
quickbeard deleted the pinned-sessions branch August 11, 2026 09:51
quickbeard added a commit that referenced this pull request Aug 11, 2026
#189 shipped with the pinned section covered only through the sidebar
row menu. Three behaviours it introduced had nothing behind them.

- The five-row cap: pinning a sixth session now has to leave the list
  capped and scrolling, with all six rows still rendered, and Recents
  still holding its height. PINNED_VISIBLE_ROWS appeared in no test
  before this.
- The in-session header menu: pinning from the ⋯ beside a session title
  has to show up in the sidebar. That is the cross-surface agreement
  DIVERGENCES singles out as the silent breakage — the header resolves
  the pin's server independently, and a mismatch files the pin under a
  key the sidebar never reads.
- Deleting a pinned session has to drop the pin from storage, and pins
  have to be stored under the server they were made against rather than
  in one flat list.

Each was run against a deliberate break of the behaviour it covers and
fails there. The per-server test needed tightening to earn that: the
first version asserted only that some single key existed, which a
hardcoded "all" satisfied, so it now asserts the exact server key.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
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