From ecfd3457f959d1060eb6b11b7254003e2ee948c4 Mon Sep 17 00:00:00 2001 From: w287346141 <287346141@qq.com> Date: Sun, 24 May 2026 13:43:45 +0800 Subject: [PATCH 1/3] Improve localized theme picker readability --- src/cli/ui/Select.tsx | 24 +++++++++++++++++++++--- src/cli/ui/ThemePicker.tsx | 30 +++++++++++++++++++++++++++++- src/i18n/EN.ts | 10 +++++++++- src/i18n/types.ts | 8 ++++++++ src/i18n/zh-CN.ts | 12 ++++++++++-- tests/ui-theme-picker.test.tsx | 18 +++++++++++++++++- 6 files changed, 94 insertions(+), 8 deletions(-) diff --git a/src/cli/ui/Select.tsx b/src/cli/ui/Select.tsx index 0c0a5cc..684e04d 100644 --- a/src/cli/ui/Select.tsx +++ b/src/cli/ui/Select.tsx @@ -26,6 +26,8 @@ export interface SingleSelectProps { footer?: string; /** Render item hints on the same row as the label instead of a second row. */ inlineHints?: boolean; + /** Render labels, hints, and footer with themed colors instead of terminal-dim text. */ + vividHints?: boolean; /** Ignore matching keystrokes so an enclosing component can own them. */ ignoreKey?: (ev: KeyEvent) => boolean; } @@ -38,6 +40,7 @@ export function SingleSelect({ onCancel, footer, inlineHints = false, + vividHints = false, ignoreKey, }: SingleSelectProps) { const color = useColor(); @@ -74,11 +77,14 @@ export function SingleSelect({ marker={i === index ? "▸" : " "} color={color} inlineHint={inlineHints} + vividHint={vividHints} /> ))} {footer ? ( - {footer} + + {footer} + ) : null} @@ -168,14 +174,22 @@ function SelectRow({ marker, color, inlineHint = false, + vividHint = false, }: { item: SelectItem; active: boolean; marker: string; color: UiColor; inlineHint?: boolean; + vividHint?: boolean; }) { - const rowColor = item.disabled ? color.info : active ? color.primary : undefined; + const rowColor = item.disabled + ? color.info + : active + ? color.primary + : vividHint + ? color.info + : undefined; const labelText = `${marker} ${item.label}`; if (inlineHint) { return ( @@ -183,7 +197,11 @@ function SelectRow({ {labelText} - {item.hint ? {` ${item.hint}`} : null} + {item.hint ? ( + + {` ${item.hint}`} + + ) : null} ); } diff --git a/src/cli/ui/ThemePicker.tsx b/src/cli/ui/ThemePicker.tsx index 746c094..166c873 100644 --- a/src/cli/ui/ThemePicker.tsx +++ b/src/cli/ui/ThemePicker.tsx @@ -20,7 +20,7 @@ export function ThemePicker({ const choices: ThemeChoice[] = ["auto", ...listThemeNames()]; const items: SelectItem[] = choices.map((value) => ({ value, - label: value, + label: themeChoiceLabel(value), hint: describeTheme(value, currentPreference, activeTheme), })); @@ -33,11 +33,39 @@ export function ThemePicker({ onSubmit={(value) => onChoose({ kind: "select", value })} onCancel={() => onChoose({ kind: "quit" })} footer={t("themePicker.footer")} + inlineHints + vividHints /> ); } +function themeChoiceLabel(value: ThemeChoice): string { + const label = t(themeLabelKey(value)); + return label === value ? value : `${value} - ${label}`; +} + +function themeLabelKey(value: ThemeChoice): string { + switch (value) { + case "auto": + return "themePicker.autoLabel"; + case "default": + return "themePicker.defaultLabel"; + case "dark": + return "themePicker.darkLabel"; + case "light": + return "themePicker.lightLabel"; + case "tokyo-night": + return "themePicker.tokyoNightLabel"; + case "github-dark": + return "themePicker.githubDarkLabel"; + case "github-light": + return "themePicker.githubLightLabel"; + case "high-contrast": + return "themePicker.highContrastLabel"; + } +} + function describeTheme( value: ThemeChoice, currentPreference: ThemeChoice, diff --git a/src/i18n/EN.ts b/src/i18n/EN.ts index 0ac9539..6dbfc3d 100644 --- a/src/i18n/EN.ts +++ b/src/i18n/EN.ts @@ -487,7 +487,15 @@ export const EN: TranslationSchema = { footer: "↑↓ pick · ⏎ confirm · esc cancel", currentPref: "current preference", activeNow: "active now", - autoDesc: "use REASONIX_THEME or default", + autoDesc: "use CARBONCODE_THEME or default", + autoLabel: "auto", + defaultLabel: "default", + darkLabel: "dark", + lightLabel: "light", + tokyoNightLabel: "tokyo-night", + githubDarkLabel: "github-dark", + githubLightLabel: "github-light", + highContrastLabel: "high-contrast", }, planFlow: { approveCardTitle: "Approve plan", diff --git a/src/i18n/types.ts b/src/i18n/types.ts index 8282c0a..7eadcd4 100644 --- a/src/i18n/types.ts +++ b/src/i18n/types.ts @@ -331,6 +331,14 @@ export interface TranslationSchema { currentPref: string; activeNow: string; autoDesc: string; + autoLabel: string; + defaultLabel: string; + darkLabel: string; + lightLabel: string; + tokyoNightLabel: string; + githubDarkLabel: string; + githubLightLabel: string; + highContrastLabel: string; }; planFlow: { approveCardTitle: string; diff --git a/src/i18n/zh-CN.ts b/src/i18n/zh-CN.ts index 7afc2e3..8b93ac3 100644 --- a/src/i18n/zh-CN.ts +++ b/src/i18n/zh-CN.ts @@ -469,11 +469,19 @@ export const zhCN: TranslationSchema = { themeSampleReasoning: "推理中", }, themePicker: { - header: "主题", + header: "选择主题", footer: "↑↓ 选择 · ⏎ 确认 · Esc 取消", currentPref: "当前偏好", activeNow: "当前生效", - autoDesc: "使用 REASONIX_THEME 或默认主题", + autoDesc: "使用 CARBONCODE_THEME 或默认主题", + autoLabel: "自动", + defaultLabel: "默认", + darkLabel: "深色", + lightLabel: "浅色", + tokyoNightLabel: "东京夜色", + githubDarkLabel: "GitHub 深色", + githubLightLabel: "GitHub 浅色", + highContrastLabel: "高对比度", }, planFlow: { approveCardTitle: "确认计划", diff --git a/tests/ui-theme-picker.test.tsx b/tests/ui-theme-picker.test.tsx index ea2c6fc..f249189 100644 --- a/tests/ui-theme-picker.test.tsx +++ b/tests/ui-theme-picker.test.tsx @@ -1,8 +1,9 @@ import { render } from "ink"; import React from "react"; -import { describe, expect, it } from "vitest"; +import { afterEach, describe, expect, it } from "vitest"; import { ThemePicker } from "../src/cli/ui/ThemePicker.js"; import { listThemeNames } from "../src/cli/ui/theme/tokens.js"; +import { setLanguageRuntime } from "../src/i18n/index.js"; import { makeFakeStdin, makeFakeStdout } from "./helpers/ink-stdio.js"; function renderPicker(props: { @@ -23,6 +24,10 @@ function renderPicker(props: { } describe("ThemePicker", () => { + afterEach(() => { + setLanguageRuntime("EN"); + }); + it("lists auto and all registered themes", () => { const text = renderPicker({ currentPreference: "auto", activeTheme: "github-dark" }); expect(text).toContain("auto"); @@ -43,4 +48,15 @@ describe("ThemePicker", () => { expect(text).toContain("⏎"); expect(text).toContain("esc"); }); + + it("renders Simplified Chinese labels when zh-CN is active", () => { + setLanguageRuntime("zh-CN"); + const text = renderPicker({ currentPreference: "auto", activeTheme: "github-dark" }); + expect(text).toContain("选择主题"); + expect(text).toContain("auto - 自动"); + expect(text).toContain("github-dark - GitHub 深色"); + expect(text).toMatch(/auto[\s\S]*当前偏好/); + expect(text).toMatch(/github-dark[\s\S]*当前生效/); + expect(text).toContain("↑↓ 选择"); + }); }); From f0624db8fa8d7fe3ba94f8cc15cbf0ab83e6bdf5 Mon Sep 17 00:00:00 2001 From: w287346141 <287346141@qq.com> Date: Mon, 25 May 2026 14:45:55 +0800 Subject: [PATCH 2/3] Support Carbon Code theme env --- src/cli/ui/App.tsx | 10 ++++------ src/cli/ui/Wizard.tsx | 5 +++-- src/cli/ui/slash/handlers/observability.ts | 4 ++-- src/cli/ui/slash/handlers/theme.ts | 4 ++-- src/config.ts | 4 ++++ tests/config.test.ts | 9 +++++++++ 6 files changed, 24 insertions(+), 12 deletions(-) diff --git a/src/cli/ui/App.tsx b/src/cli/ui/App.tsx index 4261884..8c4645c 100644 --- a/src/cli/ui/App.tsx +++ b/src/cli/ui/App.tsx @@ -37,6 +37,7 @@ import { loadBaseUrl, loadReasoningEffort, loadTheme, + loadThemeEnv, markEditModeHintShown, markMouseClipboardHintShown, mouseClipboardHintShown, @@ -393,7 +394,7 @@ export function App(props: AppProps): React.ReactElement { [props.session], ); const [themeName, setThemeName] = React.useState(() => - resolveThemePreference(loadTheme(), process.env.REASONIX_THEME), + resolveThemePreference(loadTheme(), loadThemeEnv()), ); const statusBar = React.useMemo((): StatusBarConfig => { const cfg = readConfig().statusBar ?? {}; @@ -2535,7 +2536,7 @@ function AppInner({ const handleQQThemePick = useCallback( (target: ThemeChoice): string => { saveTheme(target); - const active = resolveThemePreference(target, process.env.REASONIX_THEME); + const active = resolveThemePreference(target, loadThemeEnv()); setThemeName(active); return `theme saved: ${target}\nactive now: ${active}`; }, @@ -4280,10 +4281,7 @@ function AppInner({ setPendingThemePicker(false); if (outcome.kind === "quit") return; saveTheme(outcome.value); - const active = resolveThemePreference( - outcome.value, - process.env.REASONIX_THEME, - ); + const active = resolveThemePreference(outcome.value, loadThemeEnv()); setThemeName(active); log.pushInfo(`theme saved: ${outcome.value}\n active now: ${active}`); }} diff --git a/src/cli/ui/Wizard.tsx b/src/cli/ui/Wizard.tsx index 9706f8b..5f314a9 100644 --- a/src/cli/ui/Wizard.tsx +++ b/src/cli/ui/Wizard.tsx @@ -19,6 +19,7 @@ import { isPlausibleKey, loadBaseUrl, loadTheme, + loadThemeEnv, readConfig, redactKey, resolveThemePreference, @@ -94,13 +95,13 @@ export function Wizard({ useEffect(() => onLanguageChange(() => setLanguageVersion((v) => v + 1)), []); const [previewTheme, setPreviewTheme] = useState(() => - resolveThemePreference(initial?.theme ?? loadTheme(), process.env.REASONIX_THEME), + resolveThemePreference(initial?.theme ?? loadTheme(), loadThemeEnv()), ); const [step, setStep] = useState("language"); const [data, setData] = useState(() => ({ language: getLanguage(), - theme: resolveThemePreference(initial?.theme ?? loadTheme(), process.env.REASONIX_THEME), + theme: resolveThemePreference(initial?.theme ?? loadTheme(), loadThemeEnv()), apiKey: existingApiKey ?? "", preset: initial?.preset ?? "auto", selectedCatalog: deriveInitialCatalog(initial?.mcp ?? []), diff --git a/src/cli/ui/slash/handlers/observability.ts b/src/cli/ui/slash/handlers/observability.ts index c28cb0f..d39fe2d 100644 --- a/src/cli/ui/slash/handlers/observability.ts +++ b/src/cli/ui/slash/handlers/observability.ts @@ -1,5 +1,5 @@ import { release } from "node:os"; -import { loadRateLimit, loadTheme, resolveThemePreference } from "@/config.js"; +import { loadRateLimit, loadTheme, loadThemeEnv, resolveThemePreference } from "@/config.js"; import { getLanguage, t } from "@/i18n/index.js"; import { DEEPSEEK_CONTEXT_TOKENS, DEFAULT_CONTEXT_TOKENS, pricingFor } from "@/telemetry/stats.js"; import { countTokensBounded } from "@/tokenizer.js"; @@ -230,7 +230,7 @@ function estimateCost(userText: string, loop: import("@/loop.js").CacheFirstLoop } const feedback: SlashHandler = (_args, loop, ctx) => { - const themeName = resolveThemePreference(loadTheme(), process.env.REASONIX_THEME); + const themeName = resolveThemePreference(loadTheme(), loadThemeEnv()); const diagnostic = buildFeedbackDiagnostic({ version: VERSION, latestVersion: ctx.latestVersion ?? undefined, diff --git a/src/cli/ui/slash/handlers/theme.ts b/src/cli/ui/slash/handlers/theme.ts index ed519af..b7b0107 100644 --- a/src/cli/ui/slash/handlers/theme.ts +++ b/src/cli/ui/slash/handlers/theme.ts @@ -1,4 +1,4 @@ -import { resolveThemePreference, saveTheme } from "@/config.js"; +import { loadThemeEnv, resolveThemePreference, saveTheme } from "@/config.js"; import { type ThemeName, isThemeName, listThemeNames } from "../../theme/tokens.js"; import type { SlashHandler } from "../dispatch.js"; @@ -17,7 +17,7 @@ const theme: SlashHandler = (args) => { } saveTheme(next); - const active = resolveThemePreference(next, process.env.REASONIX_THEME); + const active = resolveThemePreference(next, loadThemeEnv()); return { info: `theme saved: ${next}\nactive on next launch: ${active}` }; }; diff --git a/src/config.ts b/src/config.ts index 373bf1e..22f8472 100644 --- a/src/config.ts +++ b/src/config.ts @@ -814,6 +814,10 @@ export function loadTheme(path: string = defaultConfigPath()): ThemeName | "auto return undefined; } +export function loadThemeEnv(env: NodeJS.ProcessEnv = process.env): string | undefined { + return env.CARBONCODE_THEME ?? env.REASONIX_THEME; +} + export function resolveThemePreference( configTheme: ThemeName | "auto" | undefined, envTheme?: string | null, diff --git a/tests/config.test.ts b/tests/config.test.ts index c8fa60c..f49af30 100644 --- a/tests/config.test.ts +++ b/tests/config.test.ts @@ -23,6 +23,7 @@ import { loadReasoningEffort, loadSemanticEmbeddingUserConfig, loadTheme, + loadThemeEnv, markEditModeHintShown, readConfig, redactKey, @@ -447,6 +448,14 @@ describe("config", () => { expect(resolveThemePreference("auto", "unknown")).toBe("github-light"); }); + it("loadThemeEnv prefers Carbon Code theme env over the legacy Reasonix env", () => { + expect(loadThemeEnv({ CARBONCODE_THEME: "tokyo-night", REASONIX_THEME: "github-dark" })).toBe( + "tokyo-night", + ); + expect(loadThemeEnv({ REASONIX_THEME: "github-dark" })).toBe("github-dark"); + expect(loadThemeEnv({})).toBeUndefined(); + }); + it("saveTheme doesn't clobber other persisted fields", () => { saveEditMode("auto", path); saveTheme("github-light", path); From 32c2d825214a4d89072f4a30eda0766e0da9a2be Mon Sep 17 00:00:00 2001 From: w287346141 <287346141@qq.com> Date: Sun, 24 May 2026 13:19:13 +0800 Subject: [PATCH 3/3] Localize theme command feedback --- src/cli/ui/slash/handlers/theme.ts | 10 ++++++++-- src/i18n/EN.ts | 4 ++++ src/i18n/zh-CN.ts | 4 ++++ tests/slash.test.ts | 17 +++++++++++++++++ 4 files changed, 33 insertions(+), 2 deletions(-) diff --git a/src/cli/ui/slash/handlers/theme.ts b/src/cli/ui/slash/handlers/theme.ts index b7b0107..dc6ac8e 100644 --- a/src/cli/ui/slash/handlers/theme.ts +++ b/src/cli/ui/slash/handlers/theme.ts @@ -1,4 +1,5 @@ import { loadThemeEnv, resolveThemePreference, saveTheme } from "@/config.js"; +import { t } from "@/i18n/index.js"; import { type ThemeName, isThemeName, listThemeNames } from "../../theme/tokens.js"; import type { SlashHandler } from "../dispatch.js"; @@ -13,12 +14,17 @@ const theme: SlashHandler = (args) => { if (!next) return { openThemePicker: true }; if (!isThemeChoice(next)) { - return { info: `unknown theme: ${next}\navailable: ${themeChoices.join(", ")}` }; + return { + info: t("handlers.theme.unknownTheme", { + theme: next, + available: themeChoices.join(", "), + }), + }; } saveTheme(next); const active = resolveThemePreference(next, loadThemeEnv()); - return { info: `theme saved: ${next}\nactive on next launch: ${active}` }; + return { info: t("handlers.theme.saved", { theme: next, active }) }; }; export const handlers: Record = { diff --git a/src/i18n/EN.ts b/src/i18n/EN.ts index 6dbfc3d..fc6f162 100644 --- a/src/i18n/EN.ts +++ b/src/i18n/EN.ts @@ -769,6 +769,10 @@ export const EN: TranslationSchema = { titleStarted: "▸ naming session…", titleFailed: "▸ session title failed: {reason}", }, + theme: { + saved: "theme saved: {theme}\nactive on next launch: {active}", + unknownTheme: "unknown theme: {theme}\navailable: {available}", + }, admin: { doctorNeedsTui: "/doctor needs a TUI context (postDoctor wired).", doctorRunning: "⚕ Doctor — running health checks…", diff --git a/src/i18n/zh-CN.ts b/src/i18n/zh-CN.ts index 8b93ac3..ddab99a 100644 --- a/src/i18n/zh-CN.ts +++ b/src/i18n/zh-CN.ts @@ -739,6 +739,10 @@ export const zhCN: TranslationSchema = { titleStarted: "▸ 正在命名会话…", titleFailed: "▸ 会话命名失败:{reason}", }, + theme: { + saved: "主题已保存:{theme}\n下次启动时生效:{active}", + unknownTheme: "未知主题:{theme}\n可用主题:{available}", + }, admin: { doctorNeedsTui: "/doctor 需要 TUI 上下文(postDoctor 已连接)。", doctorRunning: "⚕ 健康检查 — 正在运行…", diff --git a/tests/slash.test.ts b/tests/slash.test.ts index 135865b..fd99ff5 100644 --- a/tests/slash.test.ts +++ b/tests/slash.test.ts @@ -1244,6 +1244,7 @@ describe("handleSlash", () => { }); afterEach(() => { + setLanguageRuntime("EN"); process.env.HOME = originalHome; process.env.USERPROFILE = originalUserProfile; if (originalTheme === undefined) { @@ -1273,11 +1274,27 @@ describe("handleSlash", () => { expect(loadTheme()).toBe("auto"); }); + it("localizes theme feedback in zh-CN", () => { + setLanguageRuntime("zh-CN"); + const r = handleSlash("theme", ["light"], makeLoop()); + expect(r.info).toContain("主题已保存:light"); + expect(r.info).toContain("下次启动时生效:light"); + expect(loadTheme()).toBe("light"); + }); + it("rejects unknown theme names", () => { const r = handleSlash("theme", ["solarized"], makeLoop()); expect(r.info).toMatch(/unknown theme: solarized/); expect(loadTheme()).toBeUndefined(); }); + + it("localizes unknown theme names in zh-CN", () => { + setLanguageRuntime("zh-CN"); + const r = handleSlash("theme", ["solarized"], makeLoop()); + expect(r.info).toContain("未知主题:solarized"); + expect(r.info).toContain("可用主题:auto"); + expect(loadTheme()).toBeUndefined(); + }); }); describe("/language", () => {