Skip to content
Closed
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
2 changes: 1 addition & 1 deletion DIVERGENCES.md
Original file line number Diff line number Diff line change
Expand Up @@ -195,7 +195,7 @@ from the squash message first, or use a plain merge.
| `packages/desktop/scripts/finalize-latest-json.ts`, `packages/desktop/scripts/finalize-latest-yml.ts` | Deleted. | Both `gh release upload` into a GitHub release the fork does not use. `finalize-latest-yml.ts` merged per-arch `latest*.yml` from an upstream build matrix the fork does not run; `finalize-latest-json.ts` additionally built a Tauri-style `latest.json` signed with `@tauri-apps/cli signer` that nothing in the Electron updater path ever reads, and had gone stale besides — it hardcoded `opencode-desktop-mac-{x64,arm64}.app.tar.gz`, an artifact name and format this fork never produces. Leaving them in place is a trap: they look like the release path and upload to the wrong place. | Keep deleted (`git rm` the upstream side of modify/delete conflicts). If a future build matrix produces two `latest.yml` files for one platform (Windows x64 + arm64), write a bucket-targeted merge step rather than restoring these. |
| `packages/desktop/src/renderer/index.tsx` | Desktop notification icon `https://opencode.ai/favicon-96x96-v3.png` → the relative `./favicon-96x96-v3.png`. The renderer's `publicDir` is `packages/app/public`, whose `favicon-96x96-v3.png` symlinks to the rebranded `packages/ui/src/assets/favicon/` mark, so the file is emitted next to `index.html` and resolves against the renderer origin (`oc://renderer/` packaged, the vite dev server otherwise) — the same asset and the same relative form `src/renderer/index.html` already uses for `<link rel="icon">`. | A desktop app should not reach out to a remote host to draw its own notification icon (it fails offline and leaks a request per notification), and the remote file is upstream-branded while the bundled one is the codev mark. | Keep our relative path. If upstream changes the URL, re-point it at the bundled asset rather than taking a new remote host. `packages/app/src/entry.tsx` and `pages/layout/helpers.ts` keep their remote `opencode.ai/favicon*` sources — same bug class, not yet fixed. |
| `packages/desktop/scripts/prebuild.ts` | On macOS, after the node-sidecar build, runs `bun script/build-vzhelper.ts --universal` in `packages/codev-sandbox` so `dist/codev-vmm` exists (universal) before electron-builder copies it via `mac.extraResources`. | The sandbox vz backend spawns the helper from `process.resourcesPath/codev-vmm` in packaged builds; universal keeps one already-fat Mach-O valid for `--arm64`, `--x64`, and `--universal` packaging (an identical thin binary in both halves would trip `@electron/universal`'s identical-file check). | Keep the darwin-gated build line; take upstream's other steps. It must stay after nothing in particular but before packaging — prebuild is the hook `package:*` runs. |
| `packages/desktop/scripts/predev.ts` | Runs `bun run install-electron` before the existing icon copy and node-sidecar build. | `bun dev:desktop` was dead on a fresh clone: Electron 42 removed the `postinstall` hook that downloaded the platform binary and exposes it as an `install-electron` bin the consumer must call (electron 41's manifest still had `"postinstall": "node install.js"`; 42.3.3's has no `scripts` field at all), so `bun install` leaves `node_modules/electron` with no `dist/` and `electron-vite dev` aborts with `Error: Electron uninstall` from `getElectronPath`. `predev` is the right hook rather than the root `postinstall`: only someone actually launching the desktop app pays the ~100 MB download, and `install.js` short-circuits once `dist/version` and `path.txt` match, so repeat runs cost nothing. Not added to `prebuild`/`package` — `electron-vite build` never spawns Electron, and `electron-builder` fetches its own copy through `@electron/get`. | Keep our installer line at the top; take upstream's other steps. Drop the line if upstream adds its own binary-install step or pins Electron back below 42. |
| `packages/desktop/scripts/predev.ts` | Runs `bun run install-electron` before the existing icon copy and node-sidecar build, and `bun ./scripts/install-dev-desktop-entry.ts` (fork-only) after it. | `bun dev:desktop` was dead on a fresh clone: Electron 42 removed the `postinstall` hook that downloaded the platform binary and exposes it as an `install-electron` bin the consumer must call (electron 41's manifest still had `"postinstall": "node install.js"`; 42.3.3's has no `scripts` field at all), so `bun install` leaves `node_modules/electron` with no `dist/` and `electron-vite dev` aborts with `Error: Electron uninstall` from `getElectronPath`. `predev` is the right hook rather than the root `postinstall`: only someone actually launching the desktop app pays the ~100 MB download, and `install.js` short-circuits once `dist/version` and `path.txt` match, so repeat runs cost nothing. Not added to `prebuild`/`package` — `electron-vite build` never spawns Electron, and `electron-builder` fetches its own copy through `@electron/get`. The desktop-entry line fixes the dev app's Linux icon: `BrowserWindow`'s `icon` only reaches the shell through X11's `_NET_WM_ICON`, and Electron 42 runs as a native Wayland client on GNOME, where the icon comes from the .desktop file matching the window's app_id — so an unpackaged run shows the generic executable icon while macOS (`app.dock.setIcon`) and Windows (the HWND icon) are fine. Electron slugifies package.json `productName ?? name` into that app_id before main runs (`@opencode-ai/desktop` → `opencode-ai-desktop`); `app.setName()`, `--class` and `CHROME_DESKTOP` were all measured not to override it, so the script writes the matching entry instead. | Keep our installer line at the top; take upstream's other steps. Drop the line if upstream adds its own binary-install step or pins Electron back below 42. Keep the desktop-entry line after `copy-icons` — the entry points at the icon that step stages — and drop it only if the desktop package's `name`/`productName` stops deciding the app_id. |
| `packages/ui/src/assets/favicon/` (favicon-v3.svg, favicon-v3.ico, site.webmanifest, favicon-96x96-v3.png, apple-touch-icon-v3.png, web-app-manifest-192/512.png) | New codev mark: the blocky "C" in brand red `#ee0033` on a white tile, generated from the same geometry as the app icon by `packages/desktop/scripts/generate-icons.ts` (4x5 grid, one-unit stroke, unit = 120/1024, corner radius 229/1024); `favicon-v3.svg` is the hand-written 512-unit source of that geometry. The tab and `.ico` icons keep the rounded tile so they read as an app icon; `apple-touch-icon-v3.png` and the two maskable `web-app-manifest-*` icons are square and full-bleed, because iOS and PWA launchers apply their own mask and a pre-rounded icon leaves transparent corners that render black. Manifest name "CoDev Code"/"codev"; its `theme_color`/`background_color` stay `#080808` — those describe the PWA's chrome and splash, not the icon. **Was `#00cdcd` on a dark tile** until this row was rewritten: PR #10 pinned the favicon to the then-cyan TUI banner accent, that banner later moved to the navy/red wordmark, and the favicon was left behind as the only cyan in the tree — visibly so on desktop, where the dock icon was red while the window and notification icons were cyan. | Every surface that draws the mark should draw the same mark: the dock icon, the tab, notifications, the `codev --mini` badge and the landing page icon are now one geometry in one red. Cyan on `#131010` was the higher-contrast tile at 16px (~9.4:1 versus red's ~4.2:1) — a real cost, accepted so the brand reads consistently. | Keep our assets (binary: take ours wholesale). Regenerate with the script rather than hand-editing, and edit `icons/icon.svg` + `favicon-v3.svg` together if the geometry ever changes. |
| `AGENTS.md` | Default-branch note: `dev` → `main` (single merged line; upstream has two lines saying local `main` may not exist); added fork bullets requiring every upstream-file change to get a `DIVERGENCES.md` row (and matching en + es doc updates) in the same PR, and pointing agents at this file's Divergence policy (prefer config/plugins, then `packages/codev-*`, upstream edits last) and Syncing with upstream sections | The fork's default branch is `main` (see the workflow-triggers row); agents kept basing diffs on a nonexistent `dev`. Agents edit upstream files without recording the divergence — and don't apply the placement policy or sync runbook — unless the instructions file says to. | Keep our lines; take upstream's other guidance. |
| `README.md` | Replaced with fork README | Fork identity, sync + divergence policy. | Keep ours entirely. |
Expand Down
20 changes: 20 additions & 0 deletions packages/desktop/scripts/install-dev-desktop-entry.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { describe, expect, test } from "bun:test"
import { electronAppId } from "./install-dev-desktop-entry"

describe("electronAppId", () => {
// Measured against electron 42 on wayland (WAYLAND_DEBUG=1, reading
// xdg_toplevel.set_app_id): these are the exact slugs it announces.
test("slugifies the package name the way electron does", () => {
expect(electronAppId({ name: "@opencode-ai/desktop" })).toBe("opencode-ai-desktop")
expect(electronAppId({ name: "ai.opencode.desktop.dev" })).toBe("ai-opencode-desktop-dev")
expect(electronAppId({ name: "appid-probe" })).toBe("appid-probe")
})

test("prefers productName", () => {
expect(electronAppId({ productName: "CoDev Code Dev", name: "@opencode-ai/desktop" })).toBe("codev-code-dev")
})

test("rejects a manifest with nothing to slugify", () => {
expect(() => electronAppId({ name: "///" })).toThrow()
})
})
61 changes: 61 additions & 0 deletions packages/desktop/scripts/install-dev-desktop-entry.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
// Fork: gives the unpackaged dev app an icon on Linux desktops.
//
// BrowserWindow's `icon` reaches the shell through X11's _NET_WM_ICON, which
// Wayland has no equivalent for (xdg_toplevel_icon_v1 is new and Chromium does
// not speak it). Wayland shells instead resolve a window's icon by matching its
// app_id to an installed .desktop file, so on GNOME/Wayland an unpackaged run
// falls back to the generic executable icon — while macOS (app.dock.setIcon)
// and Windows (the HWND icon) are unaffected.
//
// Packaged builds ship that .desktop file via electron-builder; this writes the
// dev equivalent into the user's applications dir. Idempotent, so predev can
// run it on every launch.
import { mkdir } from "node:fs/promises"
import { homedir } from "node:os"
import { join, resolve } from "node:path"
import { resolveChannel } from "./utils"

const CHANNEL_NAMES = { dev: "CoDev Code Dev", beta: "CoDev Code Beta", prod: "CoDev Code" } as const

// Electron announces a slug of the app's package.json productName (falling back
// to name) as the Wayland app_id and the X11 WM_CLASS: lowercased, with every
// run of non-alphanumerics collapsed to a dash. Neither app.setName() nor
// --class nor CHROME_DESKTOP overrides it — it is read before main runs — so
// the entry has to be named after whatever the manifest yields.
export function electronAppId(manifest: { productName?: string; name?: string }) {
const source = manifest.productName ?? manifest.name ?? ""
const slug = source
.toLowerCase()
.replace(/[^a-z0-9]+/g, "-")
.replace(/^-+|-+$/g, "")
if (!slug.length) throw new Error("could not derive an app id from the desktop package manifest")
return slug
}

// import.meta.main keeps importing this module (the test does) side-effect free.
if (import.meta.main && process.platform === "linux") {
const packageDir = resolve(import.meta.dir, "..")
const appId = electronAppId(await Bun.file(join(packageDir, "package.json")).json())
const applications = join(process.env.XDG_DATA_HOME ?? join(homedir(), ".local", "share"), "applications")
await mkdir(applications, { recursive: true })
const entry = join(applications, `${appId}.desktop`)
// NoDisplay keeps it out of the launcher — it exists only so the shell can
// associate a running dev window with a name and an icon. StartupWMClass
// covers the X11 side, where the match is on WM_CLASS rather than the
// filename. The icon is the channel copy predev already staged, so this
// tracks whichever channel was last built.
await Bun.write(
entry,
`[Desktop Entry]
Type=Application
Name=${CHANNEL_NAMES[resolveChannel()]}
Comment=Open source AI coding agent (development build)
Icon=${join(packageDir, "resources", "icons", "icon.png")}
Exec=bun --cwd ${packageDir} dev
StartupWMClass=${appId}
NoDisplay=true
Categories=Development;
`,
)
console.log(`Installed dev desktop entry for app id ${appId} at ${entry}`)
}
4 changes: 4 additions & 0 deletions packages/desktop/scripts/predev.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,8 @@ await $`bun run install-electron`

await $`bun ./scripts/copy-icons.ts ${process.env.CODEV_CHANNEL ?? "dev"}`

// Must follow copy-icons: the entry points at the icon it stages. Linux-only,
// and a no-op everywhere else.
await $`bun ./scripts/install-dev-desktop-entry.ts`

await $`cd ../opencode && bun script/build-node.ts`
Loading