feat: custom color themes#484
Conversation
Document the custom theme file location, the color token reference, selecting a theme via /theme and tui.toml, and fallback behavior. Link it from the customization sidebar and the tui.toml theme field.
Guides the model (or a manual /custom-theme run) to author a theme JSON in ~/.kimi-code/themes/: docs token reference, deliberate color choices, hex validation, and how to apply via /theme or /reload-tui. Note in the write-tui skill to keep the token set in sync across colors.ts, the schema, the docs, and this skill. Enrich the custom-theme changeset to cover all three usage paths.
🦋 Changeset detectedLatest commit: deb5595 The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
# Conflicts: # apps/kimi-code/test/tui/components/panels/help-panel.test.ts # packages/agent-core/src/skill/builtin/index.ts
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: ea333376c0
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
|
|
||
| // Theme | ||
| applyTheme(theme: Theme, resolved?: ResolvedTheme): void; | ||
| applyTheme(theme: ThemeName, resolved?: ResolvedTheme): void; |
There was a problem hiding this comment.
Await theme application before refreshing auto tracking
When the user switches from a non-auto theme to auto, KimiTUI.applyTheme now returns a promise and only updates appState.theme after loading the palette, but the host contract still exposes it as void. Callers such as /theme and /reload-tui therefore call refreshTerminalThemeTracking() immediately while appState.theme still contains the old non-auto value, so the auto terminal-theme watcher is not installed and the UI stops following terminal background changes until another reload/restart. Please make this return a Promise<void> and await it before refreshing tracking.
Useful? React with 👍 / 👎.
| currentTheme.setPalette(palette); | ||
| this.updateEditorBorderHighlight(); | ||
| this.state.footer.setColors(palette); | ||
| this.state.todoPanel.setColors(palette); | ||
| this.state.ui.requestRender(true); |
There was a problem hiding this comment.
Invalidate transcript on auto theme updates
When the terminal reports a dark/light change while theme = "auto" is already active, this path updates the global palette but skips the transcript invalidation that applyTheme() performs. Components such as status and markdown messages cache ANSI-colored Text/Markdown children and only rebuild them from invalidate(), so existing transcript entries keep the old colors after an automatic terminal-background change while new renders use the new palette. Add the same transcript invalidation here before requesting the full render.
Useful? React with 👍 / 👎.
| return entries | ||
| .filter((e) => e.isFile() && e.name.endsWith('.json')) | ||
| .map((e) => e.name.replace(/\.json$/, '')); |
There was a problem hiding this comment.
Validate custom themes before listing them
The picker now lists every *.json filename in the themes directory without loading or validating it, and selecting one bypasses the /theme <name> validation path. If a user has a malformed theme file or a JSON file missing the required shape, the picker shows Custom: <name>, saves that name, reports success, and getColorPalette() silently falls back to dark, leaving the user with a persisted theme that does not render. Filter the list through the same custom-theme loader or validate before applying the selected value.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 41ed40b53c
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| } | ||
| if (errors.length > 0) { | ||
| // eslint-disable-next-line no-console | ||
| console.warn(`Theme "${name}" has invalid colors:\n${errors.join('\n')}`); |
There was a problem hiding this comment.
Avoid writing theme validation warnings to the raw terminal
When a custom theme has at least one invalid hex value and the user applies it from inside the TUI (for example via /theme <name> or /reload-tui), this console.warn writes directly to the process streams while pi-tui owns the terminal, so the warning text can be injected into/corrupt the rendered screen instead of appearing as a managed status/error message. Please route this validation feedback through the TUI/logging layer or return it to the caller rather than printing from the loader.
Useful? React with 👍 / 👎.
# Conflicts: # apps/kimi-code/src/tui/commands/goal.ts # apps/kimi-code/src/tui/components/dialogs/goal-queue-manager.ts # apps/kimi-code/src/tui/components/dialogs/goal-start-permission-prompt.ts # apps/kimi-code/src/tui/components/editor/custom-editor.ts # apps/kimi-code/src/tui/components/messages/goal-panel.ts
commit: |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 4a781c8023
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| 'Invalid TUI config in ~/.kimi-code/tui.toml; using defaults.'; | ||
|
|
||
| export const TuiThemeSchema = z.enum(['dark', 'light', 'auto']); | ||
| export const TuiThemeSchema = z.string(); |
There was a problem hiding this comment.
Escape custom theme names before saving
This now accepts any string as a theme name, but renderTuiConfig still writes it as theme = "${config.theme}" without using the existing TOML escaping helper. If a custom theme file is named with a quote, backslash escape, or newline (the picker uses raw *.json filenames and docs say the filename is the theme name), selecting it saves malformed or changed TOML, so the next startup falls back to defaults instead of the chosen theme. Please either validate theme names to a safe filename subset or escape config.theme the same way editorCommand is escaped.
Useful? React with 👍 / 👎.
| const accent = (text: string) => currentTheme.boldFg('primary', text); | ||
| const value = (text: string) => currentTheme.fg('text', text); | ||
| const muted = (text: string) => currentTheme.fg('textDim', text); | ||
| const errorStyle = (text: string) => currentTheme.fg('error', text); |
There was a problem hiding this comment.
Rebuild precolored report lines on theme changes
These report builders now eagerly embed currentTheme ANSI colors into the lines array, but UsagePanelComponent only stores those strings and its invalidate() remains a no-op. After /usage, /status, /mcp, or /plugins output is in the transcript, switching themes invalidates the transcript but the panel body keeps the old SGR colors while the border/new output uses the new palette. Store the raw report data or recompute the colored lines during invalidation/render.
Useful? React with 👍 / 👎.
Remove unused chalk/currentTheme/ResolvedTheme imports left by the theme refactor; break the theme <-> pi-tui-theme import cycle by dropping the markdown/editor theme getters from the Theme class (consumers call createMarkdownTheme directly); fix unused vars/params, a floating promise, and a redundant union type.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: deb559584f
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| const customThemes = listCustomThemesSync(); | ||
| const options: ChoiceOption[] = [ | ||
| ...THEME_OPTIONS, | ||
| ...customThemes.map((name) => ({ value: name, label: `Custom: ${name}` })), |
There was a problem hiding this comment.
Filter reserved names from custom theme picker
If a user adds dark.json, light.json, or auto.json under the themes directory, the picker now shows it as Custom: <name> but passes the raw value through unchanged. applyThemeChoice then treats that value as a built-in theme via isBuiltInTheme, so selecting Custom: dark can only apply the built-in dark theme and the custom file is unreachable despite the docs saying the filename is the theme name. Please either hide/reject reserved filenames or disambiguate custom option values before applying them.
Useful? React with 👍 / 👎.
| override invalidate(): void { | ||
| // Rebuild instruction line with fresh theme colours. |
There was a problem hiding this comment.
Repaint compaction headers during invalidation
When a completed, cancelled, or in-progress compaction card is already in the transcript and the user switches themes, applyTheme() invalidates the transcript but this override only rebuilds the optional instruction line. The headerText still contains the ANSI codes produced by the old palette, so the compaction bullet/title remains in the previous theme while the rest of the UI updates; refresh it with this.headerText.setText(this.buildHeader()) during invalidation.
Useful? React with 👍 / 👎.
| dim(text: string): string { | ||
| return chalk.dim(text); |
There was a problem hiding this comment.
Route dim helper through the palette
With a custom theme that changes textDim, every new call site using currentTheme.dim(...) still renders in the terminal's default foreground with SGR dim instead of the theme's textDim color. This affects many of the newly migrated dim UI elements such as tool metadata, truncation hints, and grouped agent/read details, so custom themes cannot actually control the dim text token documented for those surfaces; make this helper color via the active palette, e.g. the old styles.dim behavior.
Useful? React with 👍 / 👎.
Summary
Adds user-customizable color themes to the TUI, with the supporting theme-system work.
currentThemesingleton, so switching themes recolors everything live, including already-rendered transcript.ColorPalette: drop the unusedroleTooltoken, fold exact-alias tokens into their canonical sibling (roleAssistant→text,roleThinking/status→textDim), inline hex directly intodarkColors/lightColors, and document what each token controls. No visual change to the built-in dark/light themes — the merges are exact aliases.~/.kimi-code/themes/<name>.json. Unspecified tokens fall back to the dark palette; invalid hex and missing files degrade gracefully. The/themepicker re-scans the directory on open (no restart), andtui.toml'sthemeaccepts a custom theme name.tui.tomlthemefield./custom-themeskill — guides the model (or a manual/custom-themerun) to author a theme JSON: it asks light/dark + style + any specific colors first, then writes the file, validates the hex, and explains how to apply it. Users can also just ask Kimi to set one up.Release
@moonshot-ai/kimi-code: minor (changeset included).Test plan
/themelists newly added files without a restart.