From 6d24a22eeb953118ece229c0d6f369272f9bca0d Mon Sep 17 00:00:00 2001 From: minhn4 Date: Tue, 21 Jul 2026 07:50:54 +0700 Subject: [PATCH 1/4] feat(cli): badge the --mini splashes with the CoDev mark MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The `codev --mini` entry and exit splashes badged with the "o" of the `go` mark inherited from upstream. Add a `badge` export to the logo module — the "c" of the CoDev wordmark, derived from `logo.left` so it tracks the wordmark — and point both splash branches at it. `go` is left untouched: the background pulse (bg-pulse-render.ts) still draws the full "go" wordmark from it, so editing `go.right` in place would have altered that animation too. Co-Authored-By: Claude Opus 4.8 (1M context) --- CHANGELOG.md | 6 ++++++ DIVERGENCES.md | 3 ++- packages/opencode/src/cli/cmd/run/splash.ts | 8 ++++---- packages/tui/src/logo.ts | 5 +++++ 4 files changed, 17 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 85c74433a4..fff25e79a3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,12 @@ file is the human-readable summary of what changed and why. ## Unreleased +### Branding + +- **`codev --mini` splashes carry the CoDev mark** — the compact badge on the + entry and exit banners is now the `c` of the CoDev wordmark instead of the + inherited `o`. The background pulse's `go` wordmark is unchanged. + ### Sessions - **`Ctrl+C` interrupts a running turn** — while the agent is working, `Ctrl+C` diff --git a/DIVERGENCES.md b/DIVERGENCES.md index 9096aaf234..6f5bde6767 100644 --- a/DIVERGENCES.md +++ b/DIVERGENCES.md @@ -85,7 +85,7 @@ from the squash message first, or use a plain merge. | `packages/opencode/script/publish.ts` | `PUBLISH_NAME = "codev-code"` const; published-name→dir map; main pkg name + bin `codev` → `./bin/codev.exe` (postinstall stub file renamed from `${pkg.name}.exe`, matching the renamed platform binary); embedded README; env-guarded exit before the Docker/AUR/Homebrew section | npm-only publishing under the fork's names (unscoped, public on npmjs); the installed command is `codev`; upstream's other channels target anomalyco infra. | Keep our names, description, bin mapping + early exit; take upstream's npm-section changes above the exit. | | `packages/opencode/bin/opencode` | `base` prefix `opencode-` → `codev-code-` | Runtime platform-binary lookup must match the published names. | Keep our prefix. | | `packages/opencode/script/postinstall.mjs` | `base` prefix `opencode-` → `codev-code-`; source/target binary names `opencode`/`opencode.exe` → `codev`/`codev.exe` (matches build.ts's `bin/codev` and publish.ts's `bin/codev.exe` stub); temp-dir prefix and failure message rebranded | Install-time platform-binary fetch must match the published package and binary names. | Keep our prefix and binary names. | -| `packages/tui/src/logo.ts` | Pixel glyphs "open"+"code" → "co"+"dev"; added composed `wordmark` (plain lines) and `ansiWordmark` (bold brand teal + drop shadow) exports for CLI banners | Branded wordmark art; `ansiWordmark` feeds the CLI help banner and session epilogue. | Keep our glyphs + exports; take upstream's surrounding changes. | +| `packages/tui/src/logo.ts` | Pixel glyphs "open"+"code" → "co"+"dev"; added composed `wordmark` (plain lines) and `ansiWordmark` (bold brand teal + drop shadow) exports for CLI banners; added a `badge` export (the "c" of the wordmark — `logo.left` rows 1-3 sliced to the first glyph) for the `--mini` splashes | Branded wordmark art; `ansiWordmark` feeds the CLI help banner and session epilogue. | Keep our glyphs + exports; take upstream's surrounding changes. | | `packages/tui/src/component/logo.tsx` | Renders the lowercase codev wordmark in the theme's `secondary` color (bold, with a `tint`-derived drop shadow) instead of upstream's two-tone "open"+"code" | Home banner shows the brand wordmark in the active theme's brand color (teal in dark, purple in light). | Keep our version wholesale. | | `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 brand teal + drop shadow on TTY) | Help banner matches the TUI home banner. | Keep our wordmark + logo(); take upstream's surrounding changes. | @@ -103,6 +103,7 @@ from the squash message first, or use a plain merge. | `packages/tui/src/feature-plugins/system/which-key.tsx` | `HomeHint` reads the new `placement` prop from the `home_bottom` slot; with `"start"` it left-aligns | Same top-left banner alignment as the tips row above. | Keep our `placement` threading + conditional alignItems; take upstream's surrounding changes. | | `packages/tui/src/routes/home.tsx` | `placeholder` suggestion pool exported (was module-private) | The empty-session view in routes/session/index.tsx feeds the same pool to its prompt, so upstream edits to the suggestions flow through. | Keep the `export` keyword; take upstream's changes to the list. | | `packages/tui/src/routes/session/permission.tsx`, `packages/opencode/src/cli/cmd/run/permission.shared.ts`, `packages/opencode/test/cli/run/permission.shared.test.ts`, `packages/tui/src/component/error-component.tsx`, `packages/opencode/src/cli/cmd/run/splash.ts` | Visible sentences/badges "OpenCode"/"opencode" → "CoDev Code"/"codev-code" (incl. the permission always-allow copy and its test expectations); permission.tsx also registers a local `app.exit.confirm` command mirroring its `app.exit` reject and gathers the confirm binding (double-press Ctrl+C — see the double-Ctrl+C row) | User-facing copy; keep Ctrl+C rejecting the permission prompt as before. | Keep our strings; keep permission.tsx's `app.exit.confirm` command + binding. | +| `packages/opencode/src/cli/cmd/run/splash.ts` | The `--mini` entry and exit splashes badge with `badge` (the "c" of the codev wordmark) instead of `go.right.slice(1)` (the "o" of the `go` mark) — the import and both `const mark = …` lines; header comment "full opencode entry logo and a compact [O] exit badge" → "full codev entry logo and a compact [C] exit badge" | The splash badge is the brand mark users see entering and leaving `codev --mini`; an "o" is upstream's. `go` itself is untouched — `packages/tui/src/component/bg-pulse-render.ts` still draws the full "go" wordmark from it. | Keep the `badge` import and both `mark` assignments; take upstream's surrounding splash changes. If upstream restructures the badge rendering, feed it `badge` rather than reverting to `go`. | | `packages/tui/src/config/keybind.ts`, `packages/tui/src/routes/session/question.tsx`, `packages/tui/src/ui/toast.tsx`, `packages/tui/test/app-lifecycle.test.tsx`, `packages/web/src/content/docs/keybinds.mdx` (+ `es/`) | Double-press Ctrl+C to quit. keybind.ts: `app_exit` `ctrl+c,ctrl+d,q` → `ctrl+d,q`, added `app_exit_confirm: "ctrl+c"` definition + its `app.exit.confirm` CommandMap entry. question.tsx: a local `app.exit.confirm` command mirroring its `app.exit` reject, with the confirm binding gathered in both option branches (Ctrl+C still rejects the question — single press). toast.tsx: `ToastOptions.variant` union gains `"secondary"` (resolves to `theme.secondary`, the banner brand color, for the confirm toast). app-lifecycle.test.tsx: added the "app.exit.confirm requires two presses to exit" test. Docs (en + es `keybinds.mdx`): split the `app_exit` default and added `app_exit_confirm: "ctrl+c"`. See also the `app.tsx`, `dialog.tsx`, and `permission.tsx` rows for their `app.exit.confirm` handlers. | A stray Ctrl+C on an empty/unfocused prompt no longer quits instantly: the first press shows a "Press Ctrl+C again to exit" toast (1.5 s window), the second within that window quits. Ctrl+D, `q`, and the `/exit` slash commands stay immediate. | Keep our binding split, the `app_exit_confirm`/`app.exit.confirm` definition + mapping, the local confirm commands + gathered bindings, the toast `"secondary"` variant, the test, and the doc defaults; take upstream's surrounding changes. If upstream re-adds `ctrl+c` to `app_exit`, keep it removed. | | `packages/tui/src/config/keybind.ts`, `packages/web/src/content/docs/keybinds.mdx` (+ `es/`), `packages/opencode/test/cli/run/footer.view.test.tsx` | `sidebar_toggle` default `"b"` → `"ctrl+b,b"` (ctrl+b first so single-binding displays — the prompt-footer sidebar hint, the sidebar tip — resolve to it); `ctrl+b`'s former jobs move to `ctrl+o`: `input_move_left` `"left,ctrl+b"` → `"left,ctrl+o"`, `session_background` `"ctrl+b"` → `"ctrl+o"`; docs defaults updated to match (docs don't list `session_background`); footer.view.test.tsx asserts the run-CLI footer background hint as `"ctrl+o background"` (was `"ctrl+b background"`) since it resolves the real `session_background` default | Single-chord sidebar toggle on `ctrl+b` (tmux/VS Code sidebar muscle memory); its displaced bindings land on `ctrl+o`, which was unbound in every scope and free of terminal-encoding collisions (unlike `ctrl+h`/`ctrl+i`/`ctrl+m`) and flow control (`ctrl+s`/`ctrl+q`). | Keep our defaults on all three keybinds, the doc lines, and the footer hint assertion; take upstream's surrounding changes. If upstream binds `ctrl+o` to something else, re-audit and pick a new free key. | | `packages/tui/src/config/keybind.ts`, `packages/tui/src/component/prompt/index.tsx`, `packages/tui/src/app.tsx`, `packages/web/src/content/docs/keybinds.mdx` (+ `es/`) | Single-press Ctrl+C interrupt. keybind.ts: added `session_interrupt_now: "ctrl+c"` + its `session.interrupt.now` CommandMap entry (`session_interrupt` keeps `escape`; its description gains "(press twice)"). prompt/index.tsx: added the `session.interrupt.now` command — same guards as upstream's `session.interrupt` but it aborts on the first press, and its `enabled` requires a busy session AND an empty prompt so ctrl+c falls through to `prompt.clear` (draft present) or `app.exit.confirm` (idle) instead of racing them on layer order; gathered the command in the `prompt.palette` binding list; the busy footer hint reads `interruptShortcut()` (resolves `session.interrupt`, mapping `escape` → `esc` and falling back to the `session.interrupt.now` label when escape is unbound) instead of the hardcoded `esc`. app.tsx: split `app_exit_confirm` out of the shared exit `useBindings` into its own layer whose `enabled` also requires `sync.data.session_status[route.data.sessionID]` to be idle, so ctrl+c can't arm the quit confirm mid-turn (`app_exit` keeps the shared predicate). Docs (en + es): added the `session_interrupt_now` default. Covered by the fork-only `packages/tui/test/interrupt.view.test.tsx`. See also the double-Ctrl+C row above, whose scope this narrows. | Ctrl+C is what many users reach for to stop a running agent, so it interrupts too; `escape` stays the advertised one in the busy footer and the home tip. Gating on `enabled` rather than binding order keeps the ctrl+c precedence (clear draft → interrupt → exit confirm) deterministic, since same-priority layers resolve by registration order and `useBindings` re-registers on every reactive change. | Keep the `session_interrupt_now` definition + mapping, the `session.interrupt.now` command and its `enabled` predicate, the footer shortcut memos, the split exit layer, the doc lines, and the test; take upstream's surrounding changes. If upstream binds `ctrl+c` to something else in the prompt, re-audit the precedence chain end to end. | diff --git a/packages/opencode/src/cli/cmd/run/splash.ts b/packages/opencode/src/cli/cmd/run/splash.ts index cb0da9b5eb..591517b978 100644 --- a/packages/opencode/src/cli/cmd/run/splash.ts +++ b/packages/opencode/src/cli/cmd/run/splash.ts @@ -1,6 +1,6 @@ // Entry and exit splash banners for direct interactive mode scrollback. // -// Renders the full opencode entry logo and a compact [O] exit badge, plus +// Renders the full codev entry logo and a compact [C] exit badge, plus // session metadata and the resume command. These are scrollback snapshots, so // they become immutable terminal history once committed. // @@ -18,7 +18,7 @@ import { type ScrollbackWriter, } from "@opentui/core" import * as Locale from "@/util/locale" -import { go } from "@/cli/logo" +import { badge } from "@/cli/logo" import type { RunSplashTheme } from "./theme" export const SPLASH_TITLE_LIMIT = 50 @@ -181,7 +181,7 @@ function build(input: SplashWriterInput, kind: "entry" | "exit", ctx: Scrollback let height = 1 if (kind === "entry") { - const mark = go.right.slice(1) + const mark = badge const top = 1 const body_left = (mark[0]?.length ?? 0) + 2 @@ -209,7 +209,7 @@ function build(input: SplashWriterInput, kind: "entry" | "exit", ctx: Scrollback } if (kind === "exit") { - const mark = go.right.slice(1) + const mark = badge const top = 1 const body_left = (mark[0]?.length ?? 0) + 2 const session = "Session " diff --git a/packages/tui/src/logo.ts b/packages/tui/src/logo.ts index 085f59e0eb..78663c5ff0 100644 --- a/packages/tui/src/logo.ts +++ b/packages/tui/src/logo.ts @@ -35,6 +35,11 @@ export function ansiWordmark(pad = "") { return logo.left.map((line, index) => `${pad}${draw(line)} ${draw(logo.right[index] ?? "")}`) } +// Fork: the "c" of the codev wordmark, drawn as the compact badge on the +// `codev --mini` entry and exit splashes. Upstream badges those with the "o" of +// the `go` mark below, which is still the background pulse's wordmark. +export const badge = logo.left.slice(1).map((line) => line.slice(0, 4)) + export const go = { left: [" ", "█▀▀▀", "█_^█", "▀▀▀▀"], right: [" ", "█▀▀█", "█__█", "▀▀▀▀"], From 67e12bd09b0341b7b726ccc73697b3d5b8ed62ab Mon Sep 17 00:00:00 2001 From: minhn4 Date: Tue, 21 Jul 2026 08:36:38 +0700 Subject: [PATCH 2/4] feat(cli): color the --mini badge with the brand secondary MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The badge glyph on the `codev --mini` splashes drew in the muted `left` color shared with the detail and session/continue labels, so the brand mark read as chrome. Draw it in the brand secondary instead — teal in dark, purple in light — matching the home-screen wordmark. RunSplashTheme gains mark/markShadow: splashTheme() derives them from theme.secondary (quantized to the terminal palette like the rest of the splash), and the fallback seeds them from brand teal #0891B2. splash.ts points the badge fg/shadow at them; the labels keep the muted left. Co-Authored-By: Claude Opus 4.8 (1M context) --- CHANGELOG.md | 4 +++- DIVERGENCES.md | 3 ++- packages/opencode/src/cli/cmd/run/splash.ts | 13 ++++++++----- packages/opencode/src/cli/cmd/run/theme.ts | 12 ++++++++++++ packages/opencode/test/cli/run/theme.test.ts | 2 ++ 5 files changed, 27 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fff25e79a3..e72bd98042 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,7 +11,9 @@ file is the human-readable summary of what changed and why. - **`codev --mini` splashes carry the CoDev mark** — the compact badge on the entry and exit banners is now the `c` of the CoDev wordmark instead of the - inherited `o`. The background pulse's `go` wordmark is unchanged. + inherited `o`, and it is drawn in the brand secondary (teal in dark, purple + in light) so it matches the home-screen banner rather than the muted detail + text beside it. The background pulse's `go` wordmark is unchanged. ### Sessions diff --git a/DIVERGENCES.md b/DIVERGENCES.md index 6f5bde6767..cd864b2234 100644 --- a/DIVERGENCES.md +++ b/DIVERGENCES.md @@ -103,7 +103,8 @@ from the squash message first, or use a plain merge. | `packages/tui/src/feature-plugins/system/which-key.tsx` | `HomeHint` reads the new `placement` prop from the `home_bottom` slot; with `"start"` it left-aligns | Same top-left banner alignment as the tips row above. | Keep our `placement` threading + conditional alignItems; take upstream's surrounding changes. | | `packages/tui/src/routes/home.tsx` | `placeholder` suggestion pool exported (was module-private) | The empty-session view in routes/session/index.tsx feeds the same pool to its prompt, so upstream edits to the suggestions flow through. | Keep the `export` keyword; take upstream's changes to the list. | | `packages/tui/src/routes/session/permission.tsx`, `packages/opencode/src/cli/cmd/run/permission.shared.ts`, `packages/opencode/test/cli/run/permission.shared.test.ts`, `packages/tui/src/component/error-component.tsx`, `packages/opencode/src/cli/cmd/run/splash.ts` | Visible sentences/badges "OpenCode"/"opencode" → "CoDev Code"/"codev-code" (incl. the permission always-allow copy and its test expectations); permission.tsx also registers a local `app.exit.confirm` command mirroring its `app.exit` reject and gathers the confirm binding (double-press Ctrl+C — see the double-Ctrl+C row) | User-facing copy; keep Ctrl+C rejecting the permission prompt as before. | Keep our strings; keep permission.tsx's `app.exit.confirm` command + binding. | -| `packages/opencode/src/cli/cmd/run/splash.ts` | The `--mini` entry and exit splashes badge with `badge` (the "c" of the codev wordmark) instead of `go.right.slice(1)` (the "o" of the `go` mark) — the import and both `const mark = …` lines; header comment "full opencode entry logo and a compact [O] exit badge" → "full codev entry logo and a compact [C] exit badge" | The splash badge is the brand mark users see entering and leaving `codev --mini`; an "o" is upstream's. `go` itself is untouched — `packages/tui/src/component/bg-pulse-render.ts` still draws the full "go" wordmark from it. | Keep the `badge` import and both `mark` assignments; take upstream's surrounding splash changes. If upstream restructures the badge rendering, feed it `badge` rather than reverting to `go`. | +| `packages/opencode/src/cli/cmd/run/splash.ts` | The `--mini` entry and exit splashes badge with `badge` (the "c" of the codev wordmark) instead of `go.right.slice(1)` (the "o" of the `go` mark) — the import and both `const mark = …` lines; header comment "full opencode entry logo and a compact [O] exit badge" → "full codev entry logo and a compact [C] exit badge". The badge glyphs draw with `input.theme.mark`/`markShadow` (brand secondary) via the local `mark_fg`/`markShadow` bindings, not the muted `left`/`leftShadow` that upstream uses for the whole banner. | The splash badge is the brand mark users see entering and leaving `codev --mini`; an "o" is upstream's, and the muted grey read as chrome rather than branding. `go` itself is untouched — `packages/tui/src/component/bg-pulse-render.ts` still draws the full "go" wordmark from it. | Keep the `badge` import, both `mark` assignments, and the `mark_fg`/`markShadow` badge fg/shadow; take upstream's surrounding splash changes. If upstream restructures the badge rendering, feed it `badge` in the brand `mark` color rather than reverting to `go`/`left`. | +| `packages/opencode/src/cli/cmd/run/theme.ts`, `packages/opencode/test/cli/run/theme.test.ts` | `RunSplashTheme` gains `mark`/`markShadow` (brand-secondary badge color for the mini splash): `splashTheme()` derives them from `theme.secondary` via `nearestIndexed`/`splashShadow`; `RUN_THEME_FALLBACK.splash` seeds them from the new `fallbackSplashMark` (`RGBA.fromIndex(37, "#0891b2")`, the dark secondary). theme.test.ts asserts both new fields quantize to indexed palette cells. | The mini badge is coloured like the home-screen wordmark (`theme.secondary`); upstream's splash model has no brand slot, only muted `left`/`right`. | Keep the two `RunSplashTheme` fields, the `splashTheme` derivation, the fallback seed, and the test assertions; take upstream's surrounding theme changes. | | `packages/tui/src/config/keybind.ts`, `packages/tui/src/routes/session/question.tsx`, `packages/tui/src/ui/toast.tsx`, `packages/tui/test/app-lifecycle.test.tsx`, `packages/web/src/content/docs/keybinds.mdx` (+ `es/`) | Double-press Ctrl+C to quit. keybind.ts: `app_exit` `ctrl+c,ctrl+d,q` → `ctrl+d,q`, added `app_exit_confirm: "ctrl+c"` definition + its `app.exit.confirm` CommandMap entry. question.tsx: a local `app.exit.confirm` command mirroring its `app.exit` reject, with the confirm binding gathered in both option branches (Ctrl+C still rejects the question — single press). toast.tsx: `ToastOptions.variant` union gains `"secondary"` (resolves to `theme.secondary`, the banner brand color, for the confirm toast). app-lifecycle.test.tsx: added the "app.exit.confirm requires two presses to exit" test. Docs (en + es `keybinds.mdx`): split the `app_exit` default and added `app_exit_confirm: "ctrl+c"`. See also the `app.tsx`, `dialog.tsx`, and `permission.tsx` rows for their `app.exit.confirm` handlers. | A stray Ctrl+C on an empty/unfocused prompt no longer quits instantly: the first press shows a "Press Ctrl+C again to exit" toast (1.5 s window), the second within that window quits. Ctrl+D, `q`, and the `/exit` slash commands stay immediate. | Keep our binding split, the `app_exit_confirm`/`app.exit.confirm` definition + mapping, the local confirm commands + gathered bindings, the toast `"secondary"` variant, the test, and the doc defaults; take upstream's surrounding changes. If upstream re-adds `ctrl+c` to `app_exit`, keep it removed. | | `packages/tui/src/config/keybind.ts`, `packages/web/src/content/docs/keybinds.mdx` (+ `es/`), `packages/opencode/test/cli/run/footer.view.test.tsx` | `sidebar_toggle` default `"b"` → `"ctrl+b,b"` (ctrl+b first so single-binding displays — the prompt-footer sidebar hint, the sidebar tip — resolve to it); `ctrl+b`'s former jobs move to `ctrl+o`: `input_move_left` `"left,ctrl+b"` → `"left,ctrl+o"`, `session_background` `"ctrl+b"` → `"ctrl+o"`; docs defaults updated to match (docs don't list `session_background`); footer.view.test.tsx asserts the run-CLI footer background hint as `"ctrl+o background"` (was `"ctrl+b background"`) since it resolves the real `session_background` default | Single-chord sidebar toggle on `ctrl+b` (tmux/VS Code sidebar muscle memory); its displaced bindings land on `ctrl+o`, which was unbound in every scope and free of terminal-encoding collisions (unlike `ctrl+h`/`ctrl+i`/`ctrl+m`) and flow control (`ctrl+s`/`ctrl+q`). | Keep our defaults on all three keybinds, the doc lines, and the footer hint assertion; take upstream's surrounding changes. If upstream binds `ctrl+o` to something else, re-audit and pick a new free key. | | `packages/tui/src/config/keybind.ts`, `packages/tui/src/component/prompt/index.tsx`, `packages/tui/src/app.tsx`, `packages/web/src/content/docs/keybinds.mdx` (+ `es/`) | Single-press Ctrl+C interrupt. keybind.ts: added `session_interrupt_now: "ctrl+c"` + its `session.interrupt.now` CommandMap entry (`session_interrupt` keeps `escape`; its description gains "(press twice)"). prompt/index.tsx: added the `session.interrupt.now` command — same guards as upstream's `session.interrupt` but it aborts on the first press, and its `enabled` requires a busy session AND an empty prompt so ctrl+c falls through to `prompt.clear` (draft present) or `app.exit.confirm` (idle) instead of racing them on layer order; gathered the command in the `prompt.palette` binding list; the busy footer hint reads `interruptShortcut()` (resolves `session.interrupt`, mapping `escape` → `esc` and falling back to the `session.interrupt.now` label when escape is unbound) instead of the hardcoded `esc`. app.tsx: split `app_exit_confirm` out of the shared exit `useBindings` into its own layer whose `enabled` also requires `sync.data.session_status[route.data.sessionID]` to be idle, so ctrl+c can't arm the quit confirm mid-turn (`app_exit` keeps the shared predicate). Docs (en + es): added the `session_interrupt_now` default. Covered by the fork-only `packages/tui/test/interrupt.view.test.tsx`. See also the double-Ctrl+C row above, whose scope this narrows. | Ctrl+C is what many users reach for to stop a running agent, so it interrupts too; `escape` stays the advertised one in the busy footer and the home tip. Gating on `enabled` rather than binding order keeps the ctrl+c precedence (clear draft → interrupt → exit confirm) deterministic, since same-priority layers resolve by registration order and `useBindings` re-registers on every reactive change. | Keep the `session_interrupt_now` definition + mapping, the `session.interrupt.now` command and its `enabled` predicate, the footer shortcut memos, the split exit layer, the doc lines, and the test; take upstream's surrounding changes. If upstream binds `ctrl+c` to something else in the prompt, re-audit the precedence chain end to end. | diff --git a/packages/opencode/src/cli/cmd/run/splash.ts b/packages/opencode/src/cli/cmd/run/splash.ts index 591517b978..8cbe982d4c 100644 --- a/packages/opencode/src/cli/cmd/run/splash.ts +++ b/packages/opencode/src/cli/cmd/run/splash.ts @@ -177,7 +177,10 @@ function build(input: SplashWriterInput, kind: "entry" | "exit", ctx: Scrollback const lines: Array<{ left: number; top: number; text: string; fg: ColorInput; bg?: ColorInput; attrs?: number }> = [] const left = input.theme.left const right = input.theme.right - const leftShadow = input.theme.leftShadow + // Fork: the badge glyph is the brand mark, not the muted `left` used for the + // detail and session/continue labels. + const mark_fg = input.theme.mark + const markShadow = input.theme.markShadow let height = 1 if (kind === "entry") { @@ -189,8 +192,8 @@ function build(input: SplashWriterInput, kind: "entry" | "exit", ctx: Scrollback draw(lines, mark[i] ?? "", { left: 0, top: top + i, - fg: left, - shadow: leftShadow, + fg: mark_fg, + shadow: markShadow, }) } @@ -219,8 +222,8 @@ function build(input: SplashWriterInput, kind: "entry" | "exit", ctx: Scrollback draw(lines, mark[i] ?? "", { left: 0, top: top + i, - fg: left, - shadow: leftShadow, + fg: mark_fg, + shadow: markShadow, }) } diff --git a/packages/opencode/src/cli/cmd/run/theme.ts b/packages/opencode/src/cli/cmd/run/theme.ts index e4fb315426..0997733984 100644 --- a/packages/opencode/src/cli/cmd/run/theme.ts +++ b/packages/opencode/src/cli/cmd/run/theme.ts @@ -21,6 +21,10 @@ export type RunSplashTheme = { right: ColorInput leftShadow: ColorInput rightShadow: ColorInput + // Fork: the `codev --mini` badge glyph, in the brand secondary (teal in dark, + // purple in light) so it matches the home banner wordmark. + mark: ColorInput + markShadow: ColorInput } export type RunFooterTheme = { @@ -489,11 +493,14 @@ function quantizeTheme(theme: TuiThemeCurrent, indexed: RGBA[]): TuiThemeCurrent function splashTheme(theme: TuiThemeCurrent, indexed: RGBA[]): RunSplashTheme { const left = nearestIndexed(indexed, theme.textMuted) const right = nearestIndexed(indexed, theme.text) + const mark = nearestIndexed(indexed, theme.secondary) return { left, right, leftShadow: splashShadow(indexed, theme.background, left, 0.14), rightShadow: splashShadow(indexed, theme.background, right, 0.14), + mark, + markShadow: splashShadow(indexed, theme.background, mark, 0.14), } } @@ -601,6 +608,9 @@ function tone(body: ColorInput, start?: ColorInput): Tone { const fallbackSplashIndexed = Array.from({ length: 256 }, (_, index) => RGBA.fromIndex(index)) const fallbackSplashLeft = RGBA.fromIndex(67) const fallbackSplashRight = RGBA.fromIndex(110) +// Fork: brand teal (the dark-mode secondary #0891B2) for the mini badge when +// palette detection fails; index 37 is its nearest 256-color cell. +const fallbackSplashMark = RGBA.fromIndex(37, rgba("#0891b2")) export const RUN_THEME_FALLBACK: RunTheme = { background: RGBA.fromValues(0, 0, 0, 0), @@ -634,6 +644,8 @@ export const RUN_THEME_FALLBACK: RunTheme = { right: fallbackSplashRight, leftShadow: splashShadow(fallbackSplashIndexed, RGBA.fromValues(0, 0, 0, 0), fallbackSplashLeft, 0.14), rightShadow: splashShadow(fallbackSplashIndexed, RGBA.fromValues(0, 0, 0, 0), fallbackSplashRight, 0.14), + mark: fallbackSplashMark, + markShadow: splashShadow(fallbackSplashIndexed, RGBA.fromValues(0, 0, 0, 0), fallbackSplashMark, 0.14), }, block: { highlight: seed.highlight, diff --git a/packages/opencode/test/cli/run/theme.test.ts b/packages/opencode/test/cli/run/theme.test.ts index 4102dea1c9..89b698575f 100644 --- a/packages/opencode/test/cli/run/theme.test.ts +++ b/packages/opencode/test/cli/run/theme.test.ts @@ -74,6 +74,8 @@ test("returns syntax styles and indexed splash colors", async () => { expectIndexed(theme.splash.right) expectIndexed(theme.splash.leftShadow) expectIndexed(theme.splash.rightShadow) + expectIndexed(theme.splash.mark) + expectIndexed(theme.splash.markShadow) expectIndexed(theme.block.highlight) expectIndexed(theme.block.warning) expectRgba(theme.footer.highlight) From b10c8832e8f843f5581efec34eb4cad577fd76a3 Mon Sep 17 00:00:00 2001 From: minhn4 Date: Tue, 21 Jul 2026 08:47:20 +0700 Subject: [PATCH 3/4] fix(cli): render the --mini badge in the exact brand teal MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The previous commit coloured the badge from the run theme's own `secondary`, quantized to the terminal palette. But the run theme derives `secondary` from the terminal palette (magenta), and quantizing snapped it to a dull, off-brand cell — not the banner teal. Colour the badge from the fixed brand constant instead (#0891b2 dark / #7b5bb6 light, the codev theme's darkSecondary/lightSecondary), as truecolor rather than a nearest-palette cell, so it renders as the exact home-screen banner teal. markShadow follows the banner's tint(background, brand, 0.25) formula. splashTheme takes the resolved dark/light pick; the fallback path seeds the same truecolor teal. Verified in a running --mini: the badge glyph emits 38;2;8;145;178 with a 48;2;2;36;45 interior fill. Co-Authored-By: Claude Opus 4.8 (1M context) --- DIVERGENCES.md | 2 +- packages/opencode/src/cli/cmd/run/theme.ts | 25 +++++++++++++------- packages/opencode/test/cli/run/theme.test.ts | 6 +++-- 3 files changed, 22 insertions(+), 11 deletions(-) diff --git a/DIVERGENCES.md b/DIVERGENCES.md index cd864b2234..42d7964681 100644 --- a/DIVERGENCES.md +++ b/DIVERGENCES.md @@ -104,7 +104,7 @@ from the squash message first, or use a plain merge. | `packages/tui/src/routes/home.tsx` | `placeholder` suggestion pool exported (was module-private) | The empty-session view in routes/session/index.tsx feeds the same pool to its prompt, so upstream edits to the suggestions flow through. | Keep the `export` keyword; take upstream's changes to the list. | | `packages/tui/src/routes/session/permission.tsx`, `packages/opencode/src/cli/cmd/run/permission.shared.ts`, `packages/opencode/test/cli/run/permission.shared.test.ts`, `packages/tui/src/component/error-component.tsx`, `packages/opencode/src/cli/cmd/run/splash.ts` | Visible sentences/badges "OpenCode"/"opencode" → "CoDev Code"/"codev-code" (incl. the permission always-allow copy and its test expectations); permission.tsx also registers a local `app.exit.confirm` command mirroring its `app.exit` reject and gathers the confirm binding (double-press Ctrl+C — see the double-Ctrl+C row) | User-facing copy; keep Ctrl+C rejecting the permission prompt as before. | Keep our strings; keep permission.tsx's `app.exit.confirm` command + binding. | | `packages/opencode/src/cli/cmd/run/splash.ts` | The `--mini` entry and exit splashes badge with `badge` (the "c" of the codev wordmark) instead of `go.right.slice(1)` (the "o" of the `go` mark) — the import and both `const mark = …` lines; header comment "full opencode entry logo and a compact [O] exit badge" → "full codev entry logo and a compact [C] exit badge". The badge glyphs draw with `input.theme.mark`/`markShadow` (brand secondary) via the local `mark_fg`/`markShadow` bindings, not the muted `left`/`leftShadow` that upstream uses for the whole banner. | The splash badge is the brand mark users see entering and leaving `codev --mini`; an "o" is upstream's, and the muted grey read as chrome rather than branding. `go` itself is untouched — `packages/tui/src/component/bg-pulse-render.ts` still draws the full "go" wordmark from it. | Keep the `badge` import, both `mark` assignments, and the `mark_fg`/`markShadow` badge fg/shadow; take upstream's surrounding splash changes. If upstream restructures the badge rendering, feed it `badge` in the brand `mark` color rather than reverting to `go`/`left`. | -| `packages/opencode/src/cli/cmd/run/theme.ts`, `packages/opencode/test/cli/run/theme.test.ts` | `RunSplashTheme` gains `mark`/`markShadow` (brand-secondary badge color for the mini splash): `splashTheme()` derives them from `theme.secondary` via `nearestIndexed`/`splashShadow`; `RUN_THEME_FALLBACK.splash` seeds them from the new `fallbackSplashMark` (`RGBA.fromIndex(37, "#0891b2")`, the dark secondary). theme.test.ts asserts both new fields quantize to indexed palette cells. | The mini badge is coloured like the home-screen wordmark (`theme.secondary`); upstream's splash model has no brand slot, only muted `left`/`right`. | Keep the two `RunSplashTheme` fields, the `splashTheme` derivation, the fallback seed, and the test assertions; take upstream's surrounding theme changes. | +| `packages/opencode/src/cli/cmd/run/theme.ts`, `packages/opencode/test/cli/run/theme.test.ts` | `RunSplashTheme` gains `mark`/`markShadow` (brand badge color for the mini splash). `splashTheme(theme, indexed, pick)` takes the resolved dark/light `pick` and sets `mark` to the fixed `BRAND_SECONDARY` constant (`#0891b2` dark / `#7b5bb6` light — the codev theme's `darkSecondary`/`lightSecondary`) as **truecolor, not quantized**, with `markShadow = tint(theme.background, mark, 0.25)` (the banner's shadow formula). `RUN_THEME_FALLBACK.splash` seeds `mark` from `fallbackSplashMark = rgba("#0891b2")` (also truecolor). theme.test.ts asserts `mark` is exactly `[8,145,178]` and `markShadow` is non-indexed. Deliberately NOT the run theme's own `secondary` (that is derived from the terminal palette — magenta — and quantizing it gives a dull, off-brand cell). | The mini badge must read as the exact home-screen banner teal (`logo.tsx` draws the wordmark in the same brand `#0891b2`/`#7b5bb6` truecolor); the rest of the splash stays palette-matched, but the brand mark is a deliberate truecolor exception. | Keep the two `RunSplashTheme` fields, the `pick` param + `BRAND_SECONDARY` derivation, the truecolor `markShadow`, the fallback seed, and the test assertions; take upstream's surrounding theme changes. If upstream reworks `splashTheme`'s signature, re-thread `pick`. | | `packages/tui/src/config/keybind.ts`, `packages/tui/src/routes/session/question.tsx`, `packages/tui/src/ui/toast.tsx`, `packages/tui/test/app-lifecycle.test.tsx`, `packages/web/src/content/docs/keybinds.mdx` (+ `es/`) | Double-press Ctrl+C to quit. keybind.ts: `app_exit` `ctrl+c,ctrl+d,q` → `ctrl+d,q`, added `app_exit_confirm: "ctrl+c"` definition + its `app.exit.confirm` CommandMap entry. question.tsx: a local `app.exit.confirm` command mirroring its `app.exit` reject, with the confirm binding gathered in both option branches (Ctrl+C still rejects the question — single press). toast.tsx: `ToastOptions.variant` union gains `"secondary"` (resolves to `theme.secondary`, the banner brand color, for the confirm toast). app-lifecycle.test.tsx: added the "app.exit.confirm requires two presses to exit" test. Docs (en + es `keybinds.mdx`): split the `app_exit` default and added `app_exit_confirm: "ctrl+c"`. See also the `app.tsx`, `dialog.tsx`, and `permission.tsx` rows for their `app.exit.confirm` handlers. | A stray Ctrl+C on an empty/unfocused prompt no longer quits instantly: the first press shows a "Press Ctrl+C again to exit" toast (1.5 s window), the second within that window quits. Ctrl+D, `q`, and the `/exit` slash commands stay immediate. | Keep our binding split, the `app_exit_confirm`/`app.exit.confirm` definition + mapping, the local confirm commands + gathered bindings, the toast `"secondary"` variant, the test, and the doc defaults; take upstream's surrounding changes. If upstream re-adds `ctrl+c` to `app_exit`, keep it removed. | | `packages/tui/src/config/keybind.ts`, `packages/web/src/content/docs/keybinds.mdx` (+ `es/`), `packages/opencode/test/cli/run/footer.view.test.tsx` | `sidebar_toggle` default `"b"` → `"ctrl+b,b"` (ctrl+b first so single-binding displays — the prompt-footer sidebar hint, the sidebar tip — resolve to it); `ctrl+b`'s former jobs move to `ctrl+o`: `input_move_left` `"left,ctrl+b"` → `"left,ctrl+o"`, `session_background` `"ctrl+b"` → `"ctrl+o"`; docs defaults updated to match (docs don't list `session_background`); footer.view.test.tsx asserts the run-CLI footer background hint as `"ctrl+o background"` (was `"ctrl+b background"`) since it resolves the real `session_background` default | Single-chord sidebar toggle on `ctrl+b` (tmux/VS Code sidebar muscle memory); its displaced bindings land on `ctrl+o`, which was unbound in every scope and free of terminal-encoding collisions (unlike `ctrl+h`/`ctrl+i`/`ctrl+m`) and flow control (`ctrl+s`/`ctrl+q`). | Keep our defaults on all three keybinds, the doc lines, and the footer hint assertion; take upstream's surrounding changes. If upstream binds `ctrl+o` to something else, re-audit and pick a new free key. | | `packages/tui/src/config/keybind.ts`, `packages/tui/src/component/prompt/index.tsx`, `packages/tui/src/app.tsx`, `packages/web/src/content/docs/keybinds.mdx` (+ `es/`) | Single-press Ctrl+C interrupt. keybind.ts: added `session_interrupt_now: "ctrl+c"` + its `session.interrupt.now` CommandMap entry (`session_interrupt` keeps `escape`; its description gains "(press twice)"). prompt/index.tsx: added the `session.interrupt.now` command — same guards as upstream's `session.interrupt` but it aborts on the first press, and its `enabled` requires a busy session AND an empty prompt so ctrl+c falls through to `prompt.clear` (draft present) or `app.exit.confirm` (idle) instead of racing them on layer order; gathered the command in the `prompt.palette` binding list; the busy footer hint reads `interruptShortcut()` (resolves `session.interrupt`, mapping `escape` → `esc` and falling back to the `session.interrupt.now` label when escape is unbound) instead of the hardcoded `esc`. app.tsx: split `app_exit_confirm` out of the shared exit `useBindings` into its own layer whose `enabled` also requires `sync.data.session_status[route.data.sessionID]` to be idle, so ctrl+c can't arm the quit confirm mid-turn (`app_exit` keeps the shared predicate). Docs (en + es): added the `session_interrupt_now` default. Covered by the fork-only `packages/tui/test/interrupt.view.test.tsx`. See also the double-Ctrl+C row above, whose scope this narrows. | Ctrl+C is what many users reach for to stop a running agent, so it interrupts too; `escape` stays the advertised one in the busy footer and the home tip. Gating on `enabled` rather than binding order keeps the ctrl+c precedence (clear draft → interrupt → exit confirm) deterministic, since same-priority layers resolve by registration order and `useBindings` re-registers on every reactive change. | Keep the `session_interrupt_now` definition + mapping, the `session.interrupt.now` command and its `enabled` predicate, the footer shortcut memos, the split exit layer, the doc lines, and the test; take upstream's surrounding changes. If upstream binds `ctrl+c` to something else in the prompt, re-audit the precedence chain end to end. | diff --git a/packages/opencode/src/cli/cmd/run/theme.ts b/packages/opencode/src/cli/cmd/run/theme.ts index 0997733984..140a661eb8 100644 --- a/packages/opencode/src/cli/cmd/run/theme.ts +++ b/packages/opencode/src/cli/cmd/run/theme.ts @@ -490,17 +490,26 @@ function quantizeTheme(theme: TuiThemeCurrent, indexed: RGBA[]): TuiThemeCurrent } } -function splashTheme(theme: TuiThemeCurrent, indexed: RGBA[]): RunSplashTheme { +// Fork: the codev brand secondary (packages/tui/src/theme/assets/codev.json — +// darkSecondary/lightSecondary). The mini badge uses these exact truecolor +// values, matching the home-screen banner, rather than the run theme's own +// `secondary` (which is derived from the terminal palette — magenta — and would +// quantize to a dull, off-brand cell). +const BRAND_SECONDARY = { dark: rgba("#0891b2"), light: rgba("#7b5bb6") } as const + +function splashTheme(theme: TuiThemeCurrent, indexed: RGBA[], pick: "dark" | "light"): RunSplashTheme { const left = nearestIndexed(indexed, theme.textMuted) const right = nearestIndexed(indexed, theme.text) - const mark = nearestIndexed(indexed, theme.secondary) + // Brand truecolor, left unquantized so it renders as the banner's exact teal + // (or purple in light) instead of the nearest palette cell. + const mark = BRAND_SECONDARY[pick] return { left, right, leftShadow: splashShadow(indexed, theme.background, left, 0.14), rightShadow: splashShadow(indexed, theme.background, right, 0.14), mark, - markShadow: splashShadow(indexed, theme.background, mark, 0.14), + markShadow: tint(theme.background, mark, 0.25), } } @@ -608,9 +617,9 @@ function tone(body: ColorInput, start?: ColorInput): Tone { const fallbackSplashIndexed = Array.from({ length: 256 }, (_, index) => RGBA.fromIndex(index)) const fallbackSplashLeft = RGBA.fromIndex(67) const fallbackSplashRight = RGBA.fromIndex(110) -// Fork: brand teal (the dark-mode secondary #0891B2) for the mini badge when -// palette detection fails; index 37 is its nearest 256-color cell. -const fallbackSplashMark = RGBA.fromIndex(37, rgba("#0891b2")) +// Fork: brand teal (the dark-mode secondary) for the mini badge when palette +// detection fails — the same exact truecolor value the live path uses. +const fallbackSplashMark = rgba("#0891b2") export const RUN_THEME_FALLBACK: RunTheme = { background: RGBA.fromValues(0, 0, 0, 0), @@ -645,7 +654,7 @@ export const RUN_THEME_FALLBACK: RunTheme = { leftShadow: splashShadow(fallbackSplashIndexed, RGBA.fromValues(0, 0, 0, 0), fallbackSplashLeft, 0.14), rightShadow: splashShadow(fallbackSplashIndexed, RGBA.fromValues(0, 0, 0, 0), fallbackSplashRight, 0.14), mark: fallbackSplashMark, - markShadow: splashShadow(fallbackSplashIndexed, RGBA.fromValues(0, 0, 0, 0), fallbackSplashMark, 0.14), + markShadow: tint(RGBA.fromValues(0, 0, 0, 0), fallbackSplashMark, 0.25), }, block: { highlight: seed.highlight, @@ -692,7 +701,7 @@ export async function resolveRunTheme(renderer: CliRenderer): Promise return map( footerTheme, scrollbackTheme, - splashTheme(scrollbackTheme, indexed), + splashTheme(scrollbackTheme, indexed, pick), syntax, shared.generateSubtleSyntax(syntaxTheme), ) diff --git a/packages/opencode/test/cli/run/theme.test.ts b/packages/opencode/test/cli/run/theme.test.ts index 89b698575f..cd01fe6b59 100644 --- a/packages/opencode/test/cli/run/theme.test.ts +++ b/packages/opencode/test/cli/run/theme.test.ts @@ -74,8 +74,10 @@ test("returns syntax styles and indexed splash colors", async () => { expectIndexed(theme.splash.right) expectIndexed(theme.splash.leftShadow) expectIndexed(theme.splash.rightShadow) - expectIndexed(theme.splash.mark) - expectIndexed(theme.splash.markShadow) + // The badge is the exact brand teal (truecolor, not a palette cell) so it + // matches the home-screen banner rather than the nearest indexed color. + expect(expectRgba(theme.splash.mark).toInts().slice(0, 3)).toEqual([8, 145, 178]) + expect(expectRgba(theme.splash.markShadow).intent).not.toBe("indexed") expectIndexed(theme.block.highlight) expectIndexed(theme.block.warning) expectRgba(theme.footer.highlight) From 8b52b0e18cb5c0faa3fdf5385d1b712596494a41 Mon Sep 17 00:00:00 2001 From: minhn4 Date: Tue, 21 Jul 2026 08:52:59 +0700 Subject: [PATCH 4/4] feat(cli): drop the shadow from the --mini badge The badge's `c` glyph carried an interior shadow fill (the `_` cells in its middle row rendered as spaces with markShadow as background). Strip those marks to spaces so the badge is a flat brand-teal outline. markShadow is now unused, so remove it: from RunSplashTheme, splashTheme, the fallback, and the test. draw()'s `shadow` param becomes optional and the badge calls omit it. Verified in a running --mini: the badge glyph emits 38;2;8;145;178 with no background cell on any row. Co-Authored-By: Claude Opus 4.8 (1M context) --- DIVERGENCES.md | 6 +++--- packages/opencode/src/cli/cmd/run/splash.ts | 13 ++++++------- packages/opencode/src/cli/cmd/run/theme.ts | 13 +++++-------- packages/opencode/test/cli/run/theme.test.ts | 1 - packages/tui/src/logo.ts | 6 ++++-- 5 files changed, 18 insertions(+), 21 deletions(-) diff --git a/DIVERGENCES.md b/DIVERGENCES.md index 42d7964681..e2a3654d2e 100644 --- a/DIVERGENCES.md +++ b/DIVERGENCES.md @@ -85,7 +85,7 @@ from the squash message first, or use a plain merge. | `packages/opencode/script/publish.ts` | `PUBLISH_NAME = "codev-code"` const; published-name→dir map; main pkg name + bin `codev` → `./bin/codev.exe` (postinstall stub file renamed from `${pkg.name}.exe`, matching the renamed platform binary); embedded README; env-guarded exit before the Docker/AUR/Homebrew section | npm-only publishing under the fork's names (unscoped, public on npmjs); the installed command is `codev`; upstream's other channels target anomalyco infra. | Keep our names, description, bin mapping + early exit; take upstream's npm-section changes above the exit. | | `packages/opencode/bin/opencode` | `base` prefix `opencode-` → `codev-code-` | Runtime platform-binary lookup must match the published names. | Keep our prefix. | | `packages/opencode/script/postinstall.mjs` | `base` prefix `opencode-` → `codev-code-`; source/target binary names `opencode`/`opencode.exe` → `codev`/`codev.exe` (matches build.ts's `bin/codev` and publish.ts's `bin/codev.exe` stub); temp-dir prefix and failure message rebranded | Install-time platform-binary fetch must match the published package and binary names. | Keep our prefix and binary names. | -| `packages/tui/src/logo.ts` | Pixel glyphs "open"+"code" → "co"+"dev"; added composed `wordmark` (plain lines) and `ansiWordmark` (bold brand teal + drop shadow) exports for CLI banners; added a `badge` export (the "c" of the wordmark — `logo.left` rows 1-3 sliced to the first glyph) for the `--mini` splashes | Branded wordmark art; `ansiWordmark` feeds the CLI help banner and session epilogue. | Keep our glyphs + exports; take upstream's surrounding changes. | +| `packages/tui/src/logo.ts` | Pixel glyphs "open"+"code" → "co"+"dev"; added composed `wordmark` (plain lines) and `ansiWordmark` (bold brand teal + drop shadow) exports for CLI banners; added a `badge` export (the "c" of the wordmark — `logo.left` rows 1-3 sliced to the first glyph, with the `_` shadow-fill marks stripped to spaces so it renders as a flat outline) for the `--mini` splashes | Branded wordmark art; `ansiWordmark` feeds the CLI help banner and session epilogue. | Keep our glyphs + exports; take upstream's surrounding changes. | | `packages/tui/src/component/logo.tsx` | Renders the lowercase codev wordmark in the theme's `secondary` color (bold, with a `tint`-derived drop shadow) instead of upstream's two-tone "open"+"code" | Home banner shows the brand wordmark in the active theme's brand color (teal in dark, purple in light). | Keep our version wholesale. | | `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 brand teal + drop shadow on TTY) | Help banner matches the TUI home banner. | Keep our wordmark + logo(); take upstream's surrounding changes. | @@ -103,8 +103,8 @@ from the squash message first, or use a plain merge. | `packages/tui/src/feature-plugins/system/which-key.tsx` | `HomeHint` reads the new `placement` prop from the `home_bottom` slot; with `"start"` it left-aligns | Same top-left banner alignment as the tips row above. | Keep our `placement` threading + conditional alignItems; take upstream's surrounding changes. | | `packages/tui/src/routes/home.tsx` | `placeholder` suggestion pool exported (was module-private) | The empty-session view in routes/session/index.tsx feeds the same pool to its prompt, so upstream edits to the suggestions flow through. | Keep the `export` keyword; take upstream's changes to the list. | | `packages/tui/src/routes/session/permission.tsx`, `packages/opencode/src/cli/cmd/run/permission.shared.ts`, `packages/opencode/test/cli/run/permission.shared.test.ts`, `packages/tui/src/component/error-component.tsx`, `packages/opencode/src/cli/cmd/run/splash.ts` | Visible sentences/badges "OpenCode"/"opencode" → "CoDev Code"/"codev-code" (incl. the permission always-allow copy and its test expectations); permission.tsx also registers a local `app.exit.confirm` command mirroring its `app.exit` reject and gathers the confirm binding (double-press Ctrl+C — see the double-Ctrl+C row) | User-facing copy; keep Ctrl+C rejecting the permission prompt as before. | Keep our strings; keep permission.tsx's `app.exit.confirm` command + binding. | -| `packages/opencode/src/cli/cmd/run/splash.ts` | The `--mini` entry and exit splashes badge with `badge` (the "c" of the codev wordmark) instead of `go.right.slice(1)` (the "o" of the `go` mark) — the import and both `const mark = …` lines; header comment "full opencode entry logo and a compact [O] exit badge" → "full codev entry logo and a compact [C] exit badge". The badge glyphs draw with `input.theme.mark`/`markShadow` (brand secondary) via the local `mark_fg`/`markShadow` bindings, not the muted `left`/`leftShadow` that upstream uses for the whole banner. | The splash badge is the brand mark users see entering and leaving `codev --mini`; an "o" is upstream's, and the muted grey read as chrome rather than branding. `go` itself is untouched — `packages/tui/src/component/bg-pulse-render.ts` still draws the full "go" wordmark from it. | Keep the `badge` import, both `mark` assignments, and the `mark_fg`/`markShadow` badge fg/shadow; take upstream's surrounding splash changes. If upstream restructures the badge rendering, feed it `badge` in the brand `mark` color rather than reverting to `go`/`left`. | -| `packages/opencode/src/cli/cmd/run/theme.ts`, `packages/opencode/test/cli/run/theme.test.ts` | `RunSplashTheme` gains `mark`/`markShadow` (brand badge color for the mini splash). `splashTheme(theme, indexed, pick)` takes the resolved dark/light `pick` and sets `mark` to the fixed `BRAND_SECONDARY` constant (`#0891b2` dark / `#7b5bb6` light — the codev theme's `darkSecondary`/`lightSecondary`) as **truecolor, not quantized**, with `markShadow = tint(theme.background, mark, 0.25)` (the banner's shadow formula). `RUN_THEME_FALLBACK.splash` seeds `mark` from `fallbackSplashMark = rgba("#0891b2")` (also truecolor). theme.test.ts asserts `mark` is exactly `[8,145,178]` and `markShadow` is non-indexed. Deliberately NOT the run theme's own `secondary` (that is derived from the terminal palette — magenta — and quantizing it gives a dull, off-brand cell). | The mini badge must read as the exact home-screen banner teal (`logo.tsx` draws the wordmark in the same brand `#0891b2`/`#7b5bb6` truecolor); the rest of the splash stays palette-matched, but the brand mark is a deliberate truecolor exception. | Keep the two `RunSplashTheme` fields, the `pick` param + `BRAND_SECONDARY` derivation, the truecolor `markShadow`, the fallback seed, and the test assertions; take upstream's surrounding theme changes. If upstream reworks `splashTheme`'s signature, re-thread `pick`. | +| `packages/opencode/src/cli/cmd/run/splash.ts` | The `--mini` entry and exit splashes badge with `badge` (the "c" of the codev wordmark) instead of `go.right.slice(1)` (the "o" of the `go` mark) — the import and both `const mark = …` lines; header comment "full opencode entry logo and a compact [O] exit badge" → "full codev entry logo and a compact [C] exit badge". The badge glyphs draw with `input.theme.mark` (brand secondary) via the local `mark_fg` binding, not the muted `left`/`leftShadow` that upstream uses for the whole banner, and with no shadow — `draw`'s `shadow` param is now optional and the badge calls omit it, so the flat brand-colored outline has no interior fill. | The splash badge is the brand mark users see entering and leaving `codev --mini`; an "o" is upstream's, and the muted grey read as chrome rather than branding. `go` itself is untouched — `packages/tui/src/component/bg-pulse-render.ts` still draws the full "go" wordmark from it. | Keep the `badge` import, both `mark` assignments, the `mark_fg` badge fg, and the optional-`shadow` `draw` signature; take upstream's surrounding splash changes. If upstream restructures the badge rendering, feed it `badge` in the brand `mark` color rather than reverting to `go`/`left`. | +| `packages/opencode/src/cli/cmd/run/theme.ts`, `packages/opencode/test/cli/run/theme.test.ts` | `RunSplashTheme` gains a `mark` field (brand badge color for the mini splash — no shadow color, the badge is a flat outline). `splashTheme(theme, indexed, pick)` takes the resolved dark/light `pick` and sets `mark` to the fixed `BRAND_SECONDARY` constant (`#0891b2` dark / `#7b5bb6` light — the codev theme's `darkSecondary`/`lightSecondary`) as **truecolor, not quantized**. `RUN_THEME_FALLBACK.splash` seeds `mark` from `fallbackSplashMark = rgba("#0891b2")` (also truecolor). theme.test.ts asserts `mark` is exactly `[8,145,178]`. Deliberately NOT the run theme's own `secondary` (that is derived from the terminal palette — magenta — and quantizing it gives a dull, off-brand cell). | The mini badge must read as the exact home-screen banner teal (`logo.tsx` draws the wordmark in the same brand `#0891b2`/`#7b5bb6` truecolor); the rest of the splash stays palette-matched, but the brand mark is a deliberate truecolor exception. | Keep the `RunSplashTheme.mark` field, the `pick` param + `BRAND_SECONDARY` derivation, the fallback seed, and the test assertion; take upstream's surrounding theme changes. If upstream reworks `splashTheme`'s signature, re-thread `pick`. | | `packages/tui/src/config/keybind.ts`, `packages/tui/src/routes/session/question.tsx`, `packages/tui/src/ui/toast.tsx`, `packages/tui/test/app-lifecycle.test.tsx`, `packages/web/src/content/docs/keybinds.mdx` (+ `es/`) | Double-press Ctrl+C to quit. keybind.ts: `app_exit` `ctrl+c,ctrl+d,q` → `ctrl+d,q`, added `app_exit_confirm: "ctrl+c"` definition + its `app.exit.confirm` CommandMap entry. question.tsx: a local `app.exit.confirm` command mirroring its `app.exit` reject, with the confirm binding gathered in both option branches (Ctrl+C still rejects the question — single press). toast.tsx: `ToastOptions.variant` union gains `"secondary"` (resolves to `theme.secondary`, the banner brand color, for the confirm toast). app-lifecycle.test.tsx: added the "app.exit.confirm requires two presses to exit" test. Docs (en + es `keybinds.mdx`): split the `app_exit` default and added `app_exit_confirm: "ctrl+c"`. See also the `app.tsx`, `dialog.tsx`, and `permission.tsx` rows for their `app.exit.confirm` handlers. | A stray Ctrl+C on an empty/unfocused prompt no longer quits instantly: the first press shows a "Press Ctrl+C again to exit" toast (1.5 s window), the second within that window quits. Ctrl+D, `q`, and the `/exit` slash commands stay immediate. | Keep our binding split, the `app_exit_confirm`/`app.exit.confirm` definition + mapping, the local confirm commands + gathered bindings, the toast `"secondary"` variant, the test, and the doc defaults; take upstream's surrounding changes. If upstream re-adds `ctrl+c` to `app_exit`, keep it removed. | | `packages/tui/src/config/keybind.ts`, `packages/web/src/content/docs/keybinds.mdx` (+ `es/`), `packages/opencode/test/cli/run/footer.view.test.tsx` | `sidebar_toggle` default `"b"` → `"ctrl+b,b"` (ctrl+b first so single-binding displays — the prompt-footer sidebar hint, the sidebar tip — resolve to it); `ctrl+b`'s former jobs move to `ctrl+o`: `input_move_left` `"left,ctrl+b"` → `"left,ctrl+o"`, `session_background` `"ctrl+b"` → `"ctrl+o"`; docs defaults updated to match (docs don't list `session_background`); footer.view.test.tsx asserts the run-CLI footer background hint as `"ctrl+o background"` (was `"ctrl+b background"`) since it resolves the real `session_background` default | Single-chord sidebar toggle on `ctrl+b` (tmux/VS Code sidebar muscle memory); its displaced bindings land on `ctrl+o`, which was unbound in every scope and free of terminal-encoding collisions (unlike `ctrl+h`/`ctrl+i`/`ctrl+m`) and flow control (`ctrl+s`/`ctrl+q`). | Keep our defaults on all three keybinds, the doc lines, and the footer hint assertion; take upstream's surrounding changes. If upstream binds `ctrl+o` to something else, re-audit and pick a new free key. | | `packages/tui/src/config/keybind.ts`, `packages/tui/src/component/prompt/index.tsx`, `packages/tui/src/app.tsx`, `packages/web/src/content/docs/keybinds.mdx` (+ `es/`) | Single-press Ctrl+C interrupt. keybind.ts: added `session_interrupt_now: "ctrl+c"` + its `session.interrupt.now` CommandMap entry (`session_interrupt` keeps `escape`; its description gains "(press twice)"). prompt/index.tsx: added the `session.interrupt.now` command — same guards as upstream's `session.interrupt` but it aborts on the first press, and its `enabled` requires a busy session AND an empty prompt so ctrl+c falls through to `prompt.clear` (draft present) or `app.exit.confirm` (idle) instead of racing them on layer order; gathered the command in the `prompt.palette` binding list; the busy footer hint reads `interruptShortcut()` (resolves `session.interrupt`, mapping `escape` → `esc` and falling back to the `session.interrupt.now` label when escape is unbound) instead of the hardcoded `esc`. app.tsx: split `app_exit_confirm` out of the shared exit `useBindings` into its own layer whose `enabled` also requires `sync.data.session_status[route.data.sessionID]` to be idle, so ctrl+c can't arm the quit confirm mid-turn (`app_exit` keeps the shared predicate). Docs (en + es): added the `session_interrupt_now` default. Covered by the fork-only `packages/tui/test/interrupt.view.test.tsx`. See also the double-Ctrl+C row above, whose scope this narrows. | Ctrl+C is what many users reach for to stop a running agent, so it interrupts too; `escape` stays the advertised one in the busy footer and the home tip. Gating on `enabled` rather than binding order keeps the ctrl+c precedence (clear draft → interrupt → exit confirm) deterministic, since same-priority layers resolve by registration order and `useBindings` re-registers on every reactive change. | Keep the `session_interrupt_now` definition + mapping, the `session.interrupt.now` command and its `enabled` predicate, the footer shortcut memos, the split exit layer, the doc lines, and the test; take upstream's surrounding changes. If upstream binds `ctrl+c` to something else in the prompt, re-audit the precedence chain end to end. | diff --git a/packages/opencode/src/cli/cmd/run/splash.ts b/packages/opencode/src/cli/cmd/run/splash.ts index 8cbe982d4c..15168414bc 100644 --- a/packages/opencode/src/cli/cmd/run/splash.ts +++ b/packages/opencode/src/cli/cmd/run/splash.ts @@ -148,19 +148,21 @@ function draw( left: number top: number fg: ColorInput - shadow: ColorInput + // Optional: flat marks (e.g. the codev badge) carry no shadow, so the + // shadow-fill and half-block-top cells fall through to a plain fg cell. + shadow?: ColorInput attrs?: number }, ) { let x = input.left for (const cell of cells(row)) { - if (cell.mark === "full" || cell.mark === "mix") { + if (input.shadow && (cell.mark === "full" || cell.mark === "mix")) { push(lines, x, input.top, cell.char, input.fg, input.shadow, input.attrs) x += 1 continue } - if (cell.mark === "top") { + if (input.shadow && cell.mark === "top") { push(lines, x, input.top, cell.char, input.shadow, undefined, input.attrs) x += 1 continue @@ -178,9 +180,8 @@ function build(input: SplashWriterInput, kind: "entry" | "exit", ctx: Scrollback const left = input.theme.left const right = input.theme.right // Fork: the badge glyph is the brand mark, not the muted `left` used for the - // detail and session/continue labels. + // detail and session/continue labels. It is a flat outline with no shadow. const mark_fg = input.theme.mark - const markShadow = input.theme.markShadow let height = 1 if (kind === "entry") { @@ -193,7 +194,6 @@ function build(input: SplashWriterInput, kind: "entry" | "exit", ctx: Scrollback left: 0, top: top + i, fg: mark_fg, - shadow: markShadow, }) } @@ -223,7 +223,6 @@ function build(input: SplashWriterInput, kind: "entry" | "exit", ctx: Scrollback left: 0, top: top + i, fg: mark_fg, - shadow: markShadow, }) } diff --git a/packages/opencode/src/cli/cmd/run/theme.ts b/packages/opencode/src/cli/cmd/run/theme.ts index 140a661eb8..6c66ed15b3 100644 --- a/packages/opencode/src/cli/cmd/run/theme.ts +++ b/packages/opencode/src/cli/cmd/run/theme.ts @@ -22,9 +22,9 @@ export type RunSplashTheme = { leftShadow: ColorInput rightShadow: ColorInput // Fork: the `codev --mini` badge glyph, in the brand secondary (teal in dark, - // purple in light) so it matches the home banner wordmark. + // purple in light) so it matches the home banner wordmark. The badge is a flat + // outline with no shadow, so there is no matching shadow color. mark: ColorInput - markShadow: ColorInput } export type RunFooterTheme = { @@ -500,16 +500,14 @@ const BRAND_SECONDARY = { dark: rgba("#0891b2"), light: rgba("#7b5bb6") } as con function splashTheme(theme: TuiThemeCurrent, indexed: RGBA[], pick: "dark" | "light"): RunSplashTheme { const left = nearestIndexed(indexed, theme.textMuted) const right = nearestIndexed(indexed, theme.text) - // Brand truecolor, left unquantized so it renders as the banner's exact teal - // (or purple in light) instead of the nearest palette cell. - const mark = BRAND_SECONDARY[pick] return { left, right, leftShadow: splashShadow(indexed, theme.background, left, 0.14), rightShadow: splashShadow(indexed, theme.background, right, 0.14), - mark, - markShadow: tint(theme.background, mark, 0.25), + // Brand truecolor, left unquantized so it renders as the banner's exact teal + // (or purple in light) instead of the nearest palette cell. + mark: BRAND_SECONDARY[pick], } } @@ -654,7 +652,6 @@ export const RUN_THEME_FALLBACK: RunTheme = { leftShadow: splashShadow(fallbackSplashIndexed, RGBA.fromValues(0, 0, 0, 0), fallbackSplashLeft, 0.14), rightShadow: splashShadow(fallbackSplashIndexed, RGBA.fromValues(0, 0, 0, 0), fallbackSplashRight, 0.14), mark: fallbackSplashMark, - markShadow: tint(RGBA.fromValues(0, 0, 0, 0), fallbackSplashMark, 0.25), }, block: { highlight: seed.highlight, diff --git a/packages/opencode/test/cli/run/theme.test.ts b/packages/opencode/test/cli/run/theme.test.ts index cd01fe6b59..cb1d024ab6 100644 --- a/packages/opencode/test/cli/run/theme.test.ts +++ b/packages/opencode/test/cli/run/theme.test.ts @@ -77,7 +77,6 @@ test("returns syntax styles and indexed splash colors", async () => { // The badge is the exact brand teal (truecolor, not a palette cell) so it // matches the home-screen banner rather than the nearest indexed color. expect(expectRgba(theme.splash.mark).toInts().slice(0, 3)).toEqual([8, 145, 178]) - expect(expectRgba(theme.splash.markShadow).intent).not.toBe("indexed") expectIndexed(theme.block.highlight) expectIndexed(theme.block.warning) expectRgba(theme.footer.highlight) diff --git a/packages/tui/src/logo.ts b/packages/tui/src/logo.ts index 78663c5ff0..c48d9918ff 100644 --- a/packages/tui/src/logo.ts +++ b/packages/tui/src/logo.ts @@ -37,8 +37,10 @@ export function ansiWordmark(pad = "") { // Fork: the "c" of the codev wordmark, drawn as the compact badge on the // `codev --mini` entry and exit splashes. Upstream badges those with the "o" of -// the `go` mark below, which is still the background pulse's wordmark. -export const badge = logo.left.slice(1).map((line) => line.slice(0, 4)) +// the `go` mark below, which is still the background pulse's wordmark. The +// interior shadow-fill marks (`_`) are stripped to spaces so the badge is a +// flat brand-colored outline with no shadow. +export const badge = logo.left.slice(1).map((line) => line.slice(0, 4).replaceAll("_", " ")) export const go = { left: [" ", "█▀▀▀", "█_^█", "▀▀▀▀"],