Skip to content

fix(tui): prefer codewhale settings path#2516

Closed
cyq1017 wants to merge 2 commits into
Hmbown:mainfrom
cyq1017:codex/2369-settings-path
Closed

fix(tui): prefer codewhale settings path#2516
cyq1017 wants to merge 2 commits into
Hmbown:mainfrom
cyq1017:codex/2369-settings-path

Conversation

@cyq1017
Copy link
Copy Markdown
Contributor

@cyq1017 cyq1017 commented Jun 1, 2026

Refs #2369

Problem

settings.toml still uses the legacy DeepSeek config locations even after the broader config home moved toward .codewhale, which leaves user preferences split across old and new homes.

Change

  • prefer ~/.codewhale/settings.toml for new settings writes
  • keep existing ~/.deepseek/settings.toml as the first legacy read fallback
  • keep the old platform config-dir deepseek/settings.toml as the last compatibility fallback
  • update nearby settings docs/comments to describe the current resolver behavior

Verification

  • cargo test -p codewhale-tui settings_path_ --all-features --locked -- --nocapture
  • cargo test -p codewhale-tui settings::tests --all-features --locked -- --nocapture
  • cargo fmt --all -- --check
  • cargo check -p codewhale-tui --all-features --locked
  • cargo clippy -p codewhale-tui --all-features --locked -- -D warnings
  • git diff --check origin/main..HEAD

Greptile Summary

This PR migrates Settings::path() from the old ~/.config/deepseek/settings.toml location to prefer ~/.codewhale/settings.toml for new installs, while preserving backward-compatibility for users with existing ~/.deepseek/settings.toml or platform config-dir files. The change introduces a dedicated resolve_settings_path_from_candidates helper, four new unit tests with an EnvVarRestore RAII helper, and matching doc/comment updates across the TUI and docs.

  • Three-tier path resolver (codewhale_home → legacy_deepseek_home → platform config_dir): existing files are read from their current location; new writes land in ~/.codewhale/settings.toml.
  • TuiPrefs::path() (dead code, wired in feat(config): separate tui.toml for theme and keybinds (closes #437) #657) still falls back to ~/.deepseek/tui.toml for new installs, which will split the config home across ~/.codewhale/ and ~/.deepseek/ once that wiring lands.

Confidence Score: 5/5

Safe to merge; the three-tier resolver correctly handles new installs, legacy deepseek home, and platform config-dir fallback without data loss.

The path resolution logic is correct and well-tested. The only finding is that TuiPrefs::path() — currently dead code — will write tui.toml to ~/.deepseek/ while settings.toml now goes to ~/.codewhale/, creating a split config home when the TuiPrefs wiring (#657) lands. That inconsistency does not affect live users today.

crates/tui/src/settings.rs — specifically the TuiPrefs::path() fallback and its save() doc comment, which will matter once #657 wires up TuiPrefs.

Important Files Changed

Filename Overview
crates/tui/src/settings.rs Core change: replaces single-path config_dir resolution with a three-tier resolver. TuiPrefs::path() (not in this diff) still targets ~/.deepseek/tui.toml for new installs, creating a split config home once TuiPrefs is wired up.
crates/tui/src/tui/app.rs Doc comment updates only: replaces hardcoded ~/.deepseek/settings.toml references with generic phrasing. No logic changes.
crates/tui/src/tui/model_picker.rs Single test comment updated to remove hardcoded config path references. No logic changes.
docs/ACCESSIBILITY.md Documentation updated to describe the new three-tier path resolver accurately.
docs/CONFIGURATION.md Documentation updated to list all three settings.toml resolution tiers.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A["Settings::path() called"] --> B{DEEPSEEK_CONFIG_PATH set?}
    B -- Yes --> C["Return parent(config_path)/settings.toml"]
    B -- No --> D["codewhale_home() → primary\nlegacy_deepseek_home() → legacy_home\ndirs::config_dir() → legacy_config_dir"]
    D --> E["resolve_settings_path_from_candidates(...)"]
    E --> F{primary exists on disk?}
    F -- Yes --> G["Return ~/.codewhale/settings.toml"]
    F -- No --> H{legacy_home exists on disk?}
    H -- Yes --> I["Return ~/.deepseek/settings.toml"]
    H -- No --> J{legacy_config_dir exists on disk?}
    J -- Yes --> K["Return config_dir/deepseek/settings.toml"]
    J -- No --> L{primary is Some?}
    L -- Yes --> M["Return ~/.codewhale/settings.toml (new write)"]
    L -- No --> N{legacy_config_dir is Some?}
    N -- Yes --> O["Return config_dir/deepseek/settings.toml (new write)"]
    N -- No --> P["Error: no config directory found"]
Loading

Fix All in Codex Fix All in Claude Code Fix All in Cursor

Reviews (2): Last reviewed commit: "fix(tui): isolate settings path fallback..." | Re-trigger Greptile

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 migrates the user settings path from the legacy deepseek directory to a new .codewhale directory, while preserving compatibility fallbacks for existing configuration paths. It also updates the documentation and adds tests to verify the path resolution logic. The review feedback highlights two important issues: first, a potential early-return error in Settings::path() if dirs::config_dir() fails in sandboxed environments, which bypasses the primary path resolution; second, a test isolation issue on macOS where dirs::config_dir() ignores the HOME environment variable, potentially overwriting a developer's real settings file during test execution.

Comment thread crates/tui/src/settings.rs Outdated
Comment on lines +371 to +379
let legacy_config_dir = dirs::config_dir()
.context("Failed to resolve config directory: not found.")?
.join("deepseek");
Ok(config_dir.join("settings.toml"))
.join("deepseek")
.join(SETTINGS_FILE_NAME);
if legacy_config_dir.exists() {
return Ok(legacy_config_dir);
}

Ok(primary.unwrap_or(legacy_config_dir))
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.

high

If dirs::config_dir() returns None (which can happen in minimal, sandboxed, or containerized environments), the ? operator will cause Settings::path() to return an error immediately. This prevents the function from falling back to primary (e.g., ~/.codewhale/settings.toml), even if primary is successfully resolved (for instance, via CODEWHALE_HOME).

We should handle dirs::config_dir() as an Option and only return an error if both primary and legacy_config_dir fail to resolve.

        let legacy_config_dir = dirs::config_dir()
            .map(|d| d.join("deepseek").join(SETTINGS_FILE_NAME));
        if let Some(ref path) = legacy_config_dir
            && path.exists()
        {
            return Ok(path.clone());
        }

        primary
            .or(legacy_config_dir)
            .context("Failed to resolve config directory: not found.")

Comment on lines +2188 to +2204
fn settings_path_keeps_platform_config_dir_as_last_legacy_fallback() {
let _g = config_path_test_guard();
let tmp = tempfile::tempdir().expect("tempdir");
let _config_override = EnvVarRestore::remove("DEEPSEEK_CONFIG_PATH");
let _codewhale_home = EnvVarRestore::set("CODEWHALE_HOME", tmp.path().join(".codewhale"));
let _home = EnvVarRestore::set("HOME", tmp.path());

let config_dir = dirs::config_dir().expect("config dir");
let legacy_settings = config_dir.join("deepseek").join("settings.toml");
std::fs::create_dir_all(legacy_settings.parent().expect("parent"))
.expect("legacy config dir");
std::fs::write(&legacy_settings, "low_motion = true\n").expect("legacy settings");

let got = Settings::path().expect("settings path");

assert_eq!(got, legacy_settings);
}
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.

high

In macOS environments, dirs::config_dir() uses system APIs that ignore the HOME environment variable. As a result, running this test on macOS will write to and overwrite the developer's actual configuration file at ~/Library/Application Support/deepseek/settings.toml.

To prevent tests from modifying the host system's real configuration files, we should sandbox dirs::config_dir() by setting XDG_CONFIG_HOME (for Linux) and APPDATA (for Windows), and skip the file-writing assertion on macOS where sandboxing is not supported via environment variables.

    fn settings_path_keeps_platform_config_dir_as_last_legacy_fallback() {
        let _g = config_path_test_guard();
        let tmp = tempfile::tempdir().expect("tempdir");
        let _config_override = EnvVarRestore::remove("DEEPSEEK_CONFIG_PATH");
        let _codewhale_home = EnvVarRestore::set("CODEWHALE_HOME", tmp.path().join(".codewhale"));
        let _home = EnvVarRestore::set("HOME", tmp.path());
        let _xdg_config = EnvVarRestore::set("XDG_CONFIG_HOME", tmp.path());
        let _appdata = EnvVarRestore::set("APPDATA", tmp.path());

        #[cfg(target_os = "macos")]
        {
            // On macOS, dirs::config_dir() cannot be sandboxed via env vars,
            // so we skip this test to avoid overwriting the developer's real settings.
            return;
        }

        let config_dir = dirs::config_dir().expect("config dir");
        let legacy_settings = config_dir.join("deepseek").join("settings.toml");
        std::fs::create_dir_all(legacy_settings.parent().expect("parent"))
            .expect("legacy config dir");
        std::fs::write(&legacy_settings, "low_motion = true\n").expect("legacy settings");

        let got = Settings::path().expect("settings path");

        assert_eq!(got, legacy_settings);
    }

Comment thread crates/tui/src/settings.rs Outdated
Comment on lines +2188 to +2204
fn settings_path_keeps_platform_config_dir_as_last_legacy_fallback() {
let _g = config_path_test_guard();
let tmp = tempfile::tempdir().expect("tempdir");
let _config_override = EnvVarRestore::remove("DEEPSEEK_CONFIG_PATH");
let _codewhale_home = EnvVarRestore::set("CODEWHALE_HOME", tmp.path().join(".codewhale"));
let _home = EnvVarRestore::set("HOME", tmp.path());

let config_dir = dirs::config_dir().expect("config dir");
let legacy_settings = config_dir.join("deepseek").join("settings.toml");
std::fs::create_dir_all(legacy_settings.parent().expect("parent"))
.expect("legacy config dir");
std::fs::write(&legacy_settings, "low_motion = true\n").expect("legacy settings");

let got = Settings::path().expect("settings path");

assert_eq!(got, legacy_settings);
}
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.

P1 XDG_CONFIG_HOME not cleared — test may write into the developer's real config dir

dirs::config_dir() on Linux checks XDG_CONFIG_HOME before falling back to $HOME/.config. The test sets HOME to tmp.path() but does not clear XDG_CONFIG_HOME. When that variable is set (common in developer shells and many CI runners), dirs::config_dir() will return $XDG_CONFIG_HOME, which is outside tmp. The test then writes settings.toml to that real path and never cleans it up — either corrupting an existing settings file or leaving a stale file that causes the test to fail on subsequent runs.

Adding EnvVarRestore::remove("XDG_CONFIG_HOME") alongside the existing HOME override is the minimal fix.

Fix in Codex Fix in Claude Code Fix in Cursor

Hmbown added a commit that referenced this pull request Jun 1, 2026
@Hmbown
Copy link
Copy Markdown
Owner

Hmbown commented Jun 1, 2026

Thanks @cyq1017 — I harvested this settings-path slice into the v0.8.50 triage branch (#2504) rather than merging the PR directly.

Landed commits on #2504:

  • 3b5727f28 from this PR: prefer the CodeWhale settings path for new settings writes while preserving legacy fallbacks.
  • 0eb2ff59a from this PR: isolate the fallback-path tests.
  • 91c5bb64b local follow-up: keep TUI prefs under the same CodeWhale home preference, with legacy ~/.deepseek/tui.toml read fallback.

Local validation:

  • cargo test -p codewhale-tui --all-features --locked settings_path_ -- --nocapture
  • cargo test -p codewhale-tui --all-features --locked tui_prefs_path -- --nocapture
  • cargo test -p codewhale-tui --all-features --locked settings::tests -- --nocapture
  • cargo fmt --all -- --check
  • git diff --check
  • ./scripts/release/check-versions.sh
  • cargo clippy -p codewhale-tui --all-targets --all-features --locked -- -D warnings

I left #2369 open because this fixes the settings/TUI prefs path consolidation slice, but not the broader loud migration UX yet.

@github-actions
Copy link
Copy Markdown

github-actions Bot commented Jun 2, 2026

Thanks @cyq1017 — your contribution landed in 91c5bb64bd04 on main:

fix(tui): keep tui prefs under codewhale home

Closing this PR now that the code is on main. Credit lives in the commit message and (where applicable) the CHANGELOG.md entry for the next release. Apologies for not closing this at the time of the merge — the auto-close workflow is new in v0.8.31.

If you want to land more work and would prefer your future PRs merge cleanly without a harvest step, the CONTRIBUTING.md doc has a short note on what makes a contribution mergeable as-is.

@github-actions github-actions Bot closed this Jun 2, 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.

2 participants