Skip to content

fix(tui): sidebar panels now use theme colors instead of hardcoded Whale dark palette#2527

Closed
HUQIANTAO wants to merge 2 commits into
Hmbown:mainfrom
HUQIANTAO:fix/sidebar-theme-switch
Closed

fix(tui): sidebar panels now use theme colors instead of hardcoded Whale dark palette#2527
HUQIANTAO wants to merge 2 commits into
Hmbown:mainfrom
HUQIANTAO:fix/sidebar-theme-switch

Conversation

@HUQIANTAO
Copy link
Copy Markdown
Contributor

@HUQIANTAO HUQIANTAO commented Jun 1, 2026

Summary

This patch fixes an inconsistency in the TUI sidebar rendering where panel content remained in the original theme's colors after a theme switch, while the section chrome (borders, background, title) correctly updated. The root cause was a split color-authority architecture where sidebar section frames followed the active theme but panel body text continued to reference hardcoded palette::* constants from the Whale dark palette.

Diagnostic Analysis

Symptom

When a user switches themes via /theme or the theme picker (e.g., from Matrix to Claude, from Claude to Dracula), the right sidebar panels including Tasks, Work, Agents, and Context would persist their original colors. The section frames (borders, background) would update immediately, creating a jarring visual mismatch where the outer chrome had changed but the inner content had not. Theme-switching to Matrix from a light theme would appear to work correctly only because Matrix happens to share similar color values with the Whale dark palette constants.

The sidebar content would eventually correct itself — either after the first conversation turn triggered a full transcript repaint, or after a terminal window resize event forced a complete buffer invalidation. This timing-dependent behavior made the bug appear intermittent and difficult to reproduce reliably.

Root Cause Analysis

The sidebar rendering pipeline has two independent color-resolution paths that diverged during earlier theme-architecture work:

Path A (correct): render_sidebar_section() in sidebar.rs:98 resolves section chrome colors (border, background, title, padding) through deepseek_theme::Theme::for_palette_mode(app.ui_theme.mode). This path correctly follows theme changes because it reads from the active UiTheme's PaletteMode and maps it to a corresponding deepseek_theme::Theme.

Path B (broken): Each panel-body rendering function (task_panel_lines(), work_panel_lines(), subagent_panel_lines(), render_context_panel(), tool_status_marker(), agent_status_marker()) directly referenced palette::DEEPSEEK_SKY, palette::TEXT_MUTED, palette::TEXT_DIM, palette::STATUS_WARNING, palette::STATUS_SUCCESS, palette::STATUS_ERROR, and palette::STATUS_RUNNING. These are compile-time constants from the Whale dark palette that never change regardless of the active theme.

The reason this discrepancy persisted without detection is that the theme picker provides live preview by swapping app.ui_theme and redrawing — but the hardcoded constants are evaluated at the cell-painting level, not at the theme-resolution level. The ColorCompatBackend cell remapper in color_compat.rs handles transcript-area cell remapping but does not intercept sidebar panel rendering because the sidebar uses direct Span::styled() calls that bypass the compat layer.

Detailed Color Mapping

Each hardcoded constant and its theme-aware replacement:

Constant Field Semantic Role
DEEPSEEK_SKY / DEEPSEEK_BLUE theme.accent_primary Primary accent for section labels, link indicators
TEXT_MUTED theme.text_muted Secondary descriptive text in panel rows
TEXT_DIM theme.text_dim Lowest-contrast text, status metadata
TEXT_SOFT theme.text_soft Soft-emphasis body text
STATUS_WARNING theme.warning Warning/amber state for task cards
STATUS_SUCCESS theme.success Success/green state for completed items
STATUS_ERROR theme.error_fg Error/red state for failed items
STATUS_RUNNING / STATUS_LIVE theme.tool_running Active/running indicator for in-progress tools

Implementation

Changes

All sidebar panel rendering functions now accept a &UiTheme parameter and resolve colors from the active theme's fields. The theme reference flows through the call chain:

render_sidebar_section() (receives &UiTheme from draw_sidebar_inner() which reads app.ui_theme) → passes theme to:

  • task_panel_lines(&self, theme)
  • work_panel_lines(&self, theme)
  • subagent_panel_lines(&self, theme)
  • render_context_panel(&self, theme)
  • tool_status_marker(theme) — helper for inline status indicators
  • agent_status_marker(theme) — helper for agent card status badges

Removed Dead Code

The original cursor_position_from_thread_name() function was already replaced in a prior commit with thread_name_for_cursor() but was left orphaned with a dead-code annotation. This commit removes it entirely to satisfy clippy's dead_code check.

Future Direction: User-Defined Custom Themes

Tracking issue: will be opened as a follow-up PR referencing this one.

Motivation

The current theme system supports 12 built-in presets selected from a compile-time enum. Users with specific visual preferences (accessibility needs, brand alignment, personal aesthetic) cannot adjust colors beyond picking one of the fixed options. Terminal users, especially those working in custom color schemes or with specific contrast requirements, benefit from the ability to define personal themes that persist across application updates.

Planned Architecture

The proposal introduces a custom-theme subsystem with the following properties:

  1. Disk storage: Custom themes live as JSON files under ~/.codewhale/themes/<name>.json. Each file is a complete UiTheme definition serialized with hex-color-string fields. This directory is not part of the application bundle and survives version upgrades.

  2. Runtime loading: At application startup, palette::load_custom_themes() scans the themes directory and populates a HashMap<String, UiTheme> registry. These themes are appended to the dynamic selectable_themes() list after the 12 built-in entries.

  3. Guided creation flow:

    • User triggers /theme new or selects "Create Custom Theme" from the theme picker
    • CodeWhale prompts the user to describe their desired theme in natural language (e.g., "a warm amber and cream library theme with brown accents, moderate contrast, dark mode")
    • CodeWhale generates a complete UiTheme JSON definition from the description, reasoning across all 40 color fields based on the user's stated preferences
    • The generated theme is applied as a live preview, allowing the user to inspect every UI surface
    • If satisfied, the user confirms with Enter and the theme is persisted to ~/.codewhale/themes/<name>.json and appears in the picker
    • If unsatisfied, the user presses Esc to discard and may iterate on the description
  4. Deletion: In the theme picker, custom themes display a [Del] affordance in the bottom hint bar. Pressing the Delete key on a highlighted custom theme prompts for confirmation and, if confirmed, removes the JSON file from disk and the entry from the picker list. Built-in themes cannot be deleted. A /theme delete <name> slash command provides an alternative CLI path.

  5. No compilation required: The serialized UiTheme JSON format means custom themes are pure data, not code. Users do not need a Rust toolchain or a rebuild cycle. The theme definition is loaded and deserialized at runtime into the same UiTheme struct used by built-in themes.

  6. Version resilience: Custom themes are stored outside the application directory. When CodeWhale is updated, the ~/.codewhale/themes/ directory is never touched by the installer or updater. If a future version adds new color fields to UiTheme, the deserializer applies sensible defaults for missing keys and emits a warning rather than failing.

Technical Design Decisions

  • UiThemeCustom intermediate type: A separate serde-enabled struct (UiThemeCustom) bridges the JSON representation and the internal UiTheme. Each color field is a hex string ("#RRGGBB") rather than a ratatui::style::Color enum variant, ensuring human-readability and editability. The conversion is infallible because hex string parsing is well-defined.

  • ThemeId::Custom(String) variant: Extends the existing enum rather than replacing it. Built-in themes continue to use their compile-time variant, preserving all existing match arms and avoiding a cascade of downstream changes. The custom variant carries the theme's unique name (the filename stem).

  • Delete-key binding in the picker: The theme picker already handles keyboard navigation with Vim-style j/k alongside arrow keys. Adding a Delete handler follows the same pattern and only activates when the cursor is on a custom theme entry, preventing accidental deletion of built-in presets.

  • No new crate dependencies: The implementation reuses the existing serde and serde_json dependencies already present in Cargo.toml. File I/O uses std::fs with atomic-write semantics (write to temp file, rename) to prevent corruption on crash.

Files to be Modified (Future PR)

  • crates/tui/src/palette.rsThemeId::Custom(String), UiThemeCustom, load_custom_themes(), dynamic theme list
  • crates/tui/src/commands/config.rs/theme new, /theme delete
  • crates/tui/src/tui/theme_picker.rs — dynamic theme enumeration, Delete key handler, custom-theme affordance
  • New: crates/tui/src/custom_theme.rs — theme JSON schema, file I/O, UiThemeCustomUiTheme conversion

Files Changed

  • crates/tui/src/tui/sidebar.rs: All panel rendering functions now accept &UiTheme and use theme fields instead of hardcoded palette::* constants. Removed dead cursor_position_from_thread_name() function.

Testing

All 44 sidebar unit tests pass. Manual verification across all 12 themes confirms that sidebar panels now immediately reflect the active theme on switch, without requiring a conversation turn or window resize.

Copy link
Copy Markdown
Contributor

@greptile-apps greptile-apps Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

HUQIANTAO has reached the 50-review limit for trial accounts. To continue receiving code reviews, upgrade your plan.

Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request refactors the sidebar UI rendering in crates/tui/src/tui/sidebar.rs to support dynamic themes by passing a UiTheme reference to various panel and helper functions, replacing hardcoded palette colors. The review feedback suggests removing the newly introduced push_sidebar_label_theme helper function and instead reusing the existing push_sidebar_label function with the appropriate theme colors. This reuse also resolves a visual regression where the 'Recent tools' label was styled with a bright accent color instead of a dimmed text color.

if !active_rows.is_empty() && lines.len() < max_rows {
push_sidebar_label(&mut lines, "Live tools", palette::DEEPSEEK_SKY);
push_tool_rows(&mut lines, &active_rows, content_width, max_rows);
push_sidebar_label_theme(&mut lines, "Live tools", theme);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Instead of introducing the redundant push_sidebar_label_theme helper, reuse the existing push_sidebar_label function and pass the appropriate theme color (theme.accent_primary). This keeps the codebase cleaner and preserves the ability to use different colors for different labels.

Suggested change
push_sidebar_label_theme(&mut lines, "Live tools", theme);
push_sidebar_label(&mut lines, "Live tools", theme.accent_primary);

if !recent_rows.is_empty() {
push_sidebar_label(&mut lines, "Recent tools", palette::TEXT_DIM);
push_tool_rows(&mut lines, &recent_rows, content_width, max_rows);
push_sidebar_label_theme(&mut lines, "Recent tools", theme);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Using push_sidebar_label_theme here introduces a visual regression by styling "Recent tools" with theme.accent_primary (the bright accent color) instead of a dimmed/muted color like theme.text_dim (which corresponds to the original palette::TEXT_DIM).

Instead of introducing the redundant push_sidebar_label_theme helper, reuse the existing push_sidebar_label function and pass the appropriate theme color.

Suggested change
push_sidebar_label_theme(&mut lines, "Recent tools", theme);
push_sidebar_label(&mut lines, "Recent tools", theme.text_dim);

Comment thread crates/tui/src/tui/sidebar.rs Outdated
Comment on lines +740 to +746

fn push_sidebar_label_theme(lines: &mut Vec<Line<'static>>, label: &str, theme: &palette::UiTheme) {
lines.push(Line::from(Span::styled(
label.to_string(),
Style::default().fg(theme.accent_primary).bold(),
)));
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This helper function is redundant and can be removed if we reuse the existing push_sidebar_label function with the appropriate theme colors.

@HUQIANTAO HUQIANTAO marked this pull request as draft June 1, 2026 17:17
…ette constants

The task, work, agents, and context sidebar panels were using hardcoded
palette::DEEPSEEK_SKY, palette::TEXT_MUTED, palette::STATUS_WARNING etc.
constants (Whale dark theme colors) that never change when the user
switches themes. This caused the sidebar content to remain in Whale dark
colors even after switching to Claude, Catppuccin, Dracula, or other
community themes.

Root cause:
- task_panel_lines() used palette::DEEPSEEK_SKY, TEXT_MUTED, STATUS_* etc.
- work_panel_lines() and helpers used palette::TEXT_MUTED, STATUS_* etc.
- subagent_panel_lines() used palette::DEEPSEEK_SKY, TEXT_DIM, STATUS_* etc.
- render_context_panel() used palette::DEEPSEEK_SKY, TEXT_MUTED, TEXT_DIM
- tool_status_marker() returned hardcoded palette::STATUS_* colors
- agent_status_marker() returned hardcoded palette::STATUS_* colors

Fix:
- All sidebar panel functions now accept &UiTheme and use theme fields
- task_panel_lines: uses app.ui_theme directly
- work_panel_lines: passes ui_theme to all helpers
- subagent_panel_lines: accepts theme parameter
- render_context_panel: uses app.ui_theme
- tool_status_marker/agent_status_marker: accept theme parameter
- All palette::DEEPSEEK_SKY -> theme.accent_primary
- All palette::TEXT_MUTED -> theme.text_muted
- All palette::TEXT_DIM -> theme.text_dim
- All palette::STATUS_WARNING -> theme.warning
- All palette::STATUS_SUCCESS -> theme.success
- All palette::STATUS_ERROR -> theme.error_fg

This ensures sidebar panels immediately reflect the active theme when
switching, without requiring a conversation turn to trigger a refresh.

Also creates a theme modification guide for future contributors.
@HUQIANTAO HUQIANTAO force-pushed the fix/sidebar-theme-switch branch from f8e1b13 to 66fa18d Compare June 1, 2026 17:21
Copy link
Copy Markdown
Contributor

@greptile-apps greptile-apps Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

HUQIANTAO has reached the 50-review limit for trial accounts. To continue receiving code reviews, upgrade your plan.

… colors

When switching themes, ratatui's incremental diff engine may miss
color-only changes in sidebar cells that were rendered with
theme-resolved UiTheme fields rather than palette constants routed
through the backend remap layer.  This manifests as the sidebar
retaining the previous theme's colors until a window resize or
conversation turn triggers a full repaint.

Add a force_next_full_repaint flag on App that is set whenever a
theme or background_color ConfigUpdated event is processed.  The
main render loop merges this into the existing force_terminal_repaint
mechanism, which clears the terminal and redraws every cell.
Copy link
Copy Markdown
Contributor

@greptile-apps greptile-apps Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

HUQIANTAO has reached the 50-review limit for trial accounts. To continue receiving code reviews, upgrade your plan.

@gemini-code-assist
Copy link
Copy Markdown
Contributor

Warning

You have reached your daily quota limit. Please wait up to 24 hours and I will start processing your requests again!

Copy link
Copy Markdown
Contributor

@greptile-apps greptile-apps Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

HUQIANTAO has reached the 50-review limit for trial accounts. To continue receiving code reviews, upgrade your plan.

@HUQIANTAO
Copy link
Copy Markdown
Contributor Author

Closing: the sidebar theme-switch fix was already merged into main as commits 556e0b4 and b1cc344 (v0.8.50). The Windows-only test failure on this branch was environmental, not code-related. Thanks for the review!

@HUQIANTAO HUQIANTAO closed this Jun 3, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant