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
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ file is the human-readable summary of what changed and why.
rename (PR #212) can't attribute a `netgate` config as CoDev-managed.

- **Custom providers get a name, not an id** — the "Custom provider" flow now
asks for an optional provider *name* ("Acme AI Gateway") instead of a raw
asks for an optional provider _name_ ("Acme AI Gateway") instead of a raw
id. The id is slugged from the name (`acme-ai-gateway`, `[a-z0-9-]`, 32
chars max) and previewed live under the field; the name is written as the
provider block's display name, so the model picker shows it instead of the
Expand Down
10 changes: 5 additions & 5 deletions DIVERGENCES.md

Large diffs are not rendered by default.

32 changes: 15 additions & 17 deletions packages/opencode/src/cli/cmd/run/theme.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
// palette if detection fails.
import { RGBA, SyntaxStyle, type CliRenderer, type ColorInput, type TerminalColors } from "@opentui/core"
import type { TuiThemeCurrent } from "@opencode-ai/plugin/tui"
import { brand } from "@/cli/logo"
import type { EntryKind } from "./types"

type Tone = {
Expand All @@ -21,8 +22,7 @@ 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
// Fork: the `codev --mini` badge glyph, in the brand red. The badge is a flat
// outline with no shadow, so there is no matching shadow color.
mark: ColorInput
}
Expand Down Expand Up @@ -490,24 +490,24 @@ function quantizeTheme(theme: TuiThemeCurrent, indexed: RGBA[]): TuiThemeCurrent
}
}

// 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
// Fork: the landing page draws the compact "C" mark in the brand red in both
// light and dark (codev-landing-page app/icon.svg — the same glyph the badge
// renders), so the mini badge uses that exact truecolor value 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_MARK = rgba(brand.red)

function splashTheme(theme: TuiThemeCurrent, indexed: RGBA[], pick: "dark" | "light"): RunSplashTheme {
function splashTheme(theme: TuiThemeCurrent, indexed: RGBA[]): 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],
// Brand truecolor, left unquantized so it renders as the exact brand red
// instead of the nearest palette cell.
mark: BRAND_MARK,
}
}

Expand Down Expand Up @@ -615,9 +615,6 @@ 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 @@ -651,7 +648,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,
// Fork: the brand mark stays the exact brand red when palette detection fails.
mark: BRAND_MARK,
},
block: {
highlight: seed.highlight,
Expand Down Expand Up @@ -698,7 +696,7 @@ export async function resolveRunTheme(renderer: CliRenderer): Promise<RunTheme>
return map(
footerTheme,
scrollbackTheme,
splashTheme(scrollbackTheme, indexed, pick),
splashTheme(scrollbackTheme, indexed),
syntax,
shared.generateSubtleSyntax(syntaxTheme),
)
Expand Down
2 changes: 1 addition & 1 deletion packages/opencode/src/cli/ui.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ export function logo(pad?: string) {
if (!process.stdout.isTTY && !process.stderr.isTTY) {
return wordmark.map((row) => (pad ?? "") + row).join(EOL)
}
// Bold codev brand teal with the drop shadow, matching the TUI home banner.
// Bold, no shadow, "dev" in the brand red — matching the TUI home banner.
return ansiWordmark(pad).join(EOL)
}

Expand Down
7 changes: 4 additions & 3 deletions packages/opencode/test/cli/run/theme.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,9 +74,10 @@ 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])
// The badge is the exact brand red (truecolor, not a palette cell) so it
// matches the landing page's compact "C" mark rather than the nearest
// indexed color.
expect(expectRgba(theme.splash.mark).toInts().slice(0, 3)).toEqual([238, 0, 51])
expectIndexed(theme.block.highlight)
expectIndexed(theme.block.warning)
expectRgba(theme.footer.highlight)
Expand Down
67 changes: 16 additions & 51 deletions packages/tui/src/component/logo.tsx
Original file line number Diff line number Diff line change
@@ -1,61 +1,26 @@
import { TextAttributes } from "@opentui/core"
import { For, type JSX } from "solid-js"
import { tint, useTheme } from "../context/theme"
import { logo } from "../logo"
import { For } from "solid-js"
import { useTheme } from "../context/theme"
import { brand, wordmarkHalves } from "../logo"

// Fork: lowercase "codev" wordmark (half the height of the block banner) in the
// theme's brand secondary (teal in dark, purple in light), with shadow marks.
// Fork: lowercase "codev" wordmark (half the height of the block banner),
// matching the landing page wordmark (codev-landing-page
// app/components/PixelWordmark.tsx): "co" in the brand navy on light and the
// theme foreground on dark, "dev" in the brand red, with no drop shadow.
export function Logo() {
const { theme } = useTheme()

const renderLine = (line: string, bold: boolean): JSX.Element[] => {
const brand = theme.secondary
const shadow = tint(theme.background, brand, 0.25)
const attrs = bold ? TextAttributes.BOLD : undefined
return Array.from(line).map((char) => {
if (char === "_") {
return (
<text fg={brand} bg={shadow} attributes={attrs} selectable={false}>
{" "}
</text>
)
}
if (char === "^") {
return (
<text fg={brand} bg={shadow} attributes={attrs} selectable={false}>
▀
</text>
)
}
if (char === "~") {
return (
<text fg={shadow} attributes={attrs} selectable={false}>
▀
</text>
)
}
if (char === ",") {
return (
<text fg={shadow} attributes={attrs} selectable={false}>
▄
</text>
)
}
return (
<text fg={brand} attributes={attrs} selectable={false}>
{char}
</text>
)
})
}
const { theme, mode } = useTheme()

return (
<box>
<For each={logo.left}>
{(line, index) => (
<For each={wordmarkHalves}>
{(row) => (
<box flexDirection="row" gap={1}>
<box flexDirection="row">{renderLine(line, true)}</box>
<box flexDirection="row">{renderLine(logo.right[index()], true)}</box>
<text fg={mode() === "light" ? brand.navy : theme.text} attributes={TextAttributes.BOLD} selectable={false}>
{row.left}
</text>
<text fg={brand.red} attributes={TextAttributes.BOLD} selectable={false}>
{row.right}
</text>
</box>
)}
</For>
Expand Down
48 changes: 27 additions & 21 deletions packages/tui/src/logo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,34 +5,40 @@ export const logo = {
right: [" ▄ ", "█▀▀█ █▀▀█ █__█", "█__█ █^^^ █__█", "▀▀▀▀ ▀▀▀▀ ▀▀ "],
}

// Fork: the lowercase "codev" wordmark composed into plain lines (shadow marks
// substituted to glyphs) for CLI banners that color each line uniformly.
// Fork: the landing-page brand palette (codev-landing-page app/globals.css —
// --color-brand-navy / --color-brand-red). The wordmark draws "co" in navy and
// "dev" in red, matching codev-landing-page app/components/PixelWordmark.tsx.
export const brand = {
navy: "#19224c",
red: "#ee0033",
}

// Fork: the lowercase "codev" wordmark with the shadow marks substituted to
// glyphs, kept split into its two halves so each takes its own brand color.
const substituteMarks = (line: string) =>
Array.from(line, (char) =>
char === "_" ? " " : char === "^" || char === "~" ? "▀" : char === "," ? "▄" : char,
).join("")
export const wordmark = logo.left.map(
(line, index) => `${substituteMarks(line)} ${substituteMarks(logo.right[index] ?? "")}`,
)
export const wordmarkHalves = logo.left.map((line, index) => ({
left: substituteMarks(line),
right: substituteMarks(logo.right[index] ?? ""),
}))

// Fork: the wordmark composed into plain lines, for banners that cannot color
// the halves separately.
export const wordmark = wordmarkHalves.map((row) => `${row.left} ${row.right}`)

// Fork: ANSI-colored lowercase wordmark for CLI banners, matching the TUI home
// banner: bold brand teal with a drop shadow behind the mark cells. The shadow
// is tint(theme dark background #0a0a0a, teal, 0.25) = #0a2c34.
// Fork: ANSI-colored lowercase wordmark for CLI banners, matching the landing
// page wordmark: bold, no drop shadow, with "dev" in the brand red. "co" keeps
// the terminal's default foreground — the CLI banner cannot detect light vs
// dark, and the default foreground is the readable counterpart of the landing
// page's navy-on-white / white-on-dark.
export function ansiWordmark(pad = "") {
const reset = "\x1b[0m"
const teal = "\x1b[1m\x1b[38;2;8;145;178m"
const shadowFg = "\x1b[38;2;10;44;52m"
const shadowBg = "\x1b[48;2;10;44;52m"
const draw = (line: string) =>
Array.from(line, (char) => {
if (char === "_") return `${shadowBg} ${reset}`
if (char === "^") return `${teal}${shadowBg}▀${reset}`
if (char === "~") return `${shadowFg}▀${reset}`
if (char === ",") return `${shadowFg}▄${reset}`
if (char === " ") return " "
return `${teal}${char}${reset}`
}).join("")
return logo.left.map((line, index) => `${pad}${draw(line)} ${draw(logo.right[index] ?? "")}`)
const bold = "\x1b[1m"
// brand.red as truecolor.
const red = "\x1b[1m\x1b[38;2;238;0;51m"
return wordmarkHalves.map((row) => `${pad}${bold}${row.left}${reset} ${red}${row.right}${reset}`)
}

// Fork: the "c" of the codev wordmark, drawn as the compact badge on the
Expand Down
Loading