Skip to content

fix(app): open session row menus on the first click, and stop clipping View all - #185

Merged
quickbeard merged 2 commits into
mainfrom
sidebar-menu-fixes
Aug 11, 2026
Merged

quickbeard merged 2 commits into
mainfrom
sidebar-menu-fixes

Conversation

@quickbeard

@quickbeard quickbeard commented Aug 11, 2026 •

Copy link
Copy Markdown
Owner

The two defects the sidebar tests turned up, fixed on every surface that has them.

1. Session row ⋮ menus ignored their first click

Root cause, traced with a DOM-mutation and pointer-event trace. createHomeModel's records memo mints a fresh {session, project, projectName} wrapper per session every time it runs, and it runs on recomputes that changed nothing it reads — the projects memo alone hands back a new array on a pointerdown, three times over. The lists rendered those wrappers through a reference-keyed <For>, so every row's DOM was destroyed and rebuilt between pointerdown and mouseup. With no surviving common ancestor the browser fires no click event at all — the trace shows pointerdown on the icon, the row subtree removed and re-added, then mouseup on a different node and no click anywhere. The second click landed because the rebuild was over.

Fix: iterate stable ids, not object references, so a rebuilt record set updates rows in place.

  • Sidebar Recents (app-sidebar.tsx, fork-only) — keyed by session id.
  • View all (home.tsx, upstream) — keyed at both levels: groups() by group id, and each group's sessions by session id. groupSessions mints new group objects too, so keying only the inner list would still have rebuilt everything.

This was never only about the first click. The same rebuild could close an already-open menu, or drop a rename part-way through typing, whenever anything refreshed in the background.

The ⋯ menu beside a session title was never affected — it has no such list above it.

2. View all was drawn into space too small to hold it

Below roughly a 380px-tall window the sidebar's flexible area drops under one row (12px at 380, none at 360). recentsRowCapacity clamps the session rows to zero, but nothing withheld the View all row, and the list clips with overflow-hidden — so it was drawn cut in half, or invisibly. New fitsRecentsRow gates it on one whole row of space: whole or absent, never cut.

Tests

Every new or extended assertion was run against the unfixed code and fails there — checked, not assumed:

  • sidebar-session-menu-first-click.spec.ts and home-session-menu-first-click.spec.ts — one click opens the menu; and a forced record rebuild leaves both the open menu and the row's DOM node identity intact.
  • The height sweep in sidebar-section-collapse.spec.ts now runs past the old 400px floor down to 320.
  • fitsRecentsRow unit tests, including a cross-check that it never disagrees with recentsRowCapacity.

Divergences

home.tsx is upstream, so it gets a DIVERGENCES.md row. The on-conflict note spells out that reverting to <For each={groups()}> / <For each={group.sessions}> reintroduces the bug, and points at the regression spec to use if upstream ever makes those references stable.

One approach I tried and dropped: a stable-identity cache in the fork-only home-model.tsx would have fixed both surfaces with no upstream edit, but it does not fix the View all list — those rows come from groupSessions' own arrays — so I reverted it rather than leave unproven machinery in place.

Verification

  • bun typecheck (packages/app) — clean
  • bun test (packages/app) — 626 pass (up 7)
  • bun test:e2e:local — 101 pass (up 4), 2 fail in remote-session-settings.spec.ts; both fail identically on clean main, pre-existing and unrelated
  • bun lint — identical to the main baseline (4740 warnings, 1 pre-existing error)

🤖 Generated with Claude Code

quickbeard and others added 2 commits August 11, 2026 11:21
…ping View all

Two defects found while writing the sidebar tests.

The row menu ignored its first click. `createHomeModel`'s records memo
mints a fresh `{session, project}` wrapper per session on every recompute
— the projects memo alone returns a new array on a pointerdown — and the
list rendered those through a reference-keyed `<For>`, so the row was
torn down and rebuilt mid-gesture. With no surviving common ancestor the
browser fires no `click` at all, so the gesture vanished and only a
second click landed. Iterating session ids instead keeps the row DOM
across a rebuild and feeds it the new record reactively, which also stops
a background refresh from closing an open menu or dropping an in-progress
rename.

Below roughly a 380px window the flexible area falls under one row, and
"View all" was drawn into it anyway and clipped by overflow-hidden.
`fitsRecentsRow` withholds it at those sizes: whole or absent, never cut.

Both regressions are pinned. The three new or extended assertions were
each run against the unfixed code and fail there.

The same first-click defect remains on the home "View all" list, whose
rows live in upstream's home.tsx. A stable-identity cache in the
fork-only home-model would have covered both, but it does not fix that
list — its rows come from different arrays — so it is not included here.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…irst click

Carries the sidebar fix to the other surface. `groupSessions` mints new
group objects on every call and the records memo mints new record
wrappers, so both `<For>` levels on the View all screen were keyed on
references that churned — tearing down the rows on recomputes that
changed nothing, including between a pointerdown and its mouseup, which
is why the ⋮ menu there ignored its first click too.

Both levels now iterate ids. DIVERGENCES gains a row: home.tsx is
upstream, and the on-conflict note spells out that reverting to
`<For each={groups()}>` reintroduces the bug.

Both new assertions were run against the unfixed home.tsx and fail there.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@quickbeard quickbeard changed the title fix(app): open the sidebar row menu on the first click, and stop clipping View all fix(app): open session row menus on the first click, and stop clipping View all Aug 11, 2026
quickbeard pushed a commit that referenced this pull request Aug 11, 2026
Both specs carried a detour around a defect, each with a comment explaining
why. #185 fixes both, so the detours are now dead weight and the comments are
wrong.

- session-actions-menu: openRowMenu clicked a second time when the first was
  swallowed. One click now, and the helper no longer needs the page.
- sidebar-persistence: "marks the row whose session is already open" reloaded
  before asserting data-selected. It asserts on the click that opened the
  session.

The hover in openRowMenu stays. That one is not a workaround: the trigger is
pointer-events-none until its row is hovered, and Playwright hit-tests before
it moves the mouse.

Checked both ways rather than assumed. On #185's branch the three specs pass
20/20; on main without it, both edited tests fail — the menu never reports
aria-expanded=true, and no row is ever marked selected. So this must merge
after #185.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@quickbeard
quickbeard merged commit 424a45a into main Aug 11, 2026
0 of 5 checks passed
@quickbeard
quickbeard deleted the sidebar-menu-fixes branch August 11, 2026 05:22
quickbeard pushed a commit that referenced this pull request Aug 11, 2026
Both specs carried a detour around a defect, each with a comment explaining
why. #185 fixes both, so the detours are now dead weight and the comments are
wrong.

- session-actions-menu: openRowMenu clicked a second time when the first was
  swallowed. One click now, and the helper no longer needs the page.
- sidebar-persistence: "marks the row whose session is already open" reloaded
  before asserting data-selected. It asserts on the click that opened the
  session.

The hover in openRowMenu stays. That one is not a workaround: the trigger is
pointer-events-none until its row is hovered, and Playwright hit-tests before
it moves the mouse.

Checked both ways rather than assumed. On #185's branch the three specs pass
20/20; on main without it, both edited tests fail — the menu never reports
aria-expanded=true, and no row is ever marked selected. So this must merge
after #185.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
quickbeard added a commit that referenced this pull request Aug 11, 2026
* test(app): cover the sidebar, titlebar and session-row menu PRs

#173, #175 and #179 shipped with no tests; #181 and #183 already carried
their own, so this fills the gap left by the other three.

Unit (test-browser, since home-model reaches kobalte through the directory
picker and will not import under the server condition):

- buildHomeSessionRecords: directory filtering, pathKey normalisation so a
  Windows path and a trailing slash are one directory, sort by last update
  with a created-time fallback, dedupe keeping the fresher copy, sandbox and
  projectID resolution, and dropping a session no project claims.
- descendantIDs: the subtree a delete evicts, including cycle safety. Exported
  for this, alongside buildHomeSessionRecords which was already exported.
- session-share-request: the row-to-timeline share handoff consumes once, and
  a request meant for another session is not swallowed.

E2E:

- sidebar-persistence: the column survives opening a session, orders recents
  newest first, opens the session a row names, and View all and New session
  both work from inside a session.
- sidebar-toggle-search: the titlebar toggle hides and restores the column,
  survives a reload and a route change, and Search lands on the recents list
  with its field focused and the ?focus=search param cleared.
- session-actions-menu: the four actions on both lists, Share hidden when the
  server disables sharing, rename in place without opening the session,
  cancel sending nothing, and delete asking first.

Two defects turned up while writing these, both left unfixed and documented
where they bite:

- Clicking a Recents row opens the session but never marks the row as open;
  the marker only appears once the tab list is read back on a fresh mount.
  Arriving by URL marks it immediately, so the state is right and the click
  path fails to re-run the memo.
- The first activation of a row's actions menu is swallowed. Four clicks give
  closed, open, closed, open, and focus + Enter is swallowed the same way, so
  a user's first click on the dots lands on nothing too.

623 unit, 45 browser-condition unit and all 110 e2e regression tests pass;
tsgo -b and the e2e project both typecheck.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* test(app): drop the workarounds #185 makes unnecessary

Both specs carried a detour around a defect, each with a comment explaining
why. #185 fixes both, so the detours are now dead weight and the comments are
wrong.

- session-actions-menu: openRowMenu clicked a second time when the first was
  swallowed. One click now, and the helper no longer needs the page.
- sidebar-persistence: "marks the row whose session is already open" reloaded
  before asserting data-selected. It asserts on the click that opened the
  session.

The hover in openRowMenu stays. That one is not a workaround: the trigger is
pointer-events-none until its row is hovered, and Playwright hit-tests before
it moves the mouse.

Checked both ways rather than assumed. On #185's branch the three specs pass
20/20; on main without it, both edited tests fail — the menu never reports
aria-expanded=true, and no row is ever marked selected. So this must merge
after #185.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Quickbeard <quickbeard@gmail.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
quickbeard added a commit that referenced this pull request Aug 11, 2026
* docs: fold the seven sidebar entries into one

The Unreleased section had grown to seven bullets that are all one piece of
work: the persistent sidebar, its Recents section, the titlebar buttons, the
folding header, the row menus, and two follow-ups to those. Read from a
release, that is one feature, so it is now one entry carrying all six PR
numbers.

Written as the end state rather than as a history. #185 fixed a row menu that
ignored its first click and a View all row that could be drawn clipped, and
#184 restyled Delete — all of them defects in, or refinements to, work that
has not shipped yet, so a reader of the released changelog never saw the
behavior being corrected. What survives from them is the behavior itself: View
all is left out rather than clipped in a window too short for it, and Delete
is red with no ellipsis.

No detail was dropped on the way: the Projects/Settings/Help move, Recents
sizing and its View all row, the fold and its persistence, the narrow-window
fallback, both titlebar buttons and the absence of keybinds for them, View →
Toggle Sidebar, and every item in the row menu with its rename/delete/share
behavior are all still stated.

Released sections are untouched.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* minor changes

---------

Co-authored-by: Quickbeard <quickbeard@gmail.com>
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