Skip to content

feat(app): add a sidebar collapse toggle and a Search button to the titlebar - #175

Merged
quickbeard merged 2 commits into
mainfrom
sidebar-toggle-search
Aug 10, 2026
Merged

quickbeard merged 2 commits into
mainfrom
sidebar-toggle-search

Conversation

@quickbeard

Copy link
Copy Markdown
Owner

Issue for this PR

Closes #

Type of change

  • New feature

What does this PR do?

Two buttons in the titlebar.

Sidebar collapse toggle, leading the row — ahead of the WindowsAppMenu that renders on Windows and Linux, so it is the first item on every platform. #173 made the sidebar permanent chrome with no way to dismiss it short of shrinking the window below lg. Collapsed hides the 280px column outright rather than narrowing it to a rail, and the state persists globally so it survives a relaunch. It shows pressed while the sidebar is open.

One consequence worth stating: Projects live only in the sidebar now, so while collapsed you re-open it to switch project.

Search, immediately right of Home — opens the recent-sessions list exactly as the sidebar's View all does, and focuses its search field on arrival.

Neither has a keyboard shortcut, so mod+b keeps its current meaning.

Icons come from the existing v2 sprite, no new artwork: magnifying-glass, and sidebar-right for the toggle — despite the name its divider sits at x=7 of a 2.9–17.1 box, so the narrow pane is on the left. Labels reuse command.sidebar.toggle and home.sessions.search.placeholder, both already present in en and es.

Two things that shaped the implementation

useAppSidebar() returns undefined outside a provider instead of throwing. useV2Titlebar() is just settings.general.newLayoutDesigns(), and app.tsx still mounts LegacyServerLayout → LegacyLayout → <Titlebar> for /:dir routes even when the new layout is on — so the v2 titlebar renders in places layout-new.tsx does not wrap. createSimpleContext throws without a provider, so it is deliberately not used here. The toggle is hidden in that shell by construction, which is correct: it has no sidebar of this kind.

Search hands off through a ?focus=search param that NewHome consumes and clears, mirroring how pages/new-session.tsx handles ?prompt. The focus ref had to become a signal: HomeSessionSearch binds it from onMount, which runs after NewHome's effects, so the first version no-opped against an undefined ref and cleared the param anyway — the field never got focus. Caught in verification, not review.

How did you verify your code works?

bun typecheck clean, 624 unit tests pass, no new lint warnings (20 before and after on the touched files). bunx playwright test e2e/smoke/session-timeline.spec.ts — 5/5 pass.

Then drove a real desktop build over CDP, measuring rather than eyeballing:

  • button order left to right: Toggle (x=92), Home (x=134), Search (x=176) — toggle is leftmost
  • toggle collapses and restores the sidebar; aria-pressed tracks it both ways
  • collapsed state survives a full reload
  • Search from inside a session lands on the recents list with document.activeElement carrying the "Search sessions" placeholder, and the query param cleared
  • Search again while already on / re-focuses the field

And the legacy-shell path, since that is the failure mode the optional context exists to prevent: flipped general.newLayoutDesigns off in the persisted store, reloaded, and confirmed the legacy shell renders its own rail with no toggle and no exception — then restored it.

Not verified: Windows or Linux. Nothing builds the desktop app on those platforms, in CI or locally, and the two OS-specific behaviours here (the toggle sitting ahead of WindowsAppMenu, the different left inset) are read from the conditionals rather than executed.

Screenshots / recordings

Titlebar with both new buttons, sidebar open:

titlebar

Checklist

  • I have tested my changes locally
  • I have not included unrelated changes in this PR

DIVERGENCES.md: the titlebar.tsx and sidebar rows are extended rather than duplicated, including a note not to swap useAppSidebar for createSimpleContext. context/app-sidebar.tsx is fork-only and untracked there. One CHANGELOG.md entry under Unreleased.

One thing I left out, for your call: the app menu's View → Toggle Sidebar item is permanently greyed out in the new layout, because sidebar.toggle is only registered by LegacyLayout. Registering it from the new sidebar with no keybind would light it up without adding a shortcut — roughly five lines, say the word.

🤖 Generated with Claude Code

quickbeard and others added 2 commits August 10, 2026 12:22
…itlebar

The sidebar became permanent chrome in #173 with no way to dismiss it, and the
recents list was reachable only from its own View all row.

The toggle leads the titlebar — ahead of the Windows/Linux app menu, so it is
the first item on every platform. Collapsed hides the column outright rather
than narrowing it to a rail; the state persists globally, so it survives a
relaunch. Search sits right of Home, navigates to the recents list like View
all, and focuses its search field.

Collapse state lives in a fork-only context so the titlebar button and the
sidebar can share it. useAppSidebar() returns undefined outside a provider
rather than throwing: useV2Titlebar() is just newLayoutDesigns(), so this
titlebar also renders inside the legacy shell, which has no provider and no
sidebar of this kind — the toggle is hidden there by construction.

Search hands off through a ?focus=search param that NewHome consumes and
clears, mirroring how new-session.tsx handles ?prompt. The focus ref had to
become a signal: HomeSessionSearch binds it from onMount, which runs after
NewHome's effects, so the first attempt no-opped against an undefined ref and
cleared the param anyway.

No keyboard shortcut, so mod+b keeps its current meaning.

Verified on a running build: button order Toggle(92) Home(134) Search(176);
collapse and restore both track the pressed state; collapsed survives a reload;
Search focuses the input both from a session and from / itself. With the new
layout switched off, the legacy shell renders its own rail, no toggle, and
throws nothing. 624 unit tests and the 5 smoke e2e tests pass.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The app menu dispatches command id "sidebar.toggle", and only LegacyLayout ever
registered it — so on the new layout the item did nothing. AppSidebarProvider
now registers it with no keybind, since mod+b belongs to Home here rather than
to the sidebar as it does in the legacy layout.

It lives on the provider rather than on AppSidebar so that collapsing the
sidebar does not unregister the one command that brings it back.

Verified through the command registry, which is the path the menu item takes:
opened the palette with mod+k, typed "toggle sid", got "Toggle sidebar" as a
match, and selecting it collapsed the sidebar.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@quickbeard
quickbeard merged commit 477aee1 into main Aug 10, 2026
0 of 5 checks passed
@quickbeard
quickbeard deleted the sidebar-toggle-search branch August 10, 2026 06:40
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