diff --git a/DIVERGENCES.md b/DIVERGENCES.md
index 9594066b33..1bbc1c3e7a 100644
--- a/DIVERGENCES.md
+++ b/DIVERGENCES.md
@@ -158,7 +158,7 @@ from the squash message first, or use a plain merge.
| `packages/desktop/src/main/index.ts`, `packages/desktop/src/main/windows.ts` | APP_NAMES + window title → "CoDev Code" (Dev/Beta variants too) | Desktop app display name. | Keep our names. |
| `packages/desktop/electron-builder.config.ts` | `productName` ×3 → "CoDev Code…"; artifactName → `codev-code-desktop-…`; protocol display name. Auto-update `publish` targets repointed to the fork: beta `anomalyco/opencode-beta` and prod `anomalyco/opencode` both → `quickbeard/codev-code` (upstream has no fork-side beta repo, so the two channels share one). | Installer/product branding, and a packaged build must not offer upstream OpenCode releases as CoDev updates. appId (`ai.opencode.desktop`) and the `opencode://` deep-link scheme stay upstream — identity plumbing wired into main/index.ts and app deep-links; do not rename casually. `rpm.packageName` (`opencode`/`opencode-beta`/`opencode-dev`) also stays — it names the installed Linux package, not a publish target. | Keep our names and publish targets; take upstream's other config. Two known follow-ups, both moot while the fork ships no desktop build: beta and prod now write the same `latest.yml` in one repo (splitting them needs `publish.channel` plus the hardcoded `autoUpdater.channel = "latest"` in `src/main/updater.ts`), and `codev-code` is private, which electron-updater cannot read without a token. |
| `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 ``. | 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/ui/src/assets/favicon/` (favicon-v3.svg, site.webmanifest, favicon-96x96-v3.png, apple-touch-icon-v3.png, web-app-manifest-192/512.png) | New codev mark: blocky "C" in #00cdcd on dark; manifest name "CoDev Code"/"codev" | Web/desktop favicon + PWA branding (served via packages/app/public symlinks). | Keep our assets (binary: take ours wholesale). |
+| `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. |
| `CONTRIBUTING.md`, `SECURITY.md` | Product-brand prose "OpenCode" → "CoDev Code"; built-CLI command examples `opencode …` → `codev …` | Docs people actually read should name the fork's product. Upstream issue/advisory links, `packages/opencode` paths, and build-output paths stay upstream; env-var mentions follow the env-var rename row. | Keep our strings; take upstream's structural changes. |
diff --git a/packages/desktop/icons/README.md b/packages/desktop/icons/README.md
index a11c05fe3b..69f09e4aee 100644
--- a/packages/desktop/icons/README.md
+++ b/packages/desktop/icons/README.md
@@ -12,9 +12,18 @@ The three channels are currently identical — the same red C on white ships as
dev, beta and prod icon. `generate-icons.ts` still writes each channel separately,
so giving a channel its own treatment later is a change to that script alone.
+The same run also regenerates the web and PWA favicon set in
+`packages/ui/src/assets/favicon/`, whose `favicon-v3.svg` is the hand-written 512-unit
+copy of this geometry. One generator for both sets is deliberate: the dock icon and the
+favicon drew different marks in different colors for a while, which is the drift a
+shared source prevents. The tab and `.ico` icons keep the rounded tile; the
+apple-touch and maskable manifest icons are square, because those platforms apply
+their own mask and a pre-rounded icon leaves transparent corners that render black.
+
The generator rasterizes, encodes the PNGs and builds the `.ico` itself, so the only
-external tool it needs is `iconutil` for `.icns` — which means it has to run on macOS.
-This replaces the old Tauri-CLI + [Image2Icon](https://img2icnsapp.com/) workflow;
+external tool it needs is `iconutil` for `.icns`. That one is macOS-only, so off macOS
+the script skips `.icns` (leaving the committed file alone) and regenerates everything
+else. This replaces the old Tauri-CLI + [Image2Icon](https://img2icnsapp.com/) workflow;
the Big Sur inset and drop shadow that step used to add are now applied directly to
the `.icns` members and to `dock.png` (824/1024 rounded square), while the Windows,
Linux and Store assets stay full-bleed.
diff --git a/packages/desktop/scripts/generate-icons.ts b/packages/desktop/scripts/generate-icons.ts
index ba4183b7a8..0752c3ba8c 100644
--- a/packages/desktop/scripts/generate-icons.ts
+++ b/packages/desktop/scripts/generate-icons.ts
@@ -1,5 +1,10 @@
-// Fork: regenerates every app icon in icons/{dev,beta,prod}/ from the geometry
-// in icons/icon.svg — the brand-red CoDev "C" on a white tile.
+// Fork: regenerates every app icon in icons/{dev,beta,prod}/ — and the web/PWA
+// favicon set in packages/ui/src/assets/favicon/ — from the geometry in
+// icons/icon.svg: the brand-red CoDev "C" on a white tile.
+//
+// Both sets come from this one script on purpose. They drew different marks in
+// different colors for a while (dock icon red, favicon cyan), which is exactly
+// the drift a shared generator prevents.
//
// Replaces the manual Tauri-CLI + Image2Icon workflow the old icons/README.md
// described. Everything here is self-contained (raw rasterizer, PNG encoder, ICO
@@ -21,14 +26,17 @@ const CORNER = 229 / 1024
// shadow; anything full-bleed reads as oversized next to native icons.
const MACOS_TILE = 824 / 1024
-type Style = "flat" | "macos"
+// "square" is full-bleed with no corner radius: iOS rounds apple-touch-icon
+// itself and a pre-rounded one leaves transparent corners that render black,
+// and maskable PWA icons are cropped by the launcher for the same reason.
+type Style = "flat" | "macos" | "square"
// Coverage of the rounded rect and of the "C" for one pixel, supersampled.
function coverage(size: number, style: Style) {
const samples = size <= 256 ? 8 : 4
const tile = size * (style === "macos" ? MACOS_TILE : 1)
const origin = (size - tile) / 2
- const radius = tile * CORNER
+ const radius = style === "square" ? 0 : tile * CORNER
const unit = tile * UNIT
const mark = {
x: origin + (tile - unit * 4) / 2,
@@ -227,6 +235,19 @@ const FLAT_PNGS = [
const ICO_SIZES = [16, 24, 32, 48, 64, 128, 256]
+// The web + PWA favicon set, served out of packages/app/public through symlinks.
+// favicon-v3.svg next to these is the hand-written source of the same geometry.
+// The tab/`.ico` icons keep the rounded tile so they read as an app icon; the
+// apple-touch and maskable manifest icons are square, since their platforms
+// apply their own mask.
+const FAVICON_DIR = "../ui/src/assets/favicon"
+const FAVICON_PNGS: ReadonlyArray = [
+ ["favicon-96x96-v3.png", 96, "flat"],
+ ["apple-touch-icon-v3.png", 180, "square"],
+ ["web-app-manifest-192x192.png", 192, "square"],
+ ["web-app-manifest-512x512.png", 512, "square"],
+]
+
// Apple's required iconset members: {logical size, @2x?}.
const ICONSET = [16, 32, 128, 256, 512].flatMap((n) => [
{ name: `icon_${n}x${n}.png`, size: n },
@@ -250,12 +271,24 @@ for (const channel of ["dev", "beta", "prod"]) {
await Bun.write(`${dir}/icon.ico`, ico(ICO_SIZES.map((size) => ({ size, data: flat.get(size)! }))))
await Bun.write(`${dir}/dock.png`, macos.get(256)!)
- const iconset = `${dir}/icon.iconset`
- await rm(iconset, { recursive: true, force: true })
- await mkdir(iconset, { recursive: true })
- await Promise.all(ICONSET.map((entry) => Bun.write(`${iconset}/${entry.name}`, macos.get(entry.size)!)))
- await $`iconutil -c icns -o ${dir}/icon.icns ${iconset}`
- await rm(iconset, { recursive: true, force: true })
+ // iconutil ships with macOS only, so everything else regenerates on any
+ // platform and the committed .icns is left as is.
+ if (process.platform === "darwin") {
+ const iconset = `${dir}/icon.iconset`
+ await rm(iconset, { recursive: true, force: true })
+ await mkdir(iconset, { recursive: true })
+ await Promise.all(ICONSET.map((entry) => Bun.write(`${iconset}/${entry.name}`, macos.get(entry.size)!)))
+ await $`iconutil -c icns -o ${dir}/icon.icns ${iconset}`
+ await rm(iconset, { recursive: true, force: true })
+ }
- console.log(`Generated ${channel} icons in ${dir}`)
+ console.log(
+ `Generated ${channel} icons in ${dir}${process.platform === "darwin" ? "" : " (icon.icns skipped, needs macOS)"}`,
+ )
}
+
+await Promise.all(
+ FAVICON_PNGS.map(([name, size, style]) => Bun.write(`${FAVICON_DIR}/${name}`, png(render(size, style), size))),
+)
+await Bun.write(`${FAVICON_DIR}/favicon-v3.ico`, ico(ICO_SIZES.map((size) => ({ size, data: flat.get(size)! }))))
+console.log(`Generated favicons in ${FAVICON_DIR}`)
diff --git a/packages/ui/src/assets/favicon/apple-touch-icon-v3.png b/packages/ui/src/assets/favicon/apple-touch-icon-v3.png
index 0d398245ac..107c4126c6 100644
Binary files a/packages/ui/src/assets/favicon/apple-touch-icon-v3.png and b/packages/ui/src/assets/favicon/apple-touch-icon-v3.png differ
diff --git a/packages/ui/src/assets/favicon/favicon-96x96-v3.png b/packages/ui/src/assets/favicon/favicon-96x96-v3.png
index 2af0e7f87f..ca3a5d76fd 100644
Binary files a/packages/ui/src/assets/favicon/favicon-96x96-v3.png and b/packages/ui/src/assets/favicon/favicon-96x96-v3.png differ
diff --git a/packages/ui/src/assets/favicon/favicon-v3.ico b/packages/ui/src/assets/favicon/favicon-v3.ico
index 34ca0b9c01..07e53246bc 100644
Binary files a/packages/ui/src/assets/favicon/favicon-v3.ico and b/packages/ui/src/assets/favicon/favicon-v3.ico differ
diff --git a/packages/ui/src/assets/favicon/favicon-v3.svg b/packages/ui/src/assets/favicon/favicon-v3.svg
index aba6d7787e..e2f166f66a 100644
--- a/packages/ui/src/assets/favicon/favicon-v3.svg
+++ b/packages/ui/src/assets/favicon/favicon-v3.svg
@@ -1,6 +1,18 @@
-