diff --git a/CHANGELOG.md b/CHANGELOG.md index 85c74433a4..e72bd98042 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,14 @@ 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`, 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 - **`Ctrl+C` interrupts a running turn** — while the agent is working, `Ctrl+C` diff --git a/DIVERGENCES.md b/DIVERGENCES.md index 9096aaf234..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 | 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,6 +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` (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 cb0da9b5eb..15168414bc 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 @@ -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 @@ -177,11 +179,13 @@ 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. It is a flat outline with no shadow. + const mark_fg = input.theme.mark 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 @@ -189,8 +193,7 @@ 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, }) } @@ -209,7 +212,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 " @@ -219,8 +222,7 @@ 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, }) } diff --git a/packages/opencode/src/cli/cmd/run/theme.ts b/packages/opencode/src/cli/cmd/run/theme.ts index e4fb315426..6c66ed15b3 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. The badge is a flat + // outline with no shadow, so there is no matching shadow color. + mark: ColorInput } export type RunFooterTheme = { @@ -486,7 +490,14 @@ 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) return { @@ -494,6 +505,9 @@ function splashTheme(theme: TuiThemeCurrent, indexed: RGBA[]): RunSplashTheme { right, leftShadow: splashShadow(indexed, theme.background, left, 0.14), rightShadow: splashShadow(indexed, theme.background, right, 0.14), + // 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], } } @@ -601,6 +615,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) 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), @@ -634,6 +651,7 @@ 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, }, block: { highlight: seed.highlight, @@ -680,7 +698,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 4102dea1c9..cb1d024ab6 100644 --- a/packages/opencode/test/cli/run/theme.test.ts +++ b/packages/opencode/test/cli/run/theme.test.ts @@ -74,6 +74,9 @@ test("returns syntax styles and indexed splash colors", async () => { expectIndexed(theme.splash.right) expectIndexed(theme.splash.leftShadow) expectIndexed(theme.splash.rightShadow) + // 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]) 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 085f59e0eb..c48d9918ff 100644 --- a/packages/tui/src/logo.ts +++ b/packages/tui/src/logo.ts @@ -35,6 +35,13 @@ 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. 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: [" ", "█▀▀▀", "█_^█", "▀▀▀▀"], right: [" ", "█▀▀█", "█__█", "▀▀▀▀"],