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 package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
2 changes: 1 addition & 1 deletion src/DoctorApp.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -342,7 +342,7 @@ export function DoctorApp({ force = false }: DoctorAppProps) {
: [];

return (
<Box flexDirection="column" padding={1}>
<Box flexDirection="column" paddingX={1} paddingBottom={1}>
<Banner />
<Frame tag="CoDev">
{phase === "preparing" && (
Expand Down
4 changes: 2 additions & 2 deletions src/LoginApp.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ function AdminLoginApp({
}

return (
<Box flexDirection="column" padding={1}>
<Box flexDirection="column" paddingX={1} paddingBottom={1}>
<Banner />
<Frame tag="CoDev">
<Step active title={adminLoginTitle()}>
Expand Down Expand Up @@ -168,7 +168,7 @@ function SsoLoginApp({ force = false }: { force?: boolean }) {
);

return (
<Box flexDirection="column" padding={1}>
<Box flexDirection="column" paddingX={1} paddingBottom={1}>
<Banner />
<Frame tag="CoDev">
{phase === "preparing" && (
Expand Down
2 changes: 1 addition & 1 deletion src/ModelApp.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -247,7 +247,7 @@ export function ModelApp() {
}, [phase, creds, tools]);

return (
<Box flexDirection="column" padding={1}>
<Box flexDirection="column" paddingX={1} paddingBottom={1}>
<Banner />
<Frame tag="CoDev">
{phase === "loading" && (
Expand Down
2 changes: 1 addition & 1 deletion src/SetupApp.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -623,7 +623,7 @@ export function SetupApp({ mode }: SetupAppProps) {
const inPreflight = step === "preflight";

return (
<Box flexDirection="column" padding={1}>
<Box flexDirection="column" paddingX={1} paddingBottom={1}>
<Banner />
<Frame tag="CoDev">
{preflight.length > 0 && (
Expand Down
2 changes: 1 addition & 1 deletion src/SkillPullApp.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ export function SkillPullApp({
const title = skillName ? `Install ${skillName} skill` : "Install skill";

return (
<Box flexDirection="column" padding={1}>
<Box flexDirection="column" paddingX={1} paddingBottom={1}>
<Banner />
<Frame tag="CoDev">
<Step active title={<Text bold>{title}</Text>}>
Expand Down
2 changes: 1 addition & 1 deletion src/SkillPushApp.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -222,7 +222,7 @@ export function SkillPushApp({
const started = Object.values(stepState).some((s) => s !== "pending");

return (
<Box flexDirection="column" padding={1}>
<Box flexDirection="column" paddingX={1} paddingBottom={1}>
<Banner />
<Frame tag="CoDev">
{/* Step 1 — archive preview + confirm. Stays visible (dimmed) through
Expand Down
2 changes: 1 addition & 1 deletion src/UpdateApp.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ export function UpdateApp() {
);

return (
<Box flexDirection="column" padding={1}>
<Box flexDirection="column" paddingX={1} paddingBottom={1}>
<Banner />
<Frame tag="CoDev">
<Step active title={<Text bold>Updating packages</Text>}>
Expand Down
40 changes: 27 additions & 13 deletions src/components/Banner.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<Box alignItems="flex-start" flexDirection="column">
<Text bold color="cyan">
{LOGO}
</Text>
<Box marginBottom={1} justifyContent="center" width={LOGO_WIDTH}>
{CO.map((left, index) => (
<Text key={left + DEV[index]} bold>
<Text color={coColor}>{left}</Text>{" "}
<Text color={BRAND_RED}>{DEV[index]}</Text>
</Text>
))}
<Box marginBottom={1}>
<Text>{"AI Coding Agent Hub "}</Text>
<Text dimColor>v{VERSION}</Text>
</Box>
Expand Down
22 changes: 22 additions & 0 deletions src/lib/terminal-theme.ts
Original file line number Diff line number Diff line change
@@ -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);
}
7 changes: 4 additions & 3 deletions tests/components/Banner.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,13 @@ afterEach(() => {
});

describe("Banner", () => {
test("renders the CODEV ASCII logo", () => {
test("renders the CODEV wordmark", () => {
const { lastFrame } = render(<Banner />);

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", () => {
Expand Down
27 changes: 27 additions & 0 deletions tests/lib/terminal-theme.test.ts
Original file line number Diff line number Diff line change
@@ -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();
});
});
Loading