From 9d7880f39fed17ce7708124725c28bb598b89865 Mon Sep 17 00:00:00 2001 From: minhn4 Date: Thu, 30 Jul 2026 20:43:00 +0700 Subject: [PATCH 1/3] feat(banner): adopt CoDev Code's "codev" wordmark, theme-aware Replace the block CODEV logo with the lowercase "codev" pixel wordmark from codev-code (packages/tui/src/logo.ts): "co" on the left, "dev" on the right, no drop shadow, bold. "dev" is always the brand red (#ee0033); "co" is the brand navy (#19224c) on a light terminal and the terminal's default foreground on a dark or unknown one -- the readable counterpart of navy-on-white, matching how codev-code's own CLI banner handles the undetectable case. Add src/lib/terminal-theme.ts#terminalIsLight, a synchronous best-effort light/dark detector via COLORFGBG (the only signal that avoids an escape-sequence round trip that would fight Ink for the TTY); it returns null when unknown so the banner falls back to the always-readable default foreground. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/components/Banner.tsx | 40 +++++++++++++++++++++----------- src/lib/terminal-theme.ts | 22 ++++++++++++++++++ tests/components/Banner.test.tsx | 7 +++--- tests/lib/terminal-theme.test.ts | 27 +++++++++++++++++++++ 4 files changed, 80 insertions(+), 16 deletions(-) create mode 100644 src/lib/terminal-theme.ts create mode 100644 tests/lib/terminal-theme.test.ts diff --git a/src/components/Banner.tsx b/src/components/Banner.tsx index d49433c..02f6502 100644 --- a/src/components/Banner.tsx +++ b/src/components/Banner.tsx @@ -1,24 +1,38 @@ import { Box, Text } from "ink"; import { VERSION } from "@/lib/const.js"; +import { terminalIsLight } from "@/lib/terminal-theme.js"; -const LOGO = [ - " ██████╗ ██████╗ ██████╗ ███████╗██╗ ██╗", - "██╔════╝██╔═══██╗██╔══██╗██╔════╝██║ ██║", - "██║ ██║ ██║██║ ██║█████╗ ██║ ██║", - "██║ ██║ ██║██║ ██║██╔══╝ ╚██╗ ██╔╝", - "╚██████╗╚██████╔╝██████╔╝███████╗ ╚████╔╝ ", - " ╚═════╝ ╚═════╝ ╚═════╝ ╚══════╝ ╚═══╝ ", -].join("\n"); +// CoDev Code's lowercase "codev" pixel wordmark (codev-code +// packages/tui/src/logo.ts): "co" on the left, "dev" on the right, no drop +// shadow. Kept split so each half takes its own brand color. +const CO = [" ", "█▀▀▀ █▀▀█", "█ █ █", "▀▀▀▀ ▀▀▀▀"]; +const DEV = [ + " ▄ ", + "█▀▀█ █▀▀█ █ █", + "█ █ █▀▀▀ █ █", + "▀▀▀▀ ▀▀▀▀ ▀▀ ", +]; -const LOGO_WIDTH = 42; +// Brand palette from the CoDev landing page (--color-brand-navy / -red), the +// same values codev-code's logo uses. +const BRAND_NAVY = "#19224c"; +const BRAND_RED = "#ee0033"; export function Banner() { + // Match codev-code's TUI logo: "co" is the brand navy on a light terminal + // and the terminal's default foreground on a dark or unknown one — the + // readable counterpart of navy-on-white. "dev" is always the brand red. + const coColor = terminalIsLight() ? BRAND_NAVY : undefined; + return ( - - {LOGO} - - + {CO.map((left, index) => ( + + {left}{" "} + {DEV[index]} + + ))} + {"AI Coding Agent Hub "} v{VERSION} diff --git a/src/lib/terminal-theme.ts b/src/lib/terminal-theme.ts new file mode 100644 index 0000000..6f1ecdf --- /dev/null +++ b/src/lib/terminal-theme.ts @@ -0,0 +1,22 @@ +// Best-effort, synchronous detection of whether the terminal has a light +// background. The only signal available without an escape-sequence round trip +// (which would fight Ink for the TTY) is the `COLORFGBG` variable that Konsole, +// rxvt, and a handful of other terminals export. When it is absent or +// unparseable we return null ("unknown") and callers fall back to the +// terminal's own default foreground, which is readable on any background. +export function terminalIsLight( + env: NodeJS.ProcessEnv = process.env, +): boolean | null { + const fgbg = env.COLORFGBG; + if (!fgbg) return null; + + // `COLORFGBG` is "fg;bg" or, on rxvt, "fg;default;bg" — the background is + // always the last field. + const parts = fgbg.split(";"); + const bg = Number(parts[parts.length - 1]); + if (!Number.isInteger(bg)) return null; + + // Standard ANSI palette convention (the same one vim uses to pick + // `background`): 0–6 and 8 are dark, everything else is light. + return !(bg <= 6 || bg === 8); +} diff --git a/tests/components/Banner.test.tsx b/tests/components/Banner.test.tsx index c778e4e..5917856 100644 --- a/tests/components/Banner.test.tsx +++ b/tests/components/Banner.test.tsx @@ -8,12 +8,13 @@ afterEach(() => { }); describe("Banner", () => { - test("renders the CODEV ASCII logo", () => { + test("renders the CODEV wordmark", () => { const { lastFrame } = render(); const output = lastFrame() ?? ""; - expect(output).toContain("██████╗"); - expect(output).toContain("╚═════╝"); + // The lowercase "codev" pixel wordmark ported from CoDev Code. + expect(output).toContain("█▀▀▀ █▀▀█"); + expect(output).toContain("▀▀▀▀ ▀▀▀▀"); }); test("renders the subtitle", () => { diff --git a/tests/lib/terminal-theme.test.ts b/tests/lib/terminal-theme.test.ts new file mode 100644 index 0000000..69aeafc --- /dev/null +++ b/tests/lib/terminal-theme.test.ts @@ -0,0 +1,27 @@ +import { describe, expect, test } from "vitest"; +import { terminalIsLight } from "@/lib/terminal-theme.js"; + +describe("terminalIsLight", () => { + test("returns null when COLORFGBG is unset", () => { + expect(terminalIsLight({})).toBeNull(); + }); + + test("detects a light background (bg 15 / 7)", () => { + expect(terminalIsLight({ COLORFGBG: "0;15" })).toBe(true); + expect(terminalIsLight({ COLORFGBG: "0;7" })).toBe(true); + }); + + test("detects a dark background (bg 0 / 8)", () => { + expect(terminalIsLight({ COLORFGBG: "15;0" })).toBe(false); + expect(terminalIsLight({ COLORFGBG: "15;8" })).toBe(false); + }); + + test("reads the last field for the rxvt 'fg;default;bg' form", () => { + expect(terminalIsLight({ COLORFGBG: "0;default;15" })).toBe(true); + expect(terminalIsLight({ COLORFGBG: "15;default;0" })).toBe(false); + }); + + test("returns null when the background field is not a number", () => { + expect(terminalIsLight({ COLORFGBG: "0;abc" })).toBeNull(); + }); +}); From cb24900b00125f9d1564b27207872c1286722fdf Mon Sep 17 00:00:00 2001 From: minhn4 Date: Thu, 30 Jul 2026 20:47:27 +0700 Subject: [PATCH 2/3] fix(banner): drop top padding so the wordmark hugs the command above The "codev" wordmark's top row is near-empty (just the tittle of the "d"), so the pre-existing padding={1} top line on each app's outer Box made the banner read as floating two lines below the user's command. Switch those wrappers to paddingX + paddingBottom, keeping side and bottom spacing while removing the top gap. The block logo hid this because its first row was solid. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/DoctorApp.tsx | 2 +- src/LoginApp.tsx | 4 ++-- src/ModelApp.tsx | 2 +- src/SetupApp.tsx | 2 +- src/SkillPullApp.tsx | 2 +- src/SkillPushApp.tsx | 2 +- src/UpdateApp.tsx | 2 +- 7 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/DoctorApp.tsx b/src/DoctorApp.tsx index 515f353..ff02e25 100644 --- a/src/DoctorApp.tsx +++ b/src/DoctorApp.tsx @@ -342,7 +342,7 @@ export function DoctorApp({ force = false }: DoctorAppProps) { : []; return ( - + {phase === "preparing" && ( diff --git a/src/LoginApp.tsx b/src/LoginApp.tsx index 1fd4382..5439062 100644 --- a/src/LoginApp.tsx +++ b/src/LoginApp.tsx @@ -129,7 +129,7 @@ function AdminLoginApp({ } return ( - + @@ -168,7 +168,7 @@ function SsoLoginApp({ force = false }: { force?: boolean }) { ); return ( - + {phase === "preparing" && ( diff --git a/src/ModelApp.tsx b/src/ModelApp.tsx index 6938538..157f6b8 100644 --- a/src/ModelApp.tsx +++ b/src/ModelApp.tsx @@ -247,7 +247,7 @@ export function ModelApp() { }, [phase, creds, tools]); return ( - + {phase === "loading" && ( diff --git a/src/SetupApp.tsx b/src/SetupApp.tsx index c2498ed..a3ab848 100644 --- a/src/SetupApp.tsx +++ b/src/SetupApp.tsx @@ -623,7 +623,7 @@ export function SetupApp({ mode }: SetupAppProps) { const inPreflight = step === "preflight"; return ( - + {preflight.length > 0 && ( diff --git a/src/SkillPullApp.tsx b/src/SkillPullApp.tsx index 247d1d6..e54935f 100644 --- a/src/SkillPullApp.tsx +++ b/src/SkillPullApp.tsx @@ -115,7 +115,7 @@ export function SkillPullApp({ const title = skillName ? `Install ${skillName} skill` : "Install skill"; return ( - + {title}}> diff --git a/src/SkillPushApp.tsx b/src/SkillPushApp.tsx index 9b2baad..1d0da55 100644 --- a/src/SkillPushApp.tsx +++ b/src/SkillPushApp.tsx @@ -222,7 +222,7 @@ export function SkillPushApp({ const started = Object.values(stepState).some((s) => s !== "pending"); return ( - + {/* Step 1 — archive preview + confirm. Stays visible (dimmed) through diff --git a/src/UpdateApp.tsx b/src/UpdateApp.tsx index db47dde..85fe793 100644 --- a/src/UpdateApp.tsx +++ b/src/UpdateApp.tsx @@ -26,7 +26,7 @@ export function UpdateApp() { ); return ( - + Updating packages}> From cef8ba65788339c2b26df4f8490bb5c07ba40e2b Mon Sep 17 00:00:00 2001 From: minhn4 Date: Thu, 30 Jul 2026 20:49:47 +0700 Subject: [PATCH 3/3] chore: bump version Co-Authored-By: Claude Opus 4.8 (1M context) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index e2b041a..c938591 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "codev-ai", - "version": "0.5.2", + "version": "0.5.3", "description": "CoDev — AI Coding Agent Hub. Install, configure, and manage multiple AI coding agents.", "keywords": [ "ai",