diff --git a/README.md b/README.md index f26364a..b064677 100644 --- a/README.md +++ b/README.md @@ -160,7 +160,7 @@ Markdown responses render with per-language syntax highlighting directly in the - Slash-command autocomplete — type `/` for a fuzzy-filtered live suggestion strip - Collapsible tool output and thinking blocks — click to expand - Full mouse support: click, scroll, drag-select -- 13 themes: `dark`, `light`, `dracula`, `nord`, `ayu`, `catppuccin`, `gruvbox`, `neon`, `synthwave`, `ember`, `matrix`, `cobalt`, `midnight` (`/theme`) +- 14 themes: `dark`, `light`, `dracula`, `nord`, `ayu`, `catppuccin`, `gruvbox`, `neon`, `synthwave`, `ember`, `matrix`, `cobalt`, `midnight`, `tokyo-night` (`/theme`) - Vim keybindings (`/vimmode on`) - Sidebar: usage, context window fill, MCP servers, routing analytics - Diffs render as colored, syntax-highlighted unified-diff blocks — including multi-file patches diff --git a/assets/tokyo-night-theme.png b/assets/tokyo-night-theme.png new file mode 100644 index 0000000..b32cd56 Binary files /dev/null and b/assets/tokyo-night-theme.png differ diff --git a/src/auth/credentials.ts b/src/auth/credentials.ts index d7fa73c..afe0141 100644 --- a/src/auth/credentials.ts +++ b/src/auth/credentials.ts @@ -28,7 +28,7 @@ export interface Credentials { export interface Config { baseUrl: string; // KlaatAI API base URL routingDisplay: "minimal" | "full" | "off"; - theme: string; // Theme name: dark | light | dracula | nord | ayu | catppuccin | gruvbox + theme: string; // Theme name from THEME_NAMES (for example dark, light, tokyo-night) vimMode: boolean; // Vim-style key bindings in the input field /** * Disable TLS certificate verification (corporate MITM proxies whose root diff --git a/src/engine/theme.test.ts b/src/engine/theme.test.ts new file mode 100644 index 0000000..5a67ea2 --- /dev/null +++ b/src/engine/theme.test.ts @@ -0,0 +1,42 @@ +import { describe, expect, test } from "bun:test"; +import { + THEME_DESCRIPTIONS, + THEME_NAMES, + TOKYO_NIGHT_PALETTE, + getPalette, +} from "./theme.js"; + +describe("Tokyo Night theme", () => { + test("is registered with its description and palette", () => { + expect(THEME_NAMES).toContain("tokyo-night"); + expect(THEME_DESCRIPTIONS["tokyo-night"]).toContain("Tokyo Night"); + expect(getPalette("tokyo-night")).toBe(TOKYO_NIGHT_PALETTE); + }); + + test("uses valid 256-color slots", () => { + const numericSlots = Object.values(TOKYO_NIGHT_PALETTE) + .filter((value): value is number => typeof value === "number"); + + expect(numericSlots.length).toBeGreaterThan(0); + for (const color of numericSlots) { + expect(Number.isInteger(color)).toBe(true); + expect(color).toBeGreaterThanOrEqual(0); + expect(color).toBeLessThanOrEqual(255); + } + }); + + test("uses supported hex colors and the terminal background", () => { + const hexSlots = [ + TOKYO_NIGHT_PALETTE.accent, + TOKYO_NIGHT_PALETTE.dimText, + TOKYO_NIGHT_PALETTE.userColor, + TOKYO_NIGHT_PALETTE.border, + TOKYO_NIGHT_PALETTE.thumb, + ]; + + for (const color of hexSlots) { + expect(color).toMatch(/^#[0-9a-f]{6}$/i); + } + expect(TOKYO_NIGHT_PALETTE.bg).toBeNull(); + }); +}); diff --git a/src/engine/theme.ts b/src/engine/theme.ts index 384c29a..98ec12b 100644 --- a/src/engine/theme.ts +++ b/src/engine/theme.ts @@ -328,6 +328,27 @@ export const MIDNIGHT_PALETTE: ThemePalette = { assistantFg: 117, // sky blue }; +/** Tokyo Night — deep navy with cool blue and green accents */ +export const TOKYO_NIGHT_PALETTE: ThemePalette = { + accent: "#7aa2f7", // blue + dimText: "#7f87b2", // dark foreground + userColor: "#9ece6a", // green + border: "#3b4261", // terminal black + thumb: "#545c7e", + bg: null, + inputBg: 234, + chatFg: 189, // pale blue-white + mutedFg: 103, // muted lavender-blue + toolFg: 110, // cyan-blue + codeFg: 111, // bright blue + codeBg: 235, + headingFg: "white", + sidebarLabel: 103, + sidebarValue: 189, + userFg: 149, // green + assistantFg: 111, // blue +}; + export type Theme = | "dark" | "light" @@ -341,11 +362,12 @@ export type Theme = | "ember" | "matrix" | "cobalt" - | "midnight"; + | "midnight" + | "tokyo-night"; export const THEME_NAMES: Theme[] = [ "dark", "light", "dracula", "nord", "ayu", "catppuccin", "gruvbox", - "neon", "synthwave", "ember", "matrix", "cobalt", "midnight", + "neon", "synthwave", "ember", "matrix", "cobalt", "midnight", "tokyo-night", ]; export const THEME_DESCRIPTIONS: Record = { @@ -362,6 +384,7 @@ export const THEME_DESCRIPTIONS: Record = { matrix: "Matrix — green phosphor terminal aesthetic", cobalt: "Cobalt — deep blue with golden highlights", midnight: "Midnight — indigo depths with sky blue & lavender", + "tokyo-night": "Tokyo Night — deep navy with cool blue & green", }; export function getPalette(theme: Theme): ThemePalette { @@ -378,6 +401,7 @@ export function getPalette(theme: Theme): ThemePalette { case "matrix": return MATRIX_PALETTE; case "cobalt": return COBALT_PALETTE; case "midnight": return MIDNIGHT_PALETTE; + case "tokyo-night": return TOKYO_NIGHT_PALETTE; default: return DARK_PALETTE; } } @@ -446,4 +470,3 @@ export async function detectTheme(): Promise { timer = setTimeout(() => finish("dark"), 200); }); } -