Skip to content

fix(tui): accept the documented dark/light theme aliases - #523

Merged
emal-avala merged 13 commits into
mainfrom
fix/docs-accuracy
Jul 27, 2026
Merged

fix(tui): accept the documented dark/light theme aliases#523
emal-avala merged 13 commits into
mainfrom
fix/docs-accuracy

Conversation

@emal-avala

Copy link
Copy Markdown
Member

Summary

docs/configuration/themes.mdx lists:

| `dark` / `light`    | Aliases for the default dark / light themes.      |

Theme::from_name resolves both:

"light" => DEFAULT_LIGHT,
"dark" => DEFAULT_DARK,

…but all_names() — which /color uses to validate input — omitted them. So /color dark printed "Unknown theme" while naming a theme that resolves perfectly. Both are now in the registry, so /color accepts them and the picker offers them.

The more useful half: making the docs checkable

This repo keeps shipping advertised-but-absent features. Just this week: a theme picker that was unreachable (#505), /vim setting a config key nothing read (#522), keybindings listed as active that never fired (#511), /resume whose description promised a picker that didn't exist (#518). Every one was found by hand.

Two guards so the next one is found by CI:

every_documented_theme_name_is_accepted — checks each identifier the theme docs advertise against what /color will take. This is what caught dark/light.

every_documented_command_exists — parses the command reference's table rows and asserts each is a real command.

Two details that keep the second one honest:

  • It reads table rows only (| /foo | … |), so prose like "Type these at the start of a prompt (not as /commands)" is not mistaken for a claim. That line is why a naive grep reports a false positive.
  • It refuses to run if extraction finds fewer than 20 commands, so a table reformat makes it fail loudly rather than silently pass over nothing.

Mutation-checked: injecting | /teleport | Beam the repo to Mars | into the docs fails the test with documented but not implemented: ["teleport"]; removing it passes.

Two register rows were wrong, and are corrected rather than "fixed"

  • D9-18 ("Esc hard-wired to Cancel regardless of custom keybindings") is already satisfied — feat(tui): dispatch user keybindings #511 put esc in RESERVED_CHORDS with a test that a hostile binding cannot steal it.
  • D11-15 claims docs/tui/README.md "still claims start_paused" as though it were false. It is true: there are 4 start_paused tests in the tree. No change needed.

Verification

646 bin tests pass; clippy --all-targets -- -D warnings and fmt --check clean.

emal-avala and others added 2 commits July 26, 2026 23:05
docs/configuration/themes.mdx lists `dark` and `light` as aliases for the
default dark and light palettes. `Theme::from_name` resolves both, so the
themes work — but `all_names()`, which `/color` uses to validate input,
omitted them. `/color dark` was rejected as unknown while naming a theme
that exists.

Both are now in the registry, so `/color` accepts them and the picker
offers them.

Adds two guards against the pattern this repo keeps hitting — a theme
picker that was unreachable, `/vim` setting a key nothing read,
keybindings listed as active that never fired:

every_documented_theme_name_is_accepted checks each identifier the theme
docs advertise against what /color will take.

every_documented_command_exists parses the command reference's table rows
and asserts each one is a real command. It reads table rows only, so
prose mentioning a slash command in passing is not treated as a claim,
and it refuses to run if extraction finds implausibly few commands —
otherwise a table reformat would silently make it vacuous.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 5243bf122a

ℹ️ About Codex in GitHub

Your team has set up Codex to 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 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread crates/cli/src/ui/theme.rs Outdated
Comment on lines +648 to +649
"dark".to_string(),
"Dark (default dark palette)".to_string(),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Reserve alias IDs before adding them to the picker

When a user has a valid custom theme named dark.toml (or light.toml), this new “default” option does not select the default palette: from_name calls lookup_palette before handling aliases, so the custom palette shadows the alias, and the later user-theme loop also adds a second picker row with the same ID. Thus both rows select the custom theme while one is misleadingly labeled as the default. Either resolve these reserved aliases before user palettes or exclude/deduplicate colliding custom IDs.

Useful? React with 👍 / 👎.

@emal-avala

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: bb9c612982

ℹ️ About Codex in GitHub

Your team has set up Codex to 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 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread crates/cli/src/ui/theme.rs Outdated
Comment on lines +648 to +653
"dark".to_string(),
"Dark (default dark palette)".to_string(),
),
(
"light".to_string(),
"Light (default light palette)".to_string(),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Avoid duplicate aliases for custom dark/light themes

When a user has a valid custom dark.toml or light.toml theme, these alias rows are followed by a user-palette row with the same identifier. Since Theme::from_name resolves an exact user palette before handling aliases, selecting “Dark (default dark palette)” actually previews and applies the custom palette, while both picker entries resolve identically and /color repeats the name. Deduplicate these identifiers or omit the alias row when a user palette shadows it.

Useful? React with 👍 / 👎.

Comment thread crates/cli/src/commands/mod.rs Outdated
Comment on lines +6306 to +6309
let missing: Vec<&String> = claimed
.iter()
.filter(|c| !is_builtin_command(c) && !is_interactive_slash(&format!("/{c}")))
.collect();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Require documented commands to be registered

If an interactive command's COMMANDS entry is removed or misspelled while its hard-coded name remains in is_interactive_slash (for example theme, model, or scroll), this test still considers it implemented. However, execute() dispatches only entries from COMMANDS, and the modern TUI also gates slash execution through is_builtin_command, so the documented command would instead become a prompt while this guard passes. Check registration directly rather than accepting routing metadata as proof of implementation.

Useful? React with 👍 / 👎.

Comment thread crates/cli/src/ui/theme.rs Outdated
Comment on lines +999 to +1001
let accepted = super::Theme::all_names();
let documented = [
"auto",

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Parse documented theme names instead of duplicating them

This test never reads docs/configuration/themes.mdx; it checks a manually copied list instead. If the documentation later adds or renames an advertised identifier without updating this array, the test continues to pass even when /color rejects that identifier, defeating the stated documentation-regression guard. Extract the identifiers from the documentation, as the command test does.

Useful? React with 👍 / 👎.

Three review follow-ups, all cases where a check could pass while the
thing it guards had drifted:

- all_names/all_options listed the dark/light aliases even when a user
  palette owns those ids. from_name resolves the user palette first, so
  the alias row could never win — the picker offered the same id twice
  and one entry was a lie. Alias rows are now dropped when shadowed.

- every_documented_theme_name_is_accepted restated the documented ids
  in an array, so adding or renaming a theme in the docs left the test
  passing. It now parses the theme table out of themes.mdx (scoped to
  that table — the page also documents colour keys in backticked
  tables) and asserts the parse is non-trivial so a format change
  cannot make it vacuous. Verified by renaming a documented theme: the
  test fails, naming it.

- the documented-commands test accepted is_interactive_slash as proof
  of implementation, but that helper carries its own hard-coded list;
  a command whose COMMANDS entry was dropped or misspelled stayed
  'implemented' while execute could no longer dispatch it. Registration
  in COMMANDS is now the requirement, plus a check that every
  interactive-classified name is registered too.
@emal-avala

Copy link
Copy Markdown
Member Author

@codex review — all three addressed: alias rows are dropped when a user palette shadows dark/light (only the entry from_name resolves is listed); the documented-theme test now parses the theme table out of themes.mdx instead of restating it, scoped to that table since the page also documents colour keys, with a non-trivial-parse assertion — verified by renaming a documented theme, which makes it fail and name the theme; and the documented-command test now requires registration in COMMANDS rather than accepting is_interactive_slash, plus asserts every interactive-classified name is registered.

Separately: the Coverage failure on this PR is the known permission_modal_allow_once_runs_tool tarpaulin timing flake (it passes locally and has flaked on unrelated PRs), not a regression from these changes.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 86ff3ce4b3

ℹ️ About Codex in GitHub

Your team has set up Codex to 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 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread crates/cli/src/ui/theme.rs Outdated
Comment on lines +642 to +643
let mut seen = std::collections::HashSet::new();
names.retain(|n| seen.insert(n.clone()));

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Keep shadowed aliases in the same catalog position

When a user has a valid dark.toml or light.toml, this retains the leading alias in all_names, while all_options removes that alias and keeps the custom palette at the end. Consequently theme_picker.rs::every_registry_theme_is_offered fails its exact-vector assertion based solely on the user's real config directory, so the CLI test suite is no longer hermetic for these supported custom-theme IDs; derive both catalogs using the same shadowing and ordering rule.

AGENTS.md reference: AGENTS.md:L63-L67

Useful? React with 👍 / 👎.

Comment thread crates/cli/src/ui/theme.rs Outdated
Comment on lines +675 to +677
let is_alias = (id == "dark" || id == "light")
&& (label.contains("default dark palette")
|| label.contains("default light palette"));

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Drop the auto alias when a user palette shadows it

When the user supplies auto.toml, from_name("auto") resolves that exact user palette before running automatic detection, but this predicate only recognizes dark and light as aliases. The subsequent deduplication therefore keeps the misleading Auto (match terminal) row and discards the user's labeled row, so the picker hides the custom palette's identity while applying it; auto needs the same shadow handling.

Useful? React with 👍 / 👎.

Comment thread crates/cli/src/commands/mod.rs Outdated
Comment on lines +6317 to +6319
// The interactive list names commands too; every one of those must
// be registered as well, or `execute` will never reach it.
for name in ["theme", "model", "scroll", "editor", "open"] {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Check every interactive command registration

This list does not cover every name classified by is_interactive_slash: it omits at least session, powerup, uninstall, plugin, and team-remember. If any of those entries is removed or misspelled in COMMANDS, resolution falls back to the raw name and the existing interactive-classification assertions still pass, while execute can no longer dispatch the command, so the new registration guard misses precisely that regression for the omitted names.

Useful? React with 👍 / 👎.

Follow-ups on my own previous fix, which traded three hardcodings for
two more:

- all_names and all_options each built their own list, so a shadowed
  alias was dropped from one and kept in the other — the catalogs
  disagreed about which ids exist and where they sit. all_options is
  now the only builder and all_names derives from it.

- alias shadowing covered dark/light but not auto, though from_name
  resolves a user auto.toml ahead of automatic detection just the same.
  All three aliases are now dropped when a user palette owns the id.

- the registration test restated a five-name subset of the interactive
  commands, omitting session, powerup, uninstall and any name added
  later. The classifier's list is now a public const that both
  is_interactive_slash and the test read, so the test cannot fall
  behind it. Verified by adding an unregistered name to the const: the
  test fails and names it.
@emal-avala

Copy link
Copy Markdown
Member Author

@codex review — all three addressed at the root rather than patched:

  • all_options is now the single catalog and all_names derives from it, so the two can no longer disagree about which ids exist or their order (shadowed aliases are dropped in one place).
  • auto joins dark/light in alias shadowing — from_name resolves a user auto.toml ahead of automatic detection, so the alias row was equally unreachable there.
  • the interactive-command list is now a public const (INTERACTIVE_SLASH_NAMES) that both is_interactive_slash and the test read, replacing my restated five-name subset that omitted session, powerup and uninstall. Verified by adding an unregistered name to the const: the test fails and names it.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a60e53c0c9

ℹ️ About Codex in GitHub

Your team has set up Codex to 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 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread crates/cli/src/ui/theme.rs Outdated
("light", "Light (default light palette)"),
]
.into_iter()
.filter(|(id, _)| !shadowed.contains(id))

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Route shadowed auto to the user palette

When <config>/themes/auto.toml exists, this filter removes the built-in alias row and leaves the user-labeled row as the sole auto option, but the runtime facade still intercepts that ID: theme_runtime.rs::Theme::from_name and resolve_theme convert auto to one-dark or solarized-light before the legacy palette lookup, and /color and the theme pickers use those paths. Consequently, selecting the advertised custom row silently applies the detected built-in theme instead of the user's palette; only shadow the alias after runtime resolution can preserve a custom auto ID.

Useful? React with 👍 / 👎.

The catalog now lists a user <config>/themes/auto.toml as the sole
'auto' row, but the runtime facade intercepted the id before any
palette lookup: from_name and resolve_theme both converted 'auto' to
one-dark or solarized-light, so selecting the advertised row silently
applied a built-in the user never chose.

Both sites now defer when a user palette owns the id, matching how
dark/light already shadow. The rule is a pure auto_detection_applies
so it is testable without mutating the process environment — writing
a themes dir would have needed XDG_CONFIG_HOME, and process-global
env in tests is exactly the pattern behind the flake fixed in #528.
@emal-avala

Copy link
Copy Markdown
Member Author

@codex review — fixed: both runtime intercepts (Theme::from_name and resolve_theme) now defer to a user palette that owns auto, so the row the catalog advertises is the one actually applied. The rule is extracted as a pure auto_detection_applies(name, user_owns_auto) and tested directly — deliberately not via a temp themes dir, since that needs XDG_CONFIG_HOME and process-global env in tests is the pattern behind the flake #528 fixes.

Note on this PR's red check: Test (windows-latest) failed on plan_mode::tests::plan_pointer_roundtrip_and_pid_fallback, which is the known parallel-execution flake — unrelated to this branch and fixed in #528.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 0427473d09

ℹ️ About Codex in GitHub

Your team has set up Codex to 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 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread crates/cli/src/ui/theme_runtime.rs Outdated
// A user palette named `auto` owns the id: the catalog lists it
// as the only `auto` row, so intercepting it here would apply a
// detected built-in instead of the palette the user picked.
if auto_detection_applies(name, legacy::user_palette_exists("auto")) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Skip palette scans for non-auto theme names

When name is any value other than auto, Rust still evaluates legacy::user_palette_exists("auto") before auto_detection_applies can reject it. That helper synchronously reads and parses every user theme file, and the onboarding picker calls Theme::from_name on every redraw, so navigating standard themes now repeatedly performs blocking filesystem I/O and re-emits warnings for malformed palettes. Short-circuit on name == "auto" before querying the user catalog; the same eager evaluation also occurs in resolve_theme.

AGENTS.md reference: AGENTS.md:L124-L126

Useful? React with 👍 / 👎.

auto_detection_applies took the ownership flag by value, so every
theme resolution evaluated it — and that helper reads and parses
every file in the user themes directory. The onboarding picker
resolves a theme on each redraw, so arrowing through the standard
palettes scanned the disk once per keystroke.

The check is a closure now, run only for the literal 'auto'. A test
asserts it is not invoked for any other name.
@emal-avala

Copy link
Copy Markdown
Member Author

@codex review — fixed: the ownership check is now a closure, so legacy::user_palette_exists runs only for the literal auto rather than on every theme resolution. That matters exactly where you said — the onboarding picker resolves a theme per redraw, so this was a themes-directory scan per keystroke while arrowing through standard palettes. A test asserts the closure is never invoked for a non-auto name, and the rule stays pure/testable.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: eada28be5c

ℹ️ About Codex in GitHub

Your team has set up Codex to 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 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

pub fn resolve_theme(configured: &str) -> String {
if configured == "auto" {
// Same rule as `Theme::from_name`: a user `auto.toml` owns the id.
if auto_detection_applies(configured, || legacy::user_palette_exists("auto")) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Treat a shadowing auto palette as explicit

When a valid themes/auto.toml exists and [ui].inherit_fg = true, this condition returns false and resolve_theme preserves the literal auto; callers then pass configured_name == "auto" to init_with_options, which marks the theme as automatic and causes current() to replace the custom palette's text color with the terminal foreground. Thus the newly supported user palette does not fully shadow the alias as intended, unlike custom dark/light palettes; propagate whether detection actually applied rather than inferring Auto mode solely from the configured string.

Useful? React with 👍 / 👎.

With themes/auto.toml present, resolve_theme now preserves the
literal 'auto', so callers still passed configured_name == "auto"
to init_with_options and ActiveOptions::auto came out true. Under
[ui].inherit_fg that rewrites the text slot — of a palette the user
wrote and picked deliberately.

The flag now uses the same auto_detection_applies predicate as the
resolution path, so the two cannot disagree about what 'auto' meant.
@emal-avala

Copy link
Copy Markdown
Member Author

@codex review — fixed: ActiveOptions::auto now uses the same auto_detection_applies predicate as the resolution path, so a user themes/auto.toml is treated as the explicit choice it is and inherit_fg no longer rewrites its text slot. Keeping one predicate for both means the resolution path and the options flag cannot disagree about what auto meant, which is how this gap appeared in the first place.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 4d7c8c4094

ℹ️ About Codex in GitHub

Your team has set up Codex to 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 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread crates/cli/src/ui/theme.rs Outdated
("light", "Light (default light palette)"),
]
.into_iter()
.filter(|(id, _)| !shadowed.contains(id))

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve auto as the first option when shadowed

When a valid user themes/auto.toml exists, this filter removes the leading auto row and the user row is appended after every standard palette, so Theme::all_names().first() becomes dark. That makes the existing all_names_starts_with_auto_and_lists_standard_ids unit test depend on the developer's real config directory and fail for the newly supported custom-auto scenario; replace the leading row with the user's label or isolate the test/config lookup so the default test path remains hermetic.

AGENTS.md reference: AGENTS.md:L63-L67

Useful? React with 👍 / 👎.

Dropping the alias row and appending the user palette moved the id to
the end of the catalog, so all_names().first() became 'dark' whenever
a themes/auto.toml existed — and the existing ordering test then
passed or failed depending on the developer's own config directory.

The alias row now carries the user's label in place, so ids and their
order are identical whether or not a palette shadows one. The rule is
a pure catalog(user) so it is testable without a themes directory; a
test covers the shadowed and unshadowed shapes.
@emal-avala

Copy link
Copy Markdown
Member Author

@codex review — fixed by keeping the alias row in place with the user's label rather than dropping it and appending the palette. That preserves all_names().first() == "auto" regardless of whether a themes/auto.toml exists, which also removes the config-directory dependency you flagged in all_names_starts_with_auto_and_lists_standard_ids — no test isolation needed because the ordering no longer varies. The rule is extracted as a pure catalog(user) so shadowed and unshadowed shapes are both tested without a themes directory.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 3dfc58a87f

ℹ️ About Codex in GitHub

Your team has set up Codex to 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 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread crates/cli/src/ui/theme.rs Outdated
/// accepted, or the docs are promising a theme `/color` will reject.
#[test]
fn every_documented_theme_name_is_accepted() {
let accepted = super::Theme::all_names();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Isolate the docs test from the user's theme directory

When XDG_CONFIG_HOME or the platform config directory contains user themes, this call traverses and parses those external *.toml files through all_names(); stable ordering does not make the test hermetic, and a special or blocking file such as a .toml FIFO can even hang the test. Exercise the new pure catalog with explicit inputs, or inject an isolated themes directory, instead of reading the developer's real config.

AGENTS.md reference: AGENTS.md:L63-L65

Useful? React with 👍 / 👎.

Five tests asserted on built-in ids while calling all_names(), which
traverses and parses the user's themes directory. That makes them
depend on the developer's machine — a stray .toml changes the answer,
a malformed one fails an unrelated assertion, and a special file such
as a FIFO can block the read outright.

They now use a pure catalog_ids(Vec::new()): the shipped palettes are
what each of them is actually about. all_names() keeps its disk read
for the real callers.
@emal-avala

Copy link
Copy Markdown
Member Author

@codex review — fixed, and widened to the class: the docs test now compares against a pure catalog_ids(Vec::new()) instead of all_names(), so it never traverses the user's themes directory (no dependence on the developer's machine, and no way to block on a special file). Four sibling tests had the identical hazard — all_names_starts_with_auto_and_lists_standard_ids, every_named_theme_resolves_with_populated_slots, shipped_default_theme_ids_resolve, accessibility_themes_are_registered — and all now assert against the built-in catalog, which is what each was actually about. all_names() keeps its disk read for real callers.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 78f7af11ba

ℹ️ About Codex in GitHub

Your team has set up Codex to 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 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

pub fn resolve_theme(configured: &str) -> String {
if configured == "auto" {
// Same rule as `Theme::from_name`: a user `auto.toml` owns the id.
if auto_detection_applies(configured, || legacy::user_palette_exists("auto")) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Avoid parsing every theme file during auto resolution

When the default [ui].theme = "auto" is used and the themes directory contains any .toml FIFO or symlink to one, the modern startup path calls resolve_theme, and this new predicate invokes user_palette_exists, which loads every palette and blocks in read_to_string before terminal detection. Previously auto went directly to detect_system_theme; check only a regular auto.toml candidate rather than parsing unrelated directory entries.

Useful? React with 👍 / 👎.

user_palette_exists loaded and parsed every file in the user's themes
directory, and the default theme = "auto" takes that path during
startup: an unrelated malformed file was parsed for nothing, and a
.toml FIFO blocked before terminal detection.

A palette id is its file stem, so open only that candidate, and stat it
first so a non-regular file is rejected without a blocking read.
@emal-avala

Copy link
Copy Markdown
Member Author

@codex review

Fixed the P2 on theme_runtime.rs:230 (auto resolution parsing every theme file) at 150b37c.

user_palette_exists now opens only the one candidate instead of loading the directory. A palette id is its file stem, so auto can only ever come from auto.toml — there was never a reason to parse the other entries. It stats first and rejects anything that is not a regular file, so a .toml FIFO (or a symlink to one) is refused without the blocking read_to_string. Since the id reaches the filesystem, it also has to be a single non-dot path component, so an id can no longer name a file outside the themes dir.

Behaviour I deliberately kept: a malformed auto.toml still counts as absent, so auto-detection applies — same as before, since load_palettes_dir skipped malformed files too.

Three tests added, all pure over a temp dir: only-own-candidate (a sibling malformed file and a nested.toml directory must not affect the answer), id traversal rejection, and the FIFO case. The FIFO test runs the lookup on a worker thread with a 5s timeout so a regression fails the suite rather than hanging it — verified by deleting the stat guard, which reproduces the original block and fails with palette lookup blocked on a FIFO.

Gate: fmt, clippy -D warnings --all-targets, check --all-targets, 651 bin tests.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 150b37c8e2

ℹ️ About Codex in GitHub

Your team has set up Codex to 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 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

// A user palette named `auto` owns the id: the catalog lists it
// as the only `auto` row, so intercepting it here would apply a
// detected built-in instead of the palette the user picked.
if auto_detection_applies(name, || legacy::user_palette_exists("auto")) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Avoid rescanning the palette directory for custom auto

When auto.toml is valid, this predicate returns false and falls through to legacy::Theme::from_name("auto"); its lookup_palette calls user_palettes(), which reads every .toml entry in the directory. Consequently, startup with a custom auto.toml still parses all sibling themes and can block indefinitely on an unrelated .toml FIFO, despite the new candidate-specific stat guard. Load and return the validated candidate directly, or otherwise ensure this fallthrough does not rescan the directory.

Useful? React with 👍 / 👎.

The previous commit stopped the scan in the existence check, but a
valid auto.toml made that predicate fall through to from_name("auto"),
whose lookup_palette called user_palettes() and read the directory
anyway — so startup with a custom auto theme still paid for it.

Route both by-id paths through one targeted loader. load_palettes_dir
stays for the pickers, which do want every palette.
@emal-avala

Copy link
Copy Markdown
Member Author

@codex review

You were right and my previous fix was incomplete — pushed fff4927.

I stopped the directory scan in user_palette_exists, but that only covered the absent case. With a valid auto.toml the predicate returns false and falls through to from_name("auto")lookup_paletteuser_palettes(), which reads the whole directory — so the case that actually has a custom auto theme still paid the full cost. The scan moved rather than went away.

Both by-id paths now go through one targeted loader (user_palette / user_palette_in), which resolves the single <id>.toml candidate. load_palettes_dir is untouched and still backs all_options(), since the pickers genuinely want every palette — that is the one caller for which reading the directory is the point.

New test resolving_one_palette_ignores_its_siblings pins the property rather than either call site: a valid auto.toml resolves with a FIFO sibling in the same directory. Any reintroduced scan on either path blocks on that sibling and fails the test. It runs off-thread with a 5s timeout so a regression fails rather than hanging CI.

Gate: fmt, clippy -D warnings --all-targets, 652 bin tests.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: fff4927b47

ℹ️ About Codex in GitHub

Your team has set up Codex to 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 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread crates/cli/src/ui/theme.rs Outdated
fn user_palette_in(dir: &Path, id: &str) -> Option<Palette> {
// `id` reaches the filesystem, so it has to name a file *in* `dir`
// and not a path that walks out of it.
if Path::new(id).components().count() != 1 || id.starts_with('.') {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Allow dot-prefixed custom theme IDs

When a valid custom theme is named .foo.toml, load_palettes_dir advertises it in the picker with the ID .foo, but resolving that selection or the equivalent config value now returns None here and silently falls back to the default theme. A leading dot is a safe single-component file stem, so reject traversal components without rejecting every dot-prefixed ID.

Useful? React with 👍 / 👎.

The traversal guard rejected every id starting with a dot, but ids come
from the file stem, so .foo.toml is listed in the picker as .foo — and
choosing it fell back to the default theme instead.

Reject the traversal components themselves: only a single Normal
component that round-trips to the id is accepted, which still refuses
. and .. and anything with a separator.
@emal-avala

Copy link
Copy Markdown
Member Author

@codex review

Fixed at 30e9f59. Agreed — the guard was wrong in a way that mattered: ids come from the file stem, so .foo.toml is advertised by load_palettes_dir as .foo, and my starts_with('.') check made choosing that picker entry fall back to the default theme silently.

The guard now rejects the traversal components rather than a leading character: the id must be a single Component::Normal that round-trips back to the id. . and .. are each one component so counting was never sufficient — Normal is what excludes them — and the round-trip also refuses auto/ and anything carrying a separator or a Windows prefix.

Test palette_lookup_accepts_a_dot_prefixed_id asserts the two halves agree: load_palettes_dir advertises .foo, and user_palette_in resolves that same id. That is the invariant that was actually broken — every advertised id must resolve — so it now fails if either side drifts. The traversal test gained .. and auto/.

Gate: fmt, clippy -D warnings --all-targets, 653 bin tests.

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Breezy!

Reviewed commit: 30e9f591c6

ℹ️ About Codex in GitHub

Your team has set up Codex to 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 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@emal-avala
emal-avala merged commit 66004a9 into main Jul 27, 2026
15 checks passed
@emal-avala
emal-avala deleted the fix/docs-accuracy branch July 27, 2026 15:23
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