diff --git a/DIVERGENCES.md b/DIVERGENCES.md index 7208d8ceb2..e7e26d01a6 100644 --- a/DIVERGENCES.md +++ b/DIVERGENCES.md @@ -125,6 +125,7 @@ from the squash message first, or use a plain merge. | `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 `` 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 `` 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. | | `packages/app/src/pages/home.tsx` | `HomeSessionRow` gains the same `` (hover/focus/menu-open revealed, vertical dots) and inline rename: while renaming, the row renders a `div` with `InlineEditor` instead of the `button`, because an input inside a button is invalid markup and swallows keystrokes into the open-session handler. `NewHome` holds one `createInlineEditorController()` for the list and passes it plus the model down. Its local `archiveSession` is deleted — `createHomeModel()` owns `archiveSession`/`renameSession`/`deleteSession` now, so the sidebar and this screen run one implementation (extends the "state both screens need moves to home-model.tsx" split in the persistent-sidebar row above); the call site becomes `model.archiveSession`. Upstream's `SHOW_HOME_SESSION_ARCHIVE` block is left exactly as-is — the flag is `false`, so it renders nothing, and touching it would widen the diff for no gain. `data-component="home-session-row"` and every other selector are untouched (`packages/app/e2e` binds to them); the menu adds `data-action="session-menu"`. | The Recents rows and "View all" are the same list at two sizes; a menu on one and not the other makes "View all" a dead end. | Keep our row structure. On conflict take upstream's row and re-apply the menu, the editing branch, and the `model.*` action wiring. Never let a `data-component`/`data-action` hook drift. If upstream re-enables `SHOW_HOME_SESSION_ARCHIVE`, drop that block instead — its absolutely-positioned archive button would sit on top of the menu. | | `packages/app/src/pages/home.tsx` (again) | **Session lists keyed by id.** The "View all" list iterates `groups().map((group) => group.id)` and, within each group, `group.sessions.map((record) => record.session.id)`, resolving each back through a `createMemo` + `` rather than handing the objects to ``. `groupSessions` mints new group objects on every call and `createHomeModel`'s records memo mints a new `{session, project, projectName}` wrapper per session on every recompute — the `projects` memo alone returns a fresh array on a pointerdown — and `` keys on reference, so the rows were torn down and rebuilt on recomputes that changed nothing visible. When that landed between a pointerdown and its mouseup the browser had no surviving common ancestor to fire `click` on, so a row's ⋮ menu ignored its first click; the same rebuild could close an open menu or drop an in-progress rename. `data-component="home-session-row"`, `data-action="session-menu"` and the group header refs / opacity keys are unchanged. The fork-only `packages/app/src/components/app-sidebar.tsx` carries the same shape for the Recents list. | A menu that swallows the first click reads as broken, and the cause is structural rather than incidental: any refresh of the session index rebuilt every row. | Keep the id keying. On conflict take upstream's list body and re-apply both `` wrappers. Do not "simplify" back to `` / `` — that is the bug. If upstream ever makes groups or records referentially stable this can go, but prove it with `packages/app/e2e/regression/home-session-menu-first-click.spec.ts` before removing it. | +| `packages/app/e2e/regression/remote-session-settings.spec.ts` | Both tests pick the settings-shortcut modifier by asking the page — `/(Mac|iPod|iPhone|iPad)/.test(navigator.platform)` — through a local `openSettingsDialog(page)` helper, instead of branching on the Node-side `process.platform`. | Upstream's version pressed `Meta+,` whenever the *host* was macOS, but the app resolves `mod` from `navigator.platform` (`IS_MAC` in `src/context/command.tsx`) and Playwright's `devices["Desktop Chrome"]` profile ships a Windows user agent, so the page always reports `Win32`. On a macOS machine the suite therefore pressed Meta while the app listened for Control: the settings dialog never opened and both tests failed, while Linux CI stayed green because there the host branch happened to pick Control anyway. A real macOS browser reports `MacIntel`, so the app is right and only the test was wrong. | Keep our helper. On conflict take upstream's spec and re-apply it. If upstream fixes this itself, drop the row. Worth submitting upstream — it breaks the suite for every macOS developer. | | `packages/desktop/icons/{dev,beta,prod}/` (all PNGs, `icon.icns`, `icon.ico`, `dock.png`), `packages/desktop/icons/README.md` | Regenerated from the fork-only `packages/desktop/icons/icon.svg` by the fork-only `packages/desktop/scripts/generate-icons.ts`: the brand-red `#ee0033` CoDev "C" — the `--mini` splash badge glyph, at the VS Code extension icon's proportions (unit = 120/1024, mark 480x600 centered) — on a white tile, replacing upstream's black/blueprint "o" mark. All three channels are byte-identical for now (chosen deliberately over upstream's per-channel treatments); the generator still writes them separately so a channel can diverge later. `icon.icns` members and `dock.png` use the macOS Big Sur grid (824/1024 inset + drop shadow); Windows, Linux and Store assets stay full-bleed. README rewritten from the Tauri-CLI + Image2Icon recipe to the generator. The stale Tauri-era `android/` and `ios/` subfolders are left untouched — nothing in the Electron build reads them. | Closes the "desktop app icons need design assets" rebrand gap: the Dock/taskbar icon was still upstream's mark. | Keep our assets and README (binary: take ours wholesale). Regenerate via the script rather than hand-editing; if upstream adds an icon size, add it to `FLAT_PNGS`/`ICONSET` and rerun. | | `packages/tui/src/theme/assets/codev.json` (new file), `packages/tui/src/theme/index.ts`, `packages/tui/src/context/theme.tsx` | Added the `codev` theme — upstream's `opencode.json` palette with `darkSecondary` → `#0891b2` (brand teal) — registered it in the theme index, and made it the default/fallback theme (`"opencode"` → `"codev"` in the store default, kv fallback, and load-failure fallbacks). `theme/assets/opencode.json` itself matches upstream again. | Brand accent lives in a fork-owned theme instead of edits to upstream's palette; the default theme carries the brand. | Keep our theme file, its index registration, and the fallback strings; when upstream changes the opencode palette, mirror it into `codev.json` (keeping our `darkSecondary`). | | `packages/opencode/src/cli/ui.ts` | `wordmark`/`logo()` render the lowercase codev wordmark via `ansiWordmark` (bold, no shadow, "dev" in brand red on TTY) | Help banner matches the TUI home banner. | Keep our wordmark + logo(); take upstream's surrounding changes. | diff --git a/packages/app/e2e/regression/remote-session-settings.spec.ts b/packages/app/e2e/regression/remote-session-settings.spec.ts index c17ae5c1c6..2669c5badb 100644 --- a/packages/app/e2e/regression/remote-session-settings.spec.ts +++ b/packages/app/e2e/regression/remote-session-settings.spec.ts @@ -10,6 +10,18 @@ const sessionA = session("ses_server_a", directoryA, "Server A session") const childSessionA = { ...session("ses_server_a_child", directoryA, "Server A child session"), parentID: sessionA.id } const sessionB = session("ses_server_b", directoryB, "Server B session") +// The app resolves `mod` from `navigator.platform` (see IS_MAC in +// src/context/command.tsx), and Playwright's Desktop Chrome profile ships a +// Windows user agent, so the page reports Win32 whatever the host is. Choosing +// the modifier from `process.platform` instead pressed Meta on a macOS machine +// while the app was listening for Control, so the settings dialog never opened +// and both tests here failed — on macOS only, which is why Linux CI stayed +// green. Ask the page the same question the app asks. +async function openSettingsDialog(page: Page) { + const mac = await page.evaluate(() => /(Mac|iPod|iPhone|iPad)/.test(navigator.platform)) + await page.keyboard.press(mac ? "Meta+," : "Control+,") +} + test("session settings use the remote server context", async ({ page }) => { const permissionRequests: string[] = [] await mockServers(page, permissionRequests) @@ -17,7 +29,7 @@ test("session settings use the remote server context", async ({ page }) => { await page.goto(`/server/${base64Encode(serverB)}/session/${sessionB.id}`) await expect(page.getByText(sessionB.title).first()).toBeVisible() - await page.keyboard.press(process.platform === "darwin" ? "Meta+," : "Control+,") + await openSettingsDialog(page) const dialog = page.locator(".settings-v2-dialog") const autoAccept = dialog.locator('[data-action="settings-auto-accept-permissions"]') @@ -58,7 +70,7 @@ test("auto-accept responds for an unfocused server session", async ({ page }) => const hrefB = `/server/${base64Encode(serverB)}/session/${sessionB.id}` await page.goto(`/server/${base64Encode(serverA)}/session/${sessionA.id}`) await expect(page.getByText(sessionA.title).first()).toBeVisible() - await page.keyboard.press(process.platform === "darwin" ? "Meta+," : "Control+,") + await openSettingsDialog(page) const autoAccept = page.locator(".settings-v2-dialog").locator('[data-action="settings-auto-accept-permissions"]') await autoAccept.locator('[data-slot="switch-control"]').click() await expect(autoAccept.getByRole("switch")).toBeChecked()