diff --git a/CHANGELOG.md b/CHANGELOG.md
index b102d67316..2b8bfa3f70 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -9,6 +9,13 @@ file is the human-readable summary of what changed and why.
### Desktop / Web UI
+- **New sessions show up in Recents right away** — starting a session left the
+ sidebar's Recents list unchanged; the session only appeared once something
+ made that list reload, which is why switching between Code and Work and back
+ brought it in. The list now updates as sessions are created, and a session's
+ title lands in it as soon as the model picks one instead of waiting for the
+ next reload. This was most obvious in Work, where there is less to nudge the
+ list into reloading, but Code was affected the same way.
- **Windows sandboxes are now firewalled** — on macOS and Linux the sandbox
guest has no network device at all, but a WSL guest shares Windows' network,
so a Windows sandbox could previously reach the internet directly. The
diff --git a/DIVERGENCES.md b/DIVERGENCES.md
index f288ee2a19..235036704b 100644
--- a/DIVERGENCES.md
+++ b/DIVERGENCES.md
@@ -121,6 +121,7 @@ from the squash message first, or use a plain merge.
| `packages/app/src/context/tabs.tsx` | `newDraft` inserts with `tabs.unshift(tab)` instead of `tabs.push(tab)`, so a draft opens at the head of the tab strip. Session tabs (`addSessionTab`) still append, and nothing else about draft handling changes. | A draft is where you start a session, so it belongs at the front of the strip rather than behind every session already open — with several tabs open the New session tab was scrolling off the right-hand end. | Keep the `unshift`. On conflict take upstream's `newDraft` and swap the insert back. Note the knock-on: `promoteDraft` replaces the draft **at its own index**, so a promoted session keeps the head position and sessions accumulate newest-first. That is intended — the tab you are working in should not jump across the strip the moment you send the first message — but if it is ever unwanted, move the tab in `promoteDraft` rather than reverting this, or the New session tab goes back to the end. |
| `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 `` owned by `NewHome`, so it exists only on `/`. It becomes app chrome: `layout-new.tsx` wraps its `` in a row flex and renders the fork-only `` beside it, so the sidebar survives every navigation. **The row components stay in `home.tsx`** — `HomeUtilityNav`, `HomeServerRow`, `HomeProjectList`, `HomeProjectEmpty`, `HomeRecentlyClosedRow` and the classes `HOME_PROJECT_NAV_ROW` / `HOME_PROJECT_NAV_LABEL` / `HOME_SECTION_LABEL` gain an `export` and nothing else, so upstream edits to them still apply cleanly; the fork-only `src/components/app-sidebar.tsx` imports and composes them, adding only the column shell and the Recents section (`HOME_SECTION_LABEL` is consumed one level down, by the fork-only `src/components/sidebar-section.tsx`). `home.tsx` otherwise loses just `HomeProjectColumn` (dead once the sidebar owns that composition) and the `NewHome` selection/action block, and its grid collapses from `lg:grid-cols-[280px_minmax(0,720px)]` to one `max-w-[760px]` column. The sub-`lg` `HomeUtilityNav` mount is untouched, so the stacked mobile layout is upstream's. The state both screens need (`focusedServer`, `projects`, `selectedProject`, `newSessionProject`, `projectDirectories`, the session-index query, `openNewSession`, `openProjectNewSession`, `openSession`, `selectProject`, `addProjects`, `editProject`, `closeProject`, `unseenCount`, `clearNotifications`, `chooseProject`, `isBackgroundOpen`, `buildHomeSessionRecords`, `HomeSessionRecord`) moves to fork-only `src/pages/home-model.tsx`, which both callers instantiate — safe because the selection lives in `useLayout().home`, the index is one TanStack query deduped by key, and the server-normalizing effect is idempotent. New i18n keys `sidebar.recents`, `sidebar.recents.new`, `sidebar.recents.viewAll` in `en` + `es`. `layout-new.tsx` additionally wraps its shell in the fork-only `` (so the titlebar toggle and the sidebar share one collapse state), `AppSidebar` renders nothing while collapsed, and `NewHome` consumes a `?focus=search` param — clearing it after use, the way `pages/new-session.tsx` handles `?prompt` — to focus the search field for the titlebar's Search button. That focus ref changed from a `let` to a signal because `HomeSessionSearch` binds it from `onMount`, which runs after `NewHome`'s effects, so a plain ref is still undefined when the param arrives. `AppSidebarProvider` also registers the `sidebar.toggle` command (no keybind — `mod+b` is Home's here, unlike in the legacy layout), which is what the app menu's View → Toggle Sidebar dispatches; only `LegacyLayout` registered it before, so that item did nothing on this layout. It lives on the provider, not on `AppSidebar`, so collapsing does not unregister the command that undoes it. Both shells can be mounted at once on `/:dir` routes, so the id may be registered twice there; the command registry keeps the first entry, which is the outer (new-layout) one. Every `data-component`/`data-action` hook is untouched (`home-project-row`, `home-add-project`, `home-session-row`, `home-session-search`, …) because `packages/app/e2e` selects on them. | The desktop app should navigate like Claude Desktop — one always-visible sidebar with Projects, Recents and pinned Settings/Help — instead of losing all navigation the moment a session opens. Exporting the row components in place rather than relocating them keeps `home.tsx` down to ~36 added lines: upstream changes to those components merge without a conflict, which relocating them would have forfeited. | Keep our structure. On conflict take upstream's `home.tsx` wholesale and re-apply the four edits: the `export` keywords, the `HomeProjectColumn` deletion, the `createHomeModel()` swap in `NewHome`, and the single-column grid. If upstream edits an exported row component, just take it. Never let a `data-component`/`data-action` hook drift — the e2e suite has no other handle on these rows. |
+| `packages/app/src/app.tsx` | `AppInterface`'s `ServerShell` no longer wraps its children in a second ``; it renders `` directly. `QueryProvider` itself is unchanged and still installed once, by `AppBaseProviders`. | `QueryProvider` builds a fresh `QueryClient` on every render, and it was mounted twice: once in `AppBaseProviders` and again inside `ServerShell`. `GlobalProvider` sits above `ServerShell`, so `ServerSync` — and the home session index cache it owns (`context/global-sync/home-session-index.ts`) — resolved the outer client, while everything under the router, including the sidebar and the home screen, resolved the inner one. The index that `session.created`/`session.updated` events write to therefore had no readers, and the list those events exist to keep fresh could only change when its own query refetched. In this fork that list is the persistent sidebar's Recents section, so a session stayed missing from Recents until something remounted the observer — switching Code/Work and back reassigns the observer's key, which is why that looked like the fix. Both `AppInterface` call sites (`packages/app/src/entry.tsx`, `packages/desktop/src/renderer/index.tsx`) already wrap it in `AppBaseProviders`, so the nested client was always redundant. | Keep the single provider. If upstream reworks the shell, the invariant to preserve is that `ServerSync` and the components reading its queries resolve the *same* `QueryClient` — verify by checking that `homeSessions.apply` and the sidebar's `sessionLoad` land in one cache, not that this exact JSX survives. |
| `packages/app/src/context/settings.tsx`, `packages/app/src/components/settings-general.tsx`, `packages/app/src/components/settings-v2/general.tsx`, `packages/app/src/context/settings.test.ts`, `packages/app/src/pages/session/composer/session-revert-dock.stories.tsx`, deleted `packages/app/src/components/settings-v2/interface-transition.tsx` + `interface-transition.stories.tsx`, deleted `packages/app/e2e/regression/legacy-new-session.spec.ts`, `packages/app/e2e/regression/session-timeline-projection.spec.ts`, `packages/app/e2e/performance/timeline-stability/fixture.ts` | **The legacy layout is retired: the fork ships one layout and no way back.** Upstream runs a timed migration — `oldInterfaceSunset` (a hardcoded date), a per-profile `layoutTransitionEligible` flag, a `newLayoutDesigns` preference and a Settings → General **New layout** switch — so existing users can flip between layouts until the sunset passes. All of it goes. `newLayoutDesigns` becomes `createMemo(() => true)`: reading a constant rather than the persisted value is what makes a profile carrying `newLayoutDesigns: false` (older build, another device via sync, hand-edited store) still come up on the new layout, which suppressing the toggle alone would not. `setNewLayoutDesigns`, `layoutTransitionAvailable`, `newInterfaceNoticeVisible` and `dismissNewInterfaceNotice` are **removed from the context rather than stubbed** — a no-op setter would let a caller look like it was switching layouts, so re-introducing the toggle has to be a compile error. Both Settings screens (v1 `settings-general.tsx` and v2 `settings-v2/general.tsx`) lose their `InterfaceSection` + `InterfaceNoticeSection` and the `` blocks that mounted them; `interface-transition.tsx` and its story are deleted with their last caller. The sunset machinery in `settings.tsx` goes with it — `oldInterfaceSunset`, `legacyNewLayoutDesignsDefault`, `newLayoutDesignsDefault`, `newLayoutDesignsUpgradeCutoff`, `shouldEnableNewLayout`, `layoutTransitionState`, `maximumSunsetTimeout`, `nextSunsetCheckDelay`, `resolveNewLayoutDesigns`, the retirement `setTimeout` and the two `createEffect`s that wrote `newLayoutDesigns` back to the store — along with their tests; `settings.test.ts` keeps only the `isAppUpgrade` / `shouldDisplayTabsToast` block. **Deliberately kept**: `layoutTransitionEligible`, `layoutTransitionClassified` and `setOldLayoutEligible` (with the desktop `isOldLayoutEligible` IPC that feeds them), because the tabs toast still uses eligibility to tell a fresh profile from an existing install — they no longer gate any layout. Also kept: every `newLayoutDesigns()` **read** in `app.tsx` and the `LegacyServerLayout` / `LegacyHome` code behind them. That shell is still mounted on `/:dir` routes, and leaving the branches in place keeps upstream's edits to them applying cleanly; deleting the legacy layout outright is a far larger diff for no user-visible gain. On the test side `legacy-new-session.spec.ts` is deleted (it asserted a redirect to a route that no longer resolves), `session-timeline-projection.spec.ts` drops its one `settings: { newLayoutDesigns: false }` pin, and the timeline-stability fixture drops the `newLayoutDesigns === false` branch that back-dated `app-version.v1`. The many specs that set `newLayoutDesigns: true` are untouched — the value is simply ignored now. The fork-only `packages/app/e2e/regression/layout-theme-lock.spec.ts` covers both locks from a seeded profile; each of its four cases was checked to fail when the matching source file is reverted to upstream's. | The fork ships one layout. Leaving a switch that drops users into a layout the fork does not design for, style-test or screenshot means two UIs to maintain and support, and the upstream sunset date would have retired it anyway. | Keep ours. On conflict take upstream's file and re-apply: the constant `newLayoutDesigns`, the four removed context members, the two deleted Settings sections. If upstream adds a new layout-preference surface, remove it too rather than wiring it up. Do **not** "restore" `setNewLayoutDesigns` to make a new upstream caller compile — delete the caller. Do not delete the `LegacyServerLayout` branches in `app.tsx`; they are load-bearing on `/:dir` and deliberately left as upstream's. |
| `packages/ui/src/theme/context.tsx`, `packages/app/public/oc-theme-preload.js`, `packages/app/src/theme-preload.test.ts`, `packages/app/src/pages/layout.tsx`, `packages/app/src/components/settings-general.tsx`, `packages/app/src/components/settings-v2/general.tsx` | **One theme in the web/desktop UI; light and dark are the only axis.** Upstream ships 36 selectable palettes with a Settings → Appearance **Theme** picker and command-palette entries. The fork exposes none of them: OC-2 is the theme, `colorScheme` (system/light/dark) is the only appearance choice. The lock lives at `normalize()` in `theme/context.tsx`, renamed to the exported `normalizeThemeID()` and returning the exported `SHIPPED_THEME_ID` unconditionally — that one function is already on every path (provider init, the `onMount` migration, `load`, the cross-tab `storage` listener, `setTheme`, `previewTheme`), so a stored id from before the picker was removed, or one written by another tab, resolves to OC-2 without a second guard. It is exported (and the six call sites renamed) so `theme/context.test.ts` can cover the invariant directly instead of only through its call sites — the module imports cleanly under `bun test` because `import.meta.glob` is called lazily inside `getFiles()`. It also gives the migration for free: `onMount` already rewrites `opencode-theme-id` and calls `clear()` when the raw value differs from the normalized one, which now drops the stale `opencode-theme-css-{light,dark}` cache. `ids()` returns `["oc-2"]` and `loadThemes()` loads only OC-2 instead of all 36 (it was `Promise.all(themeIDs().map(load))`). The bundled theme JSON, the `names` map and `registerTheme` are **left alone** — unreferenced data costs nothing and keeps upstream's theme edits applying cleanly. `oc-theme-preload.js` does the same rewrite *before* mount, so the first launch after upgrading does not paint the user's old palette for a frame; with no other theme reachable its cached-CSS `