Repository navigation
feat(app): make the sidebar persistent and give it a Recents section - #173
Merged
Merged
Conversation
The project column with Settings and Help was an <aside> owned by the home page, so opening a session left the tab bar as the only navigation. It moves into the new-layout shell and now stays on screen everywhere. Settings and Help anchor to the bottom of the column. A Recents section sits between them and Projects: a New session row (the same openNewSession the home screen's button calls), a list sized to the free height by measuring its container, and a View all row pinned outside the flexible area so it is always reachable without scrolling. View all opens the existing recent-sessions screen, unchanged apart from losing the column beside it. The moved components and the state both screens need are extracted verbatim into fork-only modules rather than threaded as props, so home.tsx's diff is almost entirely deletions. Both callers instantiate the model: the selection lives in shared context, the session index is one TanStack query deduped by key, and the server-normalizing effect is idempotent. On desktop the app now boots into a new session. The redirect pins its decision to the pathname the window came up on and disarms as soon as anything navigates, so it cannot hijack a later visit to "/" — an earlier revision did exactly that and bounced View all straight back to the composer. Browser-side there is no launch event distinct from navigating to "/", so it stays off there. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The Recents list is flex-1 so it can measure how many rows fit, which meant its leftover slack sat between the last session and the View all row below it. View all now renders inside that container, directly after the rows, and the row budget reserves one slot for it — so the spacing is the same 4px as between sessions and the slack falls below it instead. Measured at 4px across 520px, 800px and 1000px window heights, with View all and Settings still inside the viewport at each. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… them The first cut moved seven components out of home.tsx into the new sidebar module, which made an upstream file's diff 820 lines and guaranteed a conflict — plus a manual re-port — every time upstream touches one of those rows. They go back where upstream keeps them and simply gain an `export`: HomeUtilityNav, HomeServerRow, HomeProjectList, HomeProjectEmpty, HomeRecentlyClosedRow, and the three row/label classes. app-sidebar.tsx imports and composes them, and now holds only what is genuinely new — the column shell, the Recents section and its row. home-styles.ts is gone; its two measuring constants sit next to the only code that reads them. home.tsx is down to 36 insertions / 409 deletions, and the deletions are just HomeProjectColumn (dead once the sidebar owns that composition) and the NewHome state block that moved to the model. Upstream edits to any exported row now merge with no conflict. The sub-lg mobile nav is upstream's code again rather than a fork wrapper. Re-verified on a cold desktop start: boot lands on the composer, the sidebar shows 13 recents with a 4px gap before View all, View all renders the 28-row recents screen, and the count reflows to 5 at 520px height. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Home toggled between "/" — the recent-sessions list — and the last active tab. Now that the home screen is a new session, that contradicted itself: pressing Home took you to a list. goHome selects an already-open draft if there is one and otherwise falls through to the existing openNewTab() project-resolution chain, so repeated presses do not stack up draft tabs. The New session button still always makes a new one; only Home reuses. The pressed state keys on the draft route instead of "home". The icon, label and mod+b keybind are unchanged, and tabs.toggleHome is left in place, just uncalled. Verified on a running app: boot shows the composer with Home pressed, View all switches to the 28-row recents list with Home unpressed, and pressing Home returns to the composer and keeps returning there — draft-tab count unchanged across three presses. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The draft screen painted bg-deep — the window-chrome tone — while sessions and the recents list sit on a raised bg-base card. Full width that was merely flat; next to the always-visible sidebar it read as empty space rather than as the screen you are on. NewSessionDesignView switches to bg-base and its wrapper in new-session.tsx takes the bg-base + elevation-raised pair SessionPanelFrame applies with `newLayout raised`. Confirmed identical at runtime rather than by eye: both cards compute to rgb(255,255,255), the same eight-layer shadow, and a 10px radius. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…the titlebar
The DEV/BETA tag sat at the far left of the v2 titlebar, pushing the Home
button 45px to the right of where the row starts. It moves to the foot of the
sidebar, under Help.
ChannelIndicator gains an export and is rendered by AppSidebar rather than
relocated, so the badge markup stays where upstream keeps it; layout-new passes
debugTools to the sidebar as well, which keeps the dev badge's debug-bar toggle
working. Titlebar keeps the prop because the legacy titlebar still renders the
badge inline — only the v2 branch changed.
The sidebar guards on the channel rather than wrapping unconditionally, so on
prod, where the indicator renders nothing, the column's gap-4 does not leave a
hole behind.
Verified on three real builds (the channel is a build-time define, so each
needed its own run):
dev DEV badge at the sidebar foot, still toggles the debug bar, Home x=92
beta BETA badge in the same place, Home x=92
prod no badge; Help's bottom lands exactly where the badge's was (762px),
Recents reclaims the row, Home x=92
x=92 is the leftmost content position on macOS: 84px of traffic-light inset
plus the row's 8px padding.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
3 tasks done
quickbeard
added a commit
that referenced
this pull request
Aug 10, 2026
…itlebar (#175) * feat(app): add a sidebar collapse toggle and a Search button to the titlebar 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> * feat(app): register sidebar.toggle so View → Toggle Sidebar works 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> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This was referenced Aug 11, 2026
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Issue for this PR
Closes #
Type of change
What does this PR do?
Reshapes the desktop home screen around one always-visible sidebar.
The project column carrying Projects plus Settings and Help was an
<aside>owned byNewHome, so it only existed on/— opening a session left the titlebar tab bar as the only navigation. It moves into the new-layout shell and now stays on screen for every route.Inside it:
openNewSession()the home screen's "New session" button does — same project resolution (selected → last touched → first), sameprojects.open+projects.touch+newDraftsequence.ResizeObserveron the list container rendersfloor((height + 4) / 32)rows, so it reflows on resize and never clips a row mid-way.Below the
lgbreakpoint the sidebar hides and the home screen keeps its previous stacked layout.On desktop the app now boots into a new session rather than a list, reusing an existing empty draft instead of accumulating one per launch. This supersedes reopening the screen the window was last on; that session is one click away in Recents and its tab is still restored.
Structure
The moved components and the state both screens need are extracted verbatim into fork-only modules rather than threaded as props, so
home.tsx's diff is almost entirely deletions — the cheaper side of an upstream conflict:components/app-sidebar.tsx— the sidebar, plus the seven components moved out ofhome.tsxpages/home-model.tsx— selection memos, the session-index query, and the actionspages/home-styles.ts— the row/label classes both files usepages/launch-new-session.tsx— the boot redirectBoth
NewHomeandAppSidebarinstantiate the model. That is safe because the selection lives inuseLayout().home, the session index is a single TanStack query deduped by key, and the server-normalizing effect is idempotent. Everydata-component/data-actionhook is preserved byte-identical, sincepackages/app/e2eselects on them.How did you verify your code works?
bun typecheckandbun testinpackages/app— clean, 624 pass / 0 fail.oxlinton the six touched files adds no new warnings (one pre-existing unusedplatforminLegacyHomeremains, unchanged frommain).Then drove a real desktop build over CDP and measured the DOM rather than eyeballing it:
home-session-rows, the search field, and the "New session" button — i.e. the untouched recents screen, now framed by the sidebarlgOne bug this caught. The first version of the boot redirect tested
location.pathname === "/"reactively. On a window that restored into a session the redirect had not yet fired, so clicking View all satisfied its condition and bounced straight back to the composer — the recents screen was unreachable. It now pins its decision to the pathname the window came up on and disarms the moment anything navigates. Re-verified after the fix; the numbers above are from that run.Not verified: the browser surface. The redirect is desktop-gated so
codev serveis unaffected by it, but I did not run the sidebar there.Screenshots / recordings
Boot (new session) and after View all — sidebar in both:
Checklist
DIVERGENCES.mdgains two rows (the sidebar extraction acrosslayout-new.tsx/home.tsx/ the i18n dicts, and the boot redirect mount), both spelling out how to re-apply on an upstream conflict. The four new modules are fork-only and untracked there.CHANGELOG.mdgets two entries under Unreleased.🤖 Generated with Claude Code