Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions DIVERGENCES.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 `<LaunchNewSession />`, 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 `<aside>` owned by `NewHome`, so it exists only on `/`. It becomes app chrome: `layout-new.tsx` wraps its `<main>` in a row flex and renders the fork-only `<AppSidebar class="hidden lg:flex">` 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 `<AppSidebarProvider>` (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 `<QueryProvider>`; it renders `<SharedProviders>` 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 `<Show>` 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 `<style>` injection is gone and it only sets the mode attributes. Removed surfaces: the **Theme** `SettingsRow`/`SettingsRowV2` in both Settings screens (with the `…/themes` docs "Learn more" link, `themeOptions`, the `ThemeOption` type and the `onMount(loadThemes)` that fed them), and in `pages/layout.tsx` the `theme.cycle` command with its `mod+shift+t` keybind, the `cycleTheme` helper and the `theme.set.<id>` command loop (with `availableThemeEntries`). The **colour-scheme** commands stay: `theme.scheme.cycle` (`mod+shift+s`), the three `theme.scheme.<scheme>` entries and their preview/commit wiring. i18n keys (`settings.general.row.theme.*`, `command.theme.cycle`, `command.theme.set`, `toast.theme.title`) are left in the dicts — unused keys are harmless and removing them only creates conflicts. | Every fork surface — the brand, the icons, the screenshots, the docs — is drawn against OC-2. A palette picker means 36 combinations nobody verifies, and a user on Synthwave '84 files bug reports about a UI the fork never shipped. Light/dark still covers the real need. | Keep ours. Take upstream's surrounding changes; re-apply the `normalize()` constant, the `ids()`/`loadThemes()` narrowing, the preload rewrite, and the removed rows/commands. If upstream adds a new theme-selection surface, remove it rather than wiring it up. Do not "fix" `normalize()` back to the `oc-1 → oc-2` mapping — that mapping is a subset of what it does now. Leave the bundled theme files and `names` map in place; deleting them is a much bigger conflict surface for no gain. |
| `packages/app/src/pages/session/timeline/message-timeline.tsx` | **Session actions menu extracted.** The ⋯ menu beside the session title — both renderings, the legacy `DropdownMenu` and the v2 `MenuV2`, ~145 lines — and the `DialogDeleteSession` closure move to the fork-only `packages/app/src/components/session-actions-menu.tsx` (verbatim at extraction; the v2 Delete item has since dropped its ellipsis and taken the danger colour, so do not diff the two expecting a match), and this file renders `<SessionActionsMenu>` instead, passing its existing `openTitleEditor` / share / `archiveSession` / `deleteSession` as callbacks and its trigger element out through `triggerRef` (the share popover still anchors to it). The extracted dialog takes a resolved `title` and an `onConfirm` rather than a `sessionID`, so the caller supplies `sessionTitle(sync().session.get(id)?.title)`. The `title` store loses `pendingRename`/`pendingShare`: deferring Rename and Share until the menu's `onCloseAutoFocus` is now the component's job, since every caller needs it (a list row's inline editor is blurred by the returning focus exactly as the header's was). Also gains one fork-only `createEffect` that consumes `consumeSessionShareRequest(sessionID())` and opens the share popover — the session rows' Share routes through it. | The sidebar's Recents rows and the home session list open the same menu; three copies of four items would drift. Only this file rendered the menu, so extraction is the smallest way to share it. | Keep the extraction. On conflict, take upstream's `message-timeline.tsx` wholesale and re-apply: delete both inline menu blocks and the local `DialogDeleteSession`, mount `<SessionActionsMenu>` with the same callbacks, drop the two `pending*` store fields, and re-add the share-request effect. If upstream adds a menu item, add it to `session-actions-menu.tsx` (both variants) rather than back here. Do not "simplify" the component's deferred rename/share into a plain `onSelect` — focus return is what it exists to survive. |
Expand Down
18 changes: 12 additions & 6 deletions packages/app/src/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -537,13 +537,19 @@ export function AppInterface(props: {
// The visual new layout lives in the router root so it remains mounted across
// route changes. Draft and session routes override only their server-bound data
// providers beneath it.
// No QueryProvider here: AppBaseProviders already installs one above, and a
// second client silently split the cache in two. ServerSync and its home
// session index resolve their client through GlobalProvider, which sits above
// this shell, while everything rendered under the router — the sidebar and
// home screen included — resolved the nested one. Session lifecycle events
// landed in a cache with no readers, so the sidebar's Recents list could only
// change when its own query refetched (switching Code/Work, which reassigns
// the observer's key, was the visible way to force that).
const ServerShell = (shellProps: ParentProps) => (
<QueryProvider>
<SharedProviders>
{props.children}
{shellProps.children}
</SharedProviders>
</QueryProvider>
<SharedProviders>
{props.children}
{shellProps.children}
</SharedProviders>
)

return (
Expand Down
Loading