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", 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}> 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(); + }); +});