From cacc5580978e91910a38acffbef280f4fd2ba069 Mon Sep 17 00:00:00 2001 From: minhn4 Date: Mon, 10 Aug 2026 12:22:59 +0700 Subject: [PATCH 1/2] feat(app): add a sidebar collapse toggle and a Search button to the titlebar MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- CHANGELOG.md | 11 + DIVERGENCES.md | 4 +- packages/app/src/components/app-sidebar.tsx | 244 ++++++++++---------- packages/app/src/components/titlebar.tsx | 42 +++- packages/app/src/context/app-sidebar.tsx | 44 ++++ packages/app/src/pages/home.tsx | 35 ++- packages/app/src/pages/layout-new.tsx | 11 + 7 files changed, 256 insertions(+), 135 deletions(-) create mode 100644 packages/app/src/context/app-sidebar.tsx diff --git a/CHANGELOG.md b/CHANGELOG.md index b44fe78cea..be189ab787 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,17 @@ file is the human-readable summary of what changed and why. ### Desktop / Web UI +- **The sidebar collapses, and Search opens the recents list** — two buttons join + the titlebar. The leftmost one hides and restores the sidebar, so a session can + have the full window width; it remembers the choice for next launch, and it + lights up while the sidebar is open. Note that Projects live only in the + sidebar, so switching project means opening it again. The second, next to + Home, opens the recent-sessions list exactly as **View all** does and drops the + cursor straight into its search field. Neither has a keyboard shortcut, so + nothing you already press has changed meaning. + +### Desktop / Web UI + - **The sidebar is always on screen, and it carries your recent sessions** — the Projects list plus Settings and Help used to be a column belonging to the home screen, so opening a session left you with no navigation but the tab bar. diff --git a/DIVERGENCES.md b/DIVERGENCES.md index 8d2a8c68af..66ca84fee5 100644 --- a/DIVERGENCES.md +++ b/DIVERGENCES.md @@ -96,9 +96,9 @@ from the squash message first, or use a plain merge. | `packages/ui/src/v2/components/wordmark-v2.tsx` (+ fork-only `wordmark-v2.css`) | `WordmarkV2` — the watermark behind the v2 new-session composer (`packages/app/src/components/session/session-new-design-view.tsx`) — draws the codev wordmark from the same 24x6 pixel grid as `Logo` instead of upstream's 720x129 "opencode", as two ``s (`data-slot="wordmark-v2-co"` / `…-dev`). Upstream's watermark treatment is kept verbatim: the `0.6` outer opacity and the alpha mask whose gradient fades the mark out over its bottom half (converted to the new viewBox: gradient `y1` 68/129 → 3.163/6). "co" keeps `fill="currentColor"` so it still follows the caller's `text-*` class; "dev" takes brand red `#ee0033`. Per-half opacities live in the new `wordmark-v2.css`, following the one-CSS-file-per-v2-component convention: `co` is `0.112` (upstream's own `0.16 × 0.7` nesting, flattened), `dev` is boosted to `0.224` on light and `0.336` on dark via `--wordmark-v2-dev-opacity`, declared under `prefers-color-scheme` and both `[data-color-scheme]` values like components/logo.css. The boost is deliberate and measured: at upstream's weight a saturated red composites to within a few levels of the grey "co" half against the dark background and the two-tone disappears entirely, while the same value already reads pink against the light one. | The component is currently **not rendered** — the new-session view drops it (see the row below) — but it stays rebranded and exported so reinstating the watermark does not bring the upstream mark back with it. | Keep our version wholesale, including the per-theme `dev` opacity. If upstream retunes the watermark opacities, re-derive the `dev` boost from the new base rather than adopting a single flat value. | | `packages/app/src/components/session/session-new-design-view.tsx`, `packages/app/src/pages/new-session.tsx` | Drops the `WordmarkV2` watermark from the v2 new-session screen and centres the composer. Upstream absolutely positions the content block at `top-[25.375%]` to leave room for the wordmark above it; with the mark gone that offset just parks the composer high in an empty pane, so the outer element becomes `flex items-center justify-center` over `size-full` (carrying the `px-6` inset across) and the `mt-8` spacer under the wordmark goes away. The pane also takes the same raised card every session sits on: `bg-v2-background-bg-deep` → `bg-v2-background-bg-base` in the view, and `new-session.tsx`'s `rounded-[10px]` wrapper gains `bg-v2-background-bg-base shadow-[var(--v2-elevation-raised)]` — the exact pair `SessionPanelFrame` (`pages/session.tsx`) applies with `newLayout raised`. Verified equal at runtime: same background, same eight-layer shadow, same 10px radius. | The wordmark reads as clutter on the desktop new-session screen; without it the pane should be balanced, not top-weighted. And with the sidebar always on screen, upstream's `bg-deep` made the draft screen read as an extension of the window chrome while every session floated on a card — the two screens have to match. | Keep our centred layout and the card. If upstream restyles this view, re-apply the drop + centring rather than restoring `top-[25.375%]`, and re-check the background against whatever `SessionPanelFrame` then uses rather than hardcoding `bg-base`. Reinstating the watermark means restoring the offset too, or it will sit centred with the composer. | | `packages/app/src/pages/home.tsx`, `packages/app/src/pages/error.tsx`, `packages/enterprise/src/routes/share/[shareID].tsx` | `Logo` width utilities retuned for the codev wordmark's 4:1 box (upstream's is 5.57:1): home `md:w-xl` → `md:w-104`, error `w-58.5` → `w-42`, share (both call sites) `w-58.5` → `w-42` | Each value is the width at which the new wordmark renders at exactly the height the upstream one did, so no surrounding layout shifts. | Keep our widths; take upstream's surrounding changes. Recompute rather than restore if upstream changes the size. | -| `packages/app/src/components/titlebar.tsx` | Two changes, both to the **v2 titlebar branch only** (`useV2Titlebar()`); the legacy branch below it is untouched. (1) The Home button (and its `home.toggle` command, `mod+b`) opens a new session instead of toggling between `/` and the last active tab: `tabs.toggleHome(...)` becomes a `goHome` that selects an already-open draft if there is one and otherwise falls through to the existing `openNewTab()` project-resolution chain. Its pressed / `aria-pressed` state keys on `layout.route().type === "draft"` rather than `"home"`. The icon, label (`home.title`) and keybind are unchanged, as is `tabs.toggleHome` itself, which is simply no longer called. (2) `` is dropped from that row and the component gains an `export`, so the sidebar can render the DEV/BETA badge at its bottom instead; `Titlebar` keeps its `debugTools` prop because the legacy branch still renders the badge inline. | The home screen is a new session in this fork, so Home has to go there; toggling into the recent-sessions list contradicted that. Reuse-before-create keeps repeated presses from stacking up draft tabs — unlike the New session button, which is expected to always make a new one. Moving the badge lets Home lead the titlebar (measured: x 137 → 92 on macOS, the traffic-light inset). | Keep our `goHome` and the removed indicator. If upstream reworks the titlebar, re-apply both rather than restoring `toggleHome` or the inline badge. If upstream deletes `tabs.toggleHome`, nothing here breaks. Do not remove `Titlebar`'s `debugTools` prop — the legacy titlebar still needs it. | +| `packages/app/src/components/titlebar.tsx` | Two changes, both to the **v2 titlebar branch only** (`useV2Titlebar()`); the legacy branch below it is untouched. (1) The Home button (and its `home.toggle` command, `mod+b`) opens a new session instead of toggling between `/` and the last active tab: `tabs.toggleHome(...)` becomes a `goHome` that selects an already-open draft if there is one and otherwise falls through to the existing `openNewTab()` project-resolution chain. Its pressed / `aria-pressed` state keys on `layout.route().type === "draft"` rather than `"home"`. The icon, label (`home.title`) and keybind are unchanged, as is `tabs.toggleHome` itself, which is simply no longer called. (2) `` is dropped from that row and the component gains an `export`, so the sidebar can render the DEV/BETA badge at its bottom instead; `Titlebar` keeps its `debugTools` prop because the legacy branch still renders the badge inline. (3) Two buttons added to the row: a sidebar collapse toggle (`sidebar-right` icon, `data-action="titlebar-sidebar-toggle"`, `pressed` while open) as its **first** child, ahead of the `windows() || linux()` `WindowsAppMenu`; and a Search button (`magnifying-glass`, `data-action="titlebar-search"`) immediately after Home, navigating to `/?focus=search`. The toggle reads the fork-only `useAppSidebar()` and is wrapped in ``, which is what keeps it out of the legacy shell. Labels reuse the existing `command.sidebar.toggle` and `home.sessions.search.placeholder` keys — no new i18n. | The home screen is a new session in this fork, so Home has to go there; toggling into the recent-sessions list contradicted that. Reuse-before-create keeps repeated presses from stacking up draft tabs — unlike the New session button, which is expected to always make a new one. Moving the badge lets Home lead the titlebar (measured: x 137 → 92 on macOS, the traffic-light inset). | Keep our `goHome` and the removed indicator. If upstream reworks the titlebar, re-apply both rather than restoring `toggleHome` or the inline badge. If upstream deletes `tabs.toggleHome`, nothing here breaks. Do not remove `Titlebar`'s `debugTools` prop — the legacy titlebar still needs it. Keep `useAppSidebar()` optional-by-design: `useV2Titlebar()` is just `newLayoutDesigns()`, so this titlebar also renders inside the legacy shell, which has no provider — swapping it for `createSimpleContext` would throw there (verified: with the new layout off, the legacy shell renders its own rail, no toggle, and no exception). | | `packages/app/src/pages/layout-new.tsx` (again) | Mounts the fork-only ``, which on desktop redirects one time per boot to a new-session draft (reusing an existing empty draft rather than stacking one up per launch). It pins its decision to the pathname the window came up on and disarms as soon as anything navigates, so it can never hijack a later visit to `/` — the sidebar's "View all" links there. Desktop-only: in the browser there is no "launch" distinct from "navigate to /", so redirecting would make the recents screen unreachable by URL and would break two `packages/app/e2e` specs that `goto("/")`. | Launching should put you in front of a prompt, the way Claude Desktop does, rather than on a list. This supersedes the desktop's restore-last-screen behaviour (`getLastActiveUrl`, `packages/desktop/src/renderer/index.tsx`); the previous session stays one click away in Recents and its tab is still restored. | Keep the mount. If upstream restructures this layout, re-add it. Do not "simplify" the boot-window guard into a plain `location.pathname === "/"` check — that is the bug it exists to prevent. | -| `packages/app/src/pages/layout-new.tsx`, `packages/app/src/pages/home.tsx`, `packages/app/src/i18n/{en,es}.ts` | **Persistent app sidebar.** Upstream's project column is an `