Skip to content

feat(app): add a session actions menu to both session lists - #179

Merged
quickbeard merged 2 commits into
mainfrom
session-row-menu
Aug 10, 2026
Merged

quickbeard merged 2 commits into
mainfrom
session-row-menu

Conversation

@quickbeard

@quickbeard quickbeard commented Aug 10, 2026 •

Copy link
Copy Markdown
Owner

Every session row — in the sidebar's Recents and on the home View all list — gains a hover-revealed vertical three-dot button at its right edge. It opens the same menu the ⋯ button beside a session title opens: Rename, Share…, Archive, Delete….

Previously the only way to rename, archive or delete a session was to open it first and use the menu next to its title, which is exactly backwards when you are clearing out sessions you do not want to open.

The menu is reused, not reimplemented

Both renderings — the legacy DropdownMenu and the v2 MenuV2 — plus the DialogDeleteSession closure move verbatim out of message-timeline.tsx into the fork-only components/session-actions-menu.tsx. The session header renders that component too, so there is one definition of the four items instead of three copies free to drift.

One subtlety worth not undoing: the component owns the deferred Rename and Share. Neither can run on onSelect — the menu is still closing and pulls focus back to its trigger, which blurs the inline editor the instant it opens. That deferral used to be the header's pendingRename/pendingShare store fields; it now lives in the component, so every caller inherits it.

Rename edits the row's label in place using the existing createInlineEditorController (the same primitive the legacy sidebar's workspace rename uses) — Enter saves, Esc cancels. archiveSession/renameSession/deleteSession move into createHomeModel() so the sidebar and the home screen run one implementation; home.tsx's duplicate archiveSession is gone.

Two bugs this surfaced

The local sidecar publishes no session lifecycle events. Watching /event while creating and archiving sessions against a running app produces nothing but server.connected. The recents index is only ever corrected by a refetch, so an archived row stayed on screen looking broken. Actions now invalidate the index query. This is pre-existing — the session header's Archive has the same staleness — but it is what made the new menu look non-functional.

share is scope-sensitive. It exists only on the directory-scoped sync store; the server-level store has no such key at all. Gating on the server-level store offered Share on a server with sharing disabled, and the session page then refused to open the popover. The rows now read ensureDirSyncContext(session.directory) — the same store the session page reads through useSync — and the row menu matches the header exactly.

Share from a row

Share opens the session and asks it to open its popover, via a small pending-signal bridge (utils/session-share-request.ts). The popover cannot live on a row: it reads share.url off the fully synced session, and the recents index drops it because SessionV2Info has no share field — making it work in place would be a Schema/Protocol/Server change.

This bridge is unverified at runtime. The environment it was tested against has share: "disabled" both globally and per-directory, so the item is correctly hidden in both menus there and the path could not be exercised.

Verification

Driven against a running desktop app over CDP:

  • Menu renders in both lists with the correct items, matching the session header
  • Inline rename opens focused, saves on Enter, cancels on Esc
  • Archive removes the row and sets archived server-side
  • Delete confirms, removes the row, and the session 404s afterwards
  • Session header menu still works (both design variants)

bun turbo typecheck clean across 32 packages. bun test in packages/app: 624 pass / 0 fail. Lint clean on touched files.

Ten throwaway sessions were created for testing and all deleted; a final audit confirmed no real session was archived or deleted.

Fork bookkeeping

DIVERGENCES.md gains rows for the two upstream files touched (message-timeline.tsx, home.tsx). The four fork-only files are not tracked there. CHANGELOG.md has an Unreleased entry.

🤖 Generated with Claude Code

Every session row in the sidebar's Recents and on the home "View all"
list gains a hover-revealed vertical three-dot button opening the same
menu the session header has: Rename, Share, Archive, Delete. Renaming
edits the row label in place rather than making you open the session
first.

The menu is reused, not reimplemented. Both renderings -- the legacy
DropdownMenu and the v2 MenuV2 -- plus DialogDeleteSession move verbatim
out of message-timeline.tsx into components/session-actions-menu.tsx,
which the header now renders too. Deferring Rename and Share to the
menu's onCloseAutoFocus becomes the component's job, since every caller
needs it: the closing menu pulls focus back to its trigger and would
otherwise blur the inline editor the moment it opened.

archiveSession/renameSession/deleteSession move into createHomeModel so
the sidebar and the home screen share one implementation.

Two fixes this surfaced:

- The local sidecar's /event stream publishes no session lifecycle
  events (verified against a running app: neither create nor archive
  emits anything), so the recents index only ever corrected itself on a
  refetch and an archived row stayed on screen. Actions now invalidate
  the index query.

- `share` exists only on the directory-scoped sync store; the
  server-level store has no such key, so gating on it offered Share on a
  server with sharing disabled, which the session page then refused to
  honour. The rows now read ensureDirSyncContext(session.directory), the
  same store the session page uses.

Sharing from a row opens the session and asks it to open its popover --
the share URL is not carried by the list, because SessionV2Info has no
share field. That bridge is unverified at runtime: sharing is disabled
in the environment this was tested against, so the item is correctly
hidden in both menus there.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@quickbeard
quickbeard merged commit 36f3233 into main Aug 10, 2026
0 of 5 checks passed
@quickbeard
quickbeard deleted the session-row-menu branch August 10, 2026 08:57
quickbeard pushed a commit that referenced this pull request Aug 11, 2026
#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>
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>
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