Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down
4 changes: 3 additions & 1 deletion DIVERGENCES.md
Original file line number Diff line number Diff line change
Expand Up @@ -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. |
Expand All @@ -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,<leader>q` → `ctrl+d,<leader>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, `<leader>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 `"<leader>b"` → `"ctrl+b,<leader>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. |
Expand Down
26 changes: 14 additions & 12 deletions packages/opencode/src/cli/cmd/run/splash.ts
Original file line number Diff line number Diff line change
@@ -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.
//
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -177,20 +179,21 @@ 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

for (let i = 0; i < mark.length; i += 1) {
draw(lines, mark[i] ?? "", {
left: 0,
top: top + i,
fg: left,
shadow: leftShadow,
fg: mark_fg,
})
}

Expand All @@ -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 "
Expand All @@ -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,
})
}

Expand Down
22 changes: 20 additions & 2 deletions packages/opencode/src/cli/cmd/run/theme.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down Expand Up @@ -486,14 +490,24 @@ 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 {
left,
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],
}
}

Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -680,7 +698,7 @@ export async function resolveRunTheme(renderer: CliRenderer): Promise<RunTheme>
return map(
footerTheme,
scrollbackTheme,
splashTheme(scrollbackTheme, indexed),
splashTheme(scrollbackTheme, indexed, pick),
syntax,
shared.generateSubtleSyntax(syntaxTheme),
)
Expand Down
3 changes: 3 additions & 0 deletions packages/opencode/test/cli/run/theme.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
7 changes: 7 additions & 0 deletions packages/tui/src/logo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: [" ", "█▀▀█", "█__█", "▀▀▀▀"],
Expand Down
Loading