fix(tui): wrap list selection, fakeable home dir, drop dead plan/cache bulk
- #4755: shared list_nav::wrap_index; modal/config lists wrap at both ends (mode, session, file, command palette, feedback, context menu, help, fleet setup/roster, approval/elevation options). - #4757: remaining dirs::home_dir() call sites in the TUI crate use effective_home_dir() so Windows HOME/USERPROFILE test fakes match production paths (paths.rs keeps the OS fallback last). - #3854: ToolCatalogCache stores only the SHA-256 digest. - #3856: remove unused PlanValidation / validate_plan_update.
This commit is contained in:
@@ -73,6 +73,16 @@ existing one behave the way it already claimed to.
|
||||
and opt-in via `[context] project_pack = true`. Language law is compressed
|
||||
while keeping the English-constitution / user-language-reply contract.
|
||||
|
||||
- Modal lists and config pickers wrap selection at both ends (Down past the
|
||||
last row returns to the top). Home-directory resolution prefers
|
||||
`HOME`/`USERPROFILE` via `effective_home_dir` across remaining call sites so
|
||||
Windows tests that fake the home env vars match production paths.
|
||||
|
||||
### Changed
|
||||
|
||||
- Prefix-cache tool catalog entries store only the SHA-256 digest, not the
|
||||
joined catalog string. Unused plan-transition validation helpers are removed.
|
||||
|
||||
## [0.9.1] - 2026-07-24
|
||||
|
||||
### Dogfood follow-ups (2026-07-24)
|
||||
|
||||
@@ -101,7 +101,7 @@ fn artifact_sessions_root() -> Option<PathBuf> {
|
||||
}
|
||||
|
||||
// Honor explicit HOME/USERPROFILE isolation before consulting the host
|
||||
// known-folder API. On Windows, `dirs::home_dir()` can ignore subprocess
|
||||
// known-folder API. On Windows, `crate::config::effective_home_dir()` can ignore subprocess
|
||||
// environment redirection and leak artifacts into the runner profile.
|
||||
let home = crate::config::effective_home_dir()?;
|
||||
let primary = home.join(".codewhale").join("sessions");
|
||||
|
||||
@@ -1170,7 +1170,7 @@ pub fn default_automations_dir() -> PathBuf {
|
||||
if let Some(home) = std::env::var_os("CODEWHALE_HOME").filter(|value| !value.is_empty()) {
|
||||
return PathBuf::from(home).join("automations");
|
||||
}
|
||||
dirs::home_dir()
|
||||
crate::config::effective_home_dir()
|
||||
.map(|home| {
|
||||
let primary = home.join(".codewhale").join("automations");
|
||||
let legacy = home.join(".deepseek").join("automations");
|
||||
|
||||
@@ -116,7 +116,7 @@ pub(crate) fn codex_home_path() -> PathBuf {
|
||||
.filter(|value| !value.is_empty())
|
||||
.map(PathBuf::from)
|
||||
.unwrap_or_else(|| {
|
||||
dirs::home_dir()
|
||||
crate::config::effective_home_dir()
|
||||
.unwrap_or_else(|| PathBuf::from("."))
|
||||
.join(".codex")
|
||||
})
|
||||
|
||||
@@ -2294,11 +2294,11 @@ fn trust_remove(workspace: &Path, raw: &str) -> CommandResult {
|
||||
|
||||
fn expand_tilde(raw: &str) -> String {
|
||||
if let Some(rest) = raw.strip_prefix("~/")
|
||||
&& let Some(home) = dirs::home_dir()
|
||||
&& let Some(home) = crate::config::effective_home_dir()
|
||||
{
|
||||
return home.join(rest).to_string_lossy().into_owned();
|
||||
} else if raw == "~"
|
||||
&& let Some(home) = dirs::home_dir()
|
||||
&& let Some(home) = crate::config::effective_home_dir()
|
||||
{
|
||||
return home.to_string_lossy().into_owned();
|
||||
}
|
||||
|
||||
@@ -394,11 +394,11 @@ pub fn workspace_switch(app: &mut App, arg: Option<&str>) -> CommandResult {
|
||||
|
||||
fn expand_workspace_path(path: &str) -> Result<PathBuf, String> {
|
||||
if path == "~" {
|
||||
return dirs::home_dir().ok_or_else(|| "Could not resolve home directory".to_string());
|
||||
return crate::config::effective_home_dir().ok_or_else(|| "Could not resolve home directory".to_string());
|
||||
}
|
||||
if let Some(rest) = path.strip_prefix("~/") {
|
||||
let home =
|
||||
dirs::home_dir().ok_or_else(|| "Could not resolve home directory".to_string())?;
|
||||
crate::config::effective_home_dir().ok_or_else(|| "Could not resolve home directory".to_string())?;
|
||||
return Ok(home.join(rest));
|
||||
}
|
||||
Ok(PathBuf::from(path))
|
||||
|
||||
@@ -509,7 +509,7 @@ fn install_skill(app: &mut App, args: &str) -> CommandResult {
|
||||
// Legacy no-scope install maps to the CodeWhale global owned root.
|
||||
let target = scope.unwrap_or(SkillTargetScope::Global);
|
||||
let workspace = app.workspace.clone();
|
||||
let home = dirs::home_dir();
|
||||
let home = crate::config::effective_home_dir();
|
||||
let (network, max_size, registry_url) = installer_settings(app);
|
||||
|
||||
let outcome = run_async(async move {
|
||||
@@ -564,7 +564,7 @@ fn update_skill(app: &mut App, args: &str) -> CommandResult {
|
||||
return CommandResult::error("Usage: /skill update [--project|--global] <name>");
|
||||
}
|
||||
let workspace = app.workspace.clone();
|
||||
let home = dirs::home_dir();
|
||||
let home = crate::config::effective_home_dir();
|
||||
let (network, max_size, registry_url) = installer_settings(app);
|
||||
let owned_name = name.to_string();
|
||||
|
||||
@@ -623,7 +623,7 @@ fn uninstall_skill(app: &mut App, args: &str) -> CommandResult {
|
||||
if name.is_empty() {
|
||||
return CommandResult::error("Usage: /skill uninstall [--project|--global] <name>");
|
||||
}
|
||||
let home = dirs::home_dir();
|
||||
let home = crate::config::effective_home_dir();
|
||||
let (network, max_size, registry_url) = installer_settings(app);
|
||||
let ctx = MutationContext {
|
||||
workspace: &app.workspace,
|
||||
@@ -662,7 +662,7 @@ fn trust_skill(app: &mut App, args: &str) -> CommandResult {
|
||||
if name.is_empty() {
|
||||
return CommandResult::error("Usage: /skill trust [--project|--global] <name>");
|
||||
}
|
||||
let home = dirs::home_dir();
|
||||
let home = crate::config::effective_home_dir();
|
||||
let (network, max_size, registry_url) = installer_settings(app);
|
||||
let ctx = MutationContext {
|
||||
workspace: &app.workspace,
|
||||
|
||||
@@ -41,12 +41,12 @@ use super::CommandResult;
|
||||
|
||||
/// Path to the global user commands directory: `~/.codewhale/commands/`.
|
||||
fn global_commands_dir() -> PathBuf {
|
||||
let home = dirs::home_dir().unwrap_or_else(|| PathBuf::from("~"));
|
||||
let home = crate::config::effective_home_dir().unwrap_or_else(|| PathBuf::from("~"));
|
||||
home.join(".codewhale").join("commands")
|
||||
}
|
||||
|
||||
fn legacy_global_commands_dir() -> PathBuf {
|
||||
let home = dirs::home_dir().unwrap_or_else(|| PathBuf::from("~"));
|
||||
let home = crate::config::effective_home_dir().unwrap_or_else(|| PathBuf::from("~"));
|
||||
home.join(".deepseek").join("commands")
|
||||
}
|
||||
|
||||
@@ -73,7 +73,7 @@ pub(crate) fn workflow_dirs(workspace: Option<&Path>) -> Vec<PathBuf> {
|
||||
if let Some(ws) = workspace {
|
||||
dirs.push(ws.join(".codewhale").join("workflows"));
|
||||
}
|
||||
let home = dirs::home_dir().unwrap_or_else(|| PathBuf::from("~"));
|
||||
let home = crate::config::effective_home_dir().unwrap_or_else(|| PathBuf::from("~"));
|
||||
dirs.push(home.join(".codewhale").join("workflows"));
|
||||
dirs
|
||||
}
|
||||
@@ -477,7 +477,7 @@ mod tests {
|
||||
"workspace version",
|
||||
);
|
||||
// Global version — simulate by putting it in a "global" temp dir.
|
||||
// Since we can't easily override `dirs::home_dir()`, we test the
|
||||
// Paths resolve via effective_home_dir (HOME/USERPROFILE-aware). We test the
|
||||
// first-match-wins semantics by putting the same name in both
|
||||
// workspace-scanned dirs. The first dir in precedence order wins.
|
||||
write_command(
|
||||
|
||||
@@ -37,7 +37,7 @@ pub const MAX_HISTORY_ENTRIES: usize = 1000;
|
||||
const HISTORY_FILE_NAME: &str = "composer_history.txt";
|
||||
|
||||
fn default_history_path() -> Option<PathBuf> {
|
||||
history_path_with_home(dirs::home_dir())
|
||||
history_path_with_home(crate::config::effective_home_dir())
|
||||
}
|
||||
|
||||
/// Resolve the composer-history file under `home`, preferring the CodeWhale
|
||||
@@ -290,7 +290,7 @@ mod tests {
|
||||
|
||||
/// Tests use the path-injecting `*_from` / `*_to` helpers so they
|
||||
/// don't have to mutate `HOME` (which is not honored by
|
||||
/// `dirs::home_dir()` on Windows — it reads `USERPROFILE` /
|
||||
/// `crate::config::effective_home_dir()` on Windows — it reads `USERPROFILE` /
|
||||
/// `SHGetKnownFolderPath` instead). This makes the suite portable
|
||||
/// across all three CI runners without per-platform env juggling.
|
||||
fn temp_history_path() -> (tempfile::TempDir, PathBuf) {
|
||||
|
||||
@@ -70,7 +70,7 @@ pub struct StashedDraft {
|
||||
}
|
||||
|
||||
fn default_stash_path() -> Option<PathBuf> {
|
||||
dirs::home_dir().map(|home| {
|
||||
crate::config::effective_home_dir().map(|home| {
|
||||
let primary = home.join(".codewhale").join(STASH_FILE_NAME);
|
||||
let legacy = home.join(".deepseek").join(STASH_FILE_NAME);
|
||||
if primary.exists() || !legacy.exists() {
|
||||
|
||||
@@ -4444,7 +4444,7 @@ fn strip_active_operation_reanchor(prompt: Option<&SystemPrompt>) -> Option<Syst
|
||||
fn default_plugin_tools_dir() -> PathBuf {
|
||||
codewhale_config::codewhale_home()
|
||||
.unwrap_or_else(|_| {
|
||||
dirs::home_dir().map_or_else(|| PathBuf::from(".codewhale"), |h| h.join(".codewhale"))
|
||||
crate::config::effective_home_dir().map_or_else(|| PathBuf::from(".codewhale"), |h| h.join(".codewhale"))
|
||||
})
|
||||
.join("tools")
|
||||
}
|
||||
|
||||
@@ -70,7 +70,7 @@ impl ExecPolicyConfig {
|
||||
}
|
||||
|
||||
pub fn default_execpolicy_path() -> Option<PathBuf> {
|
||||
dirs::home_dir().map(|home| home.join(".deepseek").join("execpolicy.toml"))
|
||||
crate::config::effective_home_dir().map(|home| home.join(".deepseek").join("execpolicy.toml"))
|
||||
}
|
||||
|
||||
pub fn load_default_policy() -> Result<Option<ExecPolicyConfig>> {
|
||||
|
||||
@@ -1333,7 +1333,7 @@ fn main() -> Result<()> {
|
||||
.unwrap_or_else(|| "unknown".to_string());
|
||||
tracing::error!(target: "panic", "Process panicked at {location}: {msg}");
|
||||
// Write crash dump best-effort
|
||||
if let Some(home) = dirs::home_dir() {
|
||||
if let Some(home) = crate::config::effective_home_dir() {
|
||||
let crash_dir = home.join(".deepseek").join("crashes");
|
||||
let _ = std::fs::create_dir_all(&crash_dir);
|
||||
use chrono::Utc;
|
||||
@@ -2984,7 +2984,7 @@ fn resolve_cors_origins(config: &Config, flag_origins: &[String]) -> Vec<String>
|
||||
|
||||
fn deepseek_home_dir() -> PathBuf {
|
||||
codewhale_config::codewhale_home().unwrap_or_else(|_| {
|
||||
dirs::home_dir().map_or_else(|| PathBuf::from(".codewhale"), |h| h.join(".codewhale"))
|
||||
crate::config::effective_home_dir().map_or_else(|| PathBuf::from(".codewhale"), |h| h.join(".codewhale"))
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -498,7 +498,7 @@ impl McpServer {
|
||||
}
|
||||
|
||||
fn default_config_path() -> Option<PathBuf> {
|
||||
dirs::home_dir().map(|home| home.join(".deepseek").join("mcp_server.toml"))
|
||||
crate::config::effective_home_dir().map(|home| home.join(".deepseek").join("mcp_server.toml"))
|
||||
}
|
||||
|
||||
fn default_expose_tools() -> Vec<String> {
|
||||
|
||||
@@ -321,7 +321,7 @@ impl NetworkAuditor {
|
||||
/// home directory can't be resolved.
|
||||
#[must_use]
|
||||
pub fn default_path(enabled: bool) -> Option<Self> {
|
||||
let home = dirs::home_dir()?;
|
||||
let home = crate::config::effective_home_dir()?;
|
||||
Some(Self::new(
|
||||
home.join(".codewhale").join("audit.log"),
|
||||
enabled,
|
||||
|
||||
@@ -61,7 +61,7 @@ pub fn auth_file_path() -> PathBuf {
|
||||
let codex_home = std::env::var("CODEX_HOME")
|
||||
.map(PathBuf::from)
|
||||
.unwrap_or_else(|_| {
|
||||
dirs::home_dir()
|
||||
crate::config::effective_home_dir()
|
||||
.unwrap_or_else(|| PathBuf::from("."))
|
||||
.join(".codex")
|
||||
});
|
||||
|
||||
@@ -32,7 +32,6 @@
|
||||
use std::collections::hash_map::DefaultHasher;
|
||||
use std::collections::{HashMap, VecDeque};
|
||||
use std::hash::{Hash, Hasher};
|
||||
use std::sync::Arc;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
@@ -190,17 +189,14 @@ pub struct PrefixStabilityManager {
|
||||
/// "session + 1 or 2 forked subagent catalogs" without unbounded growth.
|
||||
const TOOL_CATALOG_CACHE_CAPACITY: usize = 8;
|
||||
|
||||
/// Bounded LRU cache of `(tool_set_identity) -> (sha256_hex, joined_string)`.
|
||||
/// Bounded LRU cache of `(tool_set_identity) -> sha256_hex`.
|
||||
///
|
||||
/// The cache key is a content-derived `u64` hash of the tool list (length +
|
||||
/// per-tool `name` + `description` + serialized `input_schema`). On a hit,
|
||||
/// `PrefixFingerprint::compute` skips the per-tool JSON serialization, the
|
||||
/// sort, and the join — a workload that can be 100+ microseconds for a
|
||||
/// 60-tool catalog. On a miss, the work runs once and the result is stored.
|
||||
///
|
||||
/// The cache is intentionally *not* generic over `PrefixFingerprint` because
|
||||
/// only the joined string is large; the SHA-256 is recomputed from the cached
|
||||
/// joined string when the catalog changes (cheap, ≤ a few hundred bytes).
|
||||
/// 60-tool catalog. On a miss, the work runs once and only the digest is
|
||||
/// retained (#3854); the joined catalog string is ephemeral.
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub struct ToolCatalogCache {
|
||||
by_identity: HashMap<u64, CachedCatalog>,
|
||||
@@ -208,18 +204,11 @@ pub struct ToolCatalogCache {
|
||||
capacity: usize,
|
||||
}
|
||||
|
||||
/// One entry in [`ToolCatalogCache`]. Stores the joined JSON catalog plus
|
||||
/// the pre-computed SHA-256 hex digest so `PrefixFingerprint::compute`
|
||||
/// does not need to re-hash on the hot path.
|
||||
/// One entry in [`ToolCatalogCache`]. Production only needs the pre-computed
|
||||
/// SHA-256 digest of the sorted joined catalog.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct CachedCatalog {
|
||||
/// The newline-joined, sorted tool-catalog JSON. Wrapped in an `Arc` so
|
||||
/// multiple cache consumers can hold the same allocation. Exposed for
|
||||
/// observability (debug builds, `/status` chip) and for tests that
|
||||
/// need to assert byte-stability of the joined catalog.
|
||||
#[allow(dead_code)] // observability + tests; not consumed on the hot path
|
||||
pub joined: Arc<String>,
|
||||
/// SHA-256 hex digest of `joined`, computed once on cache miss.
|
||||
/// SHA-256 hex digest of the newline-joined, sorted tool-catalog JSON.
|
||||
pub sha256_hex: String,
|
||||
}
|
||||
|
||||
@@ -248,20 +237,16 @@ impl ToolCatalogCache {
|
||||
pub fn fingerprint_for(&mut self, tools: &[Tool]) -> CachedCatalog {
|
||||
let identity = tool_set_identity(tools);
|
||||
if let Some(cached) = self.by_identity.get(&identity) {
|
||||
// Hit: clone the `Arc` so the caller can hold the joined string
|
||||
// without keeping a reference to the cache.
|
||||
return cached.clone();
|
||||
}
|
||||
|
||||
// Miss: serialize, sort, join, hash. Store the joined string in an
|
||||
// `Arc` so a later hit can return the same allocation.
|
||||
// Miss: serialize, sort, join, hash. Keep only the digest in the
|
||||
// cache — the joined string is not needed on the hot path (#3854).
|
||||
let mut serialized: Vec<String> = tools.iter().filter_map(tool_to_api_json).collect();
|
||||
serialized.sort();
|
||||
let joined = Arc::new(serialized.join("\n"));
|
||||
let sha256_hex = sha256_hex(joined.as_bytes());
|
||||
let joined = serialized.join("\n");
|
||||
let entry = CachedCatalog {
|
||||
joined: Arc::clone(&joined),
|
||||
sha256_hex,
|
||||
sha256_hex: sha256_hex(joined.as_bytes()),
|
||||
};
|
||||
|
||||
if self.by_identity.len() >= self.capacity
|
||||
@@ -778,7 +763,7 @@ mod tests {
|
||||
// ── ToolCatalogCache tests ──────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn tool_catalog_cache_miss_then_hit_returns_same_arc() {
|
||||
fn tool_catalog_cache_miss_then_hit_returns_same_digest() {
|
||||
let mut cache = ToolCatalogCache::new();
|
||||
let tools = vec![make_tool("read_file"), make_tool("write_file")];
|
||||
|
||||
@@ -787,7 +772,6 @@ mod tests {
|
||||
|
||||
let second = cache.fingerprint_for(&tools);
|
||||
assert_eq!(cache.len(), 1, "second call should be a cache hit");
|
||||
assert!(Arc::ptr_eq(&first.joined, &second.joined));
|
||||
assert_eq!(first.sha256_hex, second.sha256_hex);
|
||||
}
|
||||
|
||||
@@ -801,7 +785,6 @@ mod tests {
|
||||
let entry_b = cache.fingerprint_for(&b);
|
||||
assert_eq!(cache.len(), 2);
|
||||
assert_ne!(entry_a.sha256_hex, entry_b.sha256_hex);
|
||||
assert!(!Arc::ptr_eq(&entry_a.joined, &entry_b.joined));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -815,9 +798,9 @@ mod tests {
|
||||
let b = vec![make_tool("write_file"), make_tool("read_file")];
|
||||
let entry_a = cache.fingerprint_for(&a);
|
||||
let entry_b = cache.fingerprint_for(&b);
|
||||
// Joined output is the same (sorted) but the two cache entries are
|
||||
// distinct because their identities differ.
|
||||
assert_eq!(entry_a.joined.as_str(), entry_b.joined.as_str());
|
||||
// Digests match (sorted join) but the two cache entries are distinct
|
||||
// because their identities differ.
|
||||
assert_eq!(entry_a.sha256_hex, entry_b.sha256_hex);
|
||||
assert_eq!(cache.len(), 2);
|
||||
}
|
||||
|
||||
@@ -847,9 +830,9 @@ mod tests {
|
||||
// capacity 2 means oldest is evicted when we insert the 3rd unique.
|
||||
// After inserting a, the cache holds the most recent 2: {c, a}.
|
||||
assert_eq!(cache.len(), 2);
|
||||
// The returned entry should be the same as a fresh fingerprint.
|
||||
// The returned digest should match a fresh fingerprint of the same set.
|
||||
let fresh = cache.fingerprint_for(&[make_tool("a")]);
|
||||
assert!(Arc::ptr_eq(&re_entry.joined, &fresh.joined));
|
||||
assert_eq!(re_entry.sha256_hex, fresh.sha256_hex);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -869,7 +852,7 @@ mod tests {
|
||||
let entry = cache.fingerprint_for(&[]);
|
||||
assert!(!entry.sha256_hex.is_empty());
|
||||
let again = cache.fingerprint_for(&[]);
|
||||
assert!(Arc::ptr_eq(&entry.joined, &again.joined));
|
||||
assert_eq!(entry.sha256_hex, again.sha256_hex);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -930,7 +930,7 @@ pub fn load_project_context(workspace: &Path) -> ProjectContext {
|
||||
///
|
||||
/// This allows for monorepo setups where a root AGENTS.md applies to all subdirectories.
|
||||
pub fn load_project_context_with_parents(workspace: &Path) -> ProjectContext {
|
||||
load_project_context_with_parents_cached_and_home(workspace, dirs::home_dir().as_deref())
|
||||
load_project_context_with_parents_cached_and_home(workspace, crate::config::effective_home_dir().as_deref())
|
||||
}
|
||||
|
||||
fn load_project_context_with_parents_cached_and_home(
|
||||
@@ -1187,7 +1187,7 @@ fn find_git_root(cwd: &Path) -> Option<PathBuf> {
|
||||
}
|
||||
|
||||
fn project_context_parent_search_stop_dir() -> Option<PathBuf> {
|
||||
dirs::home_dir().map(|home| canonicalize_workspace_or_keep(&home))
|
||||
crate::config::effective_home_dir().map(|home| canonicalize_workspace_or_keep(&home))
|
||||
}
|
||||
|
||||
/// Combine global user-wide preferences with a project-local
|
||||
|
||||
@@ -486,7 +486,7 @@ pub async fn run_http_server(
|
||||
);
|
||||
|
||||
let sessions_dir = default_sessions_dir().unwrap_or_else(|_| {
|
||||
dirs::home_dir()
|
||||
crate::config::effective_home_dir()
|
||||
.map(|h| h.join(".deepseek").join("sessions"))
|
||||
.unwrap_or_else(|| PathBuf::from(".deepseek").join("sessions"))
|
||||
});
|
||||
|
||||
@@ -240,7 +240,7 @@ pub(crate) fn log_directory() -> Option<PathBuf> {
|
||||
{
|
||||
return resolve(userprofile);
|
||||
}
|
||||
dirs::home_dir().and_then(resolve)
|
||||
crate::config::effective_home_dir().and_then(resolve)
|
||||
}
|
||||
|
||||
fn log_file_name(date: &str, pid: u32) -> String {
|
||||
|
||||
@@ -61,7 +61,7 @@ fn reqwest_client() -> reqwest::Client {
|
||||
/// Lives at `~/.codewhale/cache/skills/` so it's separate from user-installed
|
||||
/// skills and can be blown away without losing anything irreplaceable.
|
||||
pub fn default_cache_skills_dir() -> PathBuf {
|
||||
dirs::home_dir().map_or_else(
|
||||
crate::config::effective_home_dir().map_or_else(
|
||||
|| PathBuf::from("/tmp/codewhale/cache/skills"),
|
||||
|p| p.join(".codewhale").join("cache").join("skills"),
|
||||
)
|
||||
|
||||
@@ -42,7 +42,7 @@ const MAX_SKILL_NAME_CHARS: usize = 64;
|
||||
|
||||
#[must_use]
|
||||
pub fn default_skills_dir() -> PathBuf {
|
||||
dirs::home_dir().map_or_else(
|
||||
crate::config::effective_home_dir().map_or_else(
|
||||
|| PathBuf::from("/tmp/codewhale/skills"),
|
||||
|p| p.join(".codewhale").join("skills"),
|
||||
)
|
||||
@@ -51,7 +51,7 @@ pub fn default_skills_dir() -> PathBuf {
|
||||
/// Global agentskills.io-compatible skills directory (`~/.agents/skills`).
|
||||
#[must_use]
|
||||
pub fn agents_global_skills_dir() -> Option<PathBuf> {
|
||||
dirs::home_dir().map(|p| p.join(".agents").join("skills"))
|
||||
crate::config::effective_home_dir().map(|p| p.join(".agents").join("skills"))
|
||||
}
|
||||
|
||||
// === Types ===
|
||||
@@ -714,7 +714,7 @@ pub fn skills_directories(workspace: &Path) -> Vec<PathBuf> {
|
||||
|
||||
#[must_use]
|
||||
pub fn skills_directories_for_mode(workspace: &Path, mode: SkillDiscoveryMode) -> Vec<PathBuf> {
|
||||
let home = dirs::home_dir();
|
||||
let home = crate::config::effective_home_dir();
|
||||
skills_directories_with_home_and_mode(workspace, home.as_deref(), mode)
|
||||
}
|
||||
|
||||
@@ -1049,7 +1049,7 @@ fn sanitize_prompt_path_text(text: &str, workspace: &Path) -> String {
|
||||
{
|
||||
out = out.replace(ws, ".");
|
||||
}
|
||||
if let Some(home) = dirs::home_dir()
|
||||
if let Some(home) = crate::config::effective_home_dir()
|
||||
&& let Some(home_str) = home.to_str()
|
||||
&& !home_str.is_empty()
|
||||
{
|
||||
@@ -1078,7 +1078,7 @@ fn privacy_safe_skill_path(path: &Path, workspace: &Path) -> String {
|
||||
if let Ok(rel) = path.strip_prefix(workspace) {
|
||||
return rel.display().to_string();
|
||||
}
|
||||
if let Some(home) = dirs::home_dir()
|
||||
if let Some(home) = crate::config::effective_home_dir()
|
||||
&& let Ok(rel) = path.strip_prefix(&home)
|
||||
{
|
||||
return format!("~/{}", rel.display());
|
||||
|
||||
@@ -964,7 +964,7 @@ fn redact_exported_text(text: &mut String) {
|
||||
}
|
||||
|
||||
// Normalise secrets directory paths.
|
||||
if let Some(home) = dirs::home_dir() {
|
||||
if let Some(home) = crate::config::effective_home_dir() {
|
||||
for leaf in [".codewhale/secrets", ".deepseek/secrets"] {
|
||||
let dir = home.join(leaf);
|
||||
let prefix = dir.to_string_lossy().to_string();
|
||||
|
||||
@@ -22,7 +22,7 @@ use std::path::{Path, PathBuf};
|
||||
/// snapshots cross-worktree if they want, but the `worktree_hash` keeps
|
||||
/// commits isolated by default.
|
||||
pub fn snapshot_dir_for(workspace: &Path) -> PathBuf {
|
||||
snapshot_dir_with_home(workspace, dirs::home_dir())
|
||||
snapshot_dir_with_home(workspace, crate::config::effective_home_dir())
|
||||
}
|
||||
|
||||
/// Same as [`snapshot_dir_for`] but with an injectable home directory.
|
||||
|
||||
@@ -226,7 +226,7 @@ impl SnapshotRepo {
|
||||
.canonicalize()
|
||||
.unwrap_or_else(|_| workspace.to_path_buf());
|
||||
if let Some(reason) =
|
||||
unsafe_workspace_snapshot_reason(&work_tree, dirs::home_dir().as_deref())
|
||||
unsafe_workspace_snapshot_reason(&work_tree, crate::config::effective_home_dir().as_deref())
|
||||
{
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidInput,
|
||||
@@ -1000,7 +1000,7 @@ mod tests {
|
||||
}
|
||||
|
||||
/// Build a side-repo whose snapshot dir lives under the same
|
||||
/// tempdir we're using for `HOME` — so the inner `dirs::home_dir()`
|
||||
/// tempdir we're using for `HOME` — so the inner `crate::config::effective_home_dir()`
|
||||
/// lookup stays inside our sandbox. Returns the guard alongside so
|
||||
/// the caller can keep HOME pinned for the rest of the test.
|
||||
fn make_repo(tmp: &Path) -> (SnapshotRepo, ScopedHome) {
|
||||
|
||||
@@ -1854,7 +1854,7 @@ pub fn default_tasks_dir() -> PathBuf {
|
||||
return PathBuf::from(path);
|
||||
}
|
||||
}
|
||||
dirs::home_dir()
|
||||
crate::config::effective_home_dir()
|
||||
.map(|home| default_tasks_dir_for_home(&home))
|
||||
.unwrap_or_else(|| PathBuf::from(".codewhale").join("tasks"))
|
||||
}
|
||||
|
||||
@@ -325,6 +325,7 @@ impl PlanState {
|
||||
state
|
||||
}
|
||||
|
||||
#[allow(dead_code)] // retained for PlanState consumers / older tests
|
||||
pub fn steps(&self) -> &[PlanStep] {
|
||||
&self.steps
|
||||
}
|
||||
@@ -373,48 +374,6 @@ fn clean_list(values: Vec<String>) -> Vec<String> {
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Validation result for plan transitions
|
||||
#[derive(Debug)]
|
||||
#[allow(dead_code)]
|
||||
pub enum PlanValidation {
|
||||
Ok,
|
||||
Warning(String),
|
||||
Error(String),
|
||||
}
|
||||
|
||||
/// Validate a plan update
|
||||
#[allow(dead_code)]
|
||||
pub fn validate_plan_update(current: &PlanState, update: &UpdatePlanArgs) -> PlanValidation {
|
||||
let current_steps: std::collections::HashMap<_, _> = current
|
||||
.steps()
|
||||
.iter()
|
||||
.map(|s| (s.text.clone(), &s.status))
|
||||
.collect();
|
||||
|
||||
for item in &update.plan {
|
||||
if let Some(old_status) = current_steps.get(&item.step) {
|
||||
// Check for invalid transitions
|
||||
match (old_status, &item.status) {
|
||||
(StepStatus::Completed, StepStatus::Pending) => {
|
||||
return PlanValidation::Warning(format!(
|
||||
"Step '{}' was completed but is now pending",
|
||||
item.step
|
||||
));
|
||||
}
|
||||
(StepStatus::Completed, StepStatus::InProgress) => {
|
||||
return PlanValidation::Warning(format!(
|
||||
"Step '{}' was completed but is now in progress",
|
||||
item.step
|
||||
));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
PlanValidation::Ok
|
||||
}
|
||||
|
||||
// === UpdatePlanTool - ToolSpec implementation ===
|
||||
|
||||
/// Shared reference to `PlanState` for use across tools
|
||||
|
||||
@@ -632,7 +632,7 @@ mod tests {
|
||||
);
|
||||
|
||||
// Keep this test independent of the process-native home directory:
|
||||
// `dirs::home_dir()` cannot be redirected reliably after process start
|
||||
// `crate::config::effective_home_dir()` cannot be redirected reliably after process start
|
||||
// on Windows. The injected-home discovery test in `skills::tests`
|
||||
// separately proves that ~/.codewhale/skills enters the default catalog.
|
||||
let context = ToolContext::new(&workspace).with_skills_config(global_skills.clone(), false);
|
||||
|
||||
@@ -91,7 +91,7 @@ pub fn spillover_root() -> Option<PathBuf> {
|
||||
return Some(root);
|
||||
}
|
||||
|
||||
let home = dirs::home_dir()?;
|
||||
let home = crate::config::effective_home_dir()?;
|
||||
let primary = home.join(".codewhale").join(SPILLOVER_DIR_NAME);
|
||||
let legacy = home.join(".deepseek").join(SPILLOVER_DIR_NAME);
|
||||
if primary.exists() || !legacy.exists() {
|
||||
|
||||
@@ -1320,14 +1320,13 @@ impl ApprovalView {
|
||||
}
|
||||
|
||||
fn select_prev(&mut self) {
|
||||
self.selected = self.selected.saturating_sub(1);
|
||||
let len = ApprovalOption::order_for(&self.request.tool_name).len();
|
||||
self.selected = crate::tui::list_nav::wrap_index(self.selected, len, -1);
|
||||
}
|
||||
|
||||
fn select_next(&mut self) {
|
||||
let max = ApprovalOption::order_for(&self.request.tool_name)
|
||||
.len()
|
||||
.saturating_sub(1);
|
||||
self.selected = (self.selected + 1).min(max);
|
||||
let len = ApprovalOption::order_for(&self.request.tool_name).len();
|
||||
self.selected = crate::tui::list_nav::wrap_index(self.selected, len, 1);
|
||||
}
|
||||
|
||||
fn current_option(&self) -> ApprovalOption {
|
||||
@@ -1728,12 +1727,13 @@ impl ElevationView {
|
||||
}
|
||||
|
||||
fn select_prev(&mut self) {
|
||||
self.selected = self.selected.saturating_sub(1);
|
||||
self.selected =
|
||||
crate::tui::list_nav::wrap_index(self.selected, self.request.options.len(), -1);
|
||||
}
|
||||
|
||||
fn select_next(&mut self) {
|
||||
let max = self.request.options.len().saturating_sub(1);
|
||||
self.selected = (self.selected + 1).min(max);
|
||||
self.selected =
|
||||
crate::tui::list_nav::wrap_index(self.selected, self.request.options.len(), 1);
|
||||
}
|
||||
|
||||
fn current_option(&self) -> &ElevationOption {
|
||||
|
||||
@@ -566,7 +566,7 @@ fn osc52_sequence(text: &str) -> Result<String> {
|
||||
/// matches the location described in user-facing docs; falls back to
|
||||
/// `<workspace>/clipboard-images/` if the home dir is unavailable.
|
||||
pub(crate) fn clipboard_images_dir(workspace: &Path) -> PathBuf {
|
||||
let home = dirs::home_dir();
|
||||
let home = crate::config::effective_home_dir();
|
||||
clipboard_images_dir_for_home(workspace, home.as_deref())
|
||||
}
|
||||
|
||||
|
||||
@@ -817,13 +817,8 @@ impl CommandPaletteView {
|
||||
}
|
||||
|
||||
fn move_selection(&mut self, delta: isize) {
|
||||
if self.filtered.is_empty() {
|
||||
self.selected = 0;
|
||||
return;
|
||||
}
|
||||
let len = self.filtered.len() as isize;
|
||||
let next = (self.selected as isize + delta).clamp(0, len - 1) as usize;
|
||||
self.selected = next;
|
||||
self.selected =
|
||||
crate::tui::list_nav::wrap_index(self.selected, self.filtered.len(), delta);
|
||||
}
|
||||
|
||||
fn selected_entry(&self) -> Option<&CommandPaletteEntry> {
|
||||
|
||||
@@ -128,12 +128,8 @@ impl ContextMenuView {
|
||||
}
|
||||
|
||||
fn move_selection(&mut self, delta: isize) {
|
||||
if self.entries.is_empty() {
|
||||
self.selected = 0;
|
||||
return;
|
||||
}
|
||||
let max = self.entries.len().saturating_sub(1) as isize;
|
||||
self.selected = (self.selected as isize + delta).clamp(0, max) as usize;
|
||||
self.selected =
|
||||
crate::tui::list_nav::wrap_index(self.selected, self.entries.len(), delta);
|
||||
}
|
||||
|
||||
fn menu_width(&self, area_width: u16) -> u16 {
|
||||
|
||||
@@ -55,16 +55,11 @@ impl FeedbackPickerView {
|
||||
}
|
||||
|
||||
fn move_up(&mut self) {
|
||||
if self.selected > 0 {
|
||||
self.selected -= 1;
|
||||
}
|
||||
self.selected = crate::tui::list_nav::wrap_index(self.selected, OPTIONS.len(), -1);
|
||||
}
|
||||
|
||||
fn move_down(&mut self) {
|
||||
let max = OPTIONS.len().saturating_sub(1);
|
||||
if self.selected < max {
|
||||
self.selected += 1;
|
||||
}
|
||||
self.selected = crate::tui::list_nav::wrap_index(self.selected, OPTIONS.len(), 1);
|
||||
}
|
||||
|
||||
fn select_number(&mut self, number: char) -> Option<ViewAction> {
|
||||
|
||||
@@ -233,13 +233,8 @@ impl FilePickerView {
|
||||
if self.filtered.is_empty() {
|
||||
return;
|
||||
}
|
||||
let max = self.filtered.len() - 1;
|
||||
let next = if delta.is_negative() {
|
||||
self.selected.saturating_sub(delta.unsigned_abs())
|
||||
} else {
|
||||
(self.selected + delta as usize).min(max)
|
||||
};
|
||||
self.selected = next;
|
||||
self.selected =
|
||||
crate::tui::list_nav::wrap_index(self.selected, self.filtered.len(), delta);
|
||||
self.adjust_scroll();
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
//! Shared list-selection navigation (#4755).
|
||||
//!
|
||||
//! Modal lists and config screens should wrap at the ends so Down on the last
|
||||
//! row returns to the top and Up on the first row returns to the bottom.
|
||||
//! Centralizing the arithmetic keeps that behavior consistent without each
|
||||
//! picker inventing its own clamp.
|
||||
|
||||
/// Move a 0-based selection by `delta`, wrapping at both ends.
|
||||
///
|
||||
/// Empty lists leave the selection at `0`. A zero `len` is treated as empty.
|
||||
#[must_use]
|
||||
pub fn wrap_index(selected: usize, len: usize, delta: isize) -> usize {
|
||||
if len == 0 {
|
||||
return 0;
|
||||
}
|
||||
(selected as isize + delta).rem_euclid(len as isize) as usize
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::wrap_index;
|
||||
|
||||
#[test]
|
||||
fn wraps_forward_and_backward() {
|
||||
assert_eq!(wrap_index(0, 3, -1), 2);
|
||||
assert_eq!(wrap_index(2, 3, 1), 0);
|
||||
assert_eq!(wrap_index(1, 3, 1), 2);
|
||||
assert_eq!(wrap_index(1, 3, -1), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_list_stays_at_zero() {
|
||||
assert_eq!(wrap_index(5, 0, 1), 0);
|
||||
assert_eq!(wrap_index(0, 0, -1), 0);
|
||||
}
|
||||
}
|
||||
@@ -51,6 +51,7 @@ pub mod hover_layer;
|
||||
pub mod key_actions;
|
||||
pub mod key_shortcuts;
|
||||
pub mod keybindings;
|
||||
pub mod list_nav;
|
||||
pub mod live_transcript;
|
||||
pub mod markdown_render;
|
||||
mod mcp_routing;
|
||||
|
||||
@@ -179,13 +179,8 @@ impl SessionPickerView {
|
||||
}
|
||||
|
||||
fn move_selection(&mut self, delta: isize) {
|
||||
if self.filtered.is_empty() {
|
||||
self.selected = 0;
|
||||
return;
|
||||
}
|
||||
let len = self.filtered.len() as isize;
|
||||
let next = (self.selected as isize + delta).clamp(0, len - 1) as usize;
|
||||
self.selected = next;
|
||||
self.selected =
|
||||
crate::tui::list_nav::wrap_index(self.selected, self.filtered.len(), delta);
|
||||
self.ensure_selected_visible();
|
||||
self.refresh_preview();
|
||||
}
|
||||
|
||||
@@ -522,7 +522,7 @@ impl SetupRuntimeFacts {
|
||||
|
||||
fn setup_codewhale_home_dir() -> std::path::PathBuf {
|
||||
codewhale_config::codewhale_home().unwrap_or_else(|_| {
|
||||
dirs::home_dir().map_or_else(
|
||||
crate::config::effective_home_dir().map_or_else(
|
||||
|| std::path::PathBuf::from(".codewhale"),
|
||||
|home| home.join(".codewhale"),
|
||||
)
|
||||
|
||||
@@ -11473,7 +11473,7 @@ fn mcp_import_consent_path() -> PathBuf {
|
||||
|
||||
fn mcp_external_import_status_text(workspace: &std::path::Path) -> String {
|
||||
use crate::mcp::external_import::{discover_external_sources, format_candidates_for_display};
|
||||
let home = dirs::home_dir().unwrap_or_else(|| PathBuf::from("."));
|
||||
let home = crate::config::effective_home_dir().unwrap_or_else(|| PathBuf::from("."));
|
||||
let market_path = codewhale_config::codewhale_home()
|
||||
.ok()
|
||||
.map(|h| h.join("mcp-marketplace.json"));
|
||||
@@ -11497,7 +11497,7 @@ fn mcp_import_apply(
|
||||
use std::collections::HashMap;
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
let home = dirs::home_dir().unwrap_or_else(|| PathBuf::from("."));
|
||||
let home = crate::config::effective_home_dir().unwrap_or_else(|| PathBuf::from("."));
|
||||
let market_path = codewhale_config::codewhale_home()
|
||||
.ok()
|
||||
.map(|h| h.join("mcp-marketplace.json"));
|
||||
@@ -12851,7 +12851,7 @@ async fn handle_skill_mutation_requested(
|
||||
};
|
||||
|
||||
let workspace = app.workspace.clone();
|
||||
let home = dirs::home_dir();
|
||||
let home = crate::config::effective_home_dir();
|
||||
let cfg = crate::config::Config::load(None, None).unwrap_or_default();
|
||||
let network = cfg
|
||||
.network
|
||||
|
||||
@@ -126,12 +126,12 @@ impl FleetRosterView {
|
||||
}
|
||||
|
||||
fn move_up(&mut self) {
|
||||
self.selected = self.selected.saturating_sub(1);
|
||||
self.selected = crate::tui::list_nav::wrap_index(self.selected, self.row_count(), -1);
|
||||
self.detail_scroll = 0;
|
||||
}
|
||||
|
||||
fn move_down(&mut self) {
|
||||
self.selected = (self.selected + 1).min(self.row_count().saturating_sub(1));
|
||||
self.selected = crate::tui::list_nav::wrap_index(self.selected, self.row_count(), 1);
|
||||
self.detail_scroll = 0;
|
||||
}
|
||||
|
||||
|
||||
@@ -634,11 +634,13 @@ impl FleetSetupView {
|
||||
fn move_up(&mut self) {
|
||||
match self.step {
|
||||
Step::Role => {
|
||||
self.role_idx = self.role_idx.saturating_sub(1);
|
||||
self.role_idx =
|
||||
crate::tui::list_nav::wrap_index(self.role_idx, self.step_len(), -1);
|
||||
self.discard_model_draft();
|
||||
}
|
||||
Step::Model => {
|
||||
self.model_idx = self.model_idx.saturating_sub(1);
|
||||
self.model_idx =
|
||||
crate::tui::list_nav::wrap_index(self.model_idx, self.step_len(), -1);
|
||||
self.discard_model_draft();
|
||||
}
|
||||
Step::Review => self.review_scroll = self.review_scroll.saturating_sub(1),
|
||||
@@ -655,11 +657,13 @@ impl FleetSetupView {
|
||||
fn move_down(&mut self) {
|
||||
match self.step {
|
||||
Step::Role => {
|
||||
self.role_idx = (self.role_idx + 1).min(self.step_len().saturating_sub(1));
|
||||
self.role_idx =
|
||||
crate::tui::list_nav::wrap_index(self.role_idx, self.step_len(), 1);
|
||||
self.discard_model_draft();
|
||||
}
|
||||
Step::Model => {
|
||||
self.model_idx = (self.model_idx + 1).min(self.step_len().saturating_sub(1));
|
||||
self.model_idx =
|
||||
crate::tui::list_nav::wrap_index(self.model_idx, self.step_len(), 1);
|
||||
self.discard_model_draft();
|
||||
}
|
||||
Step::Review => self.review_scroll = self.review_scroll.saturating_add(1),
|
||||
|
||||
@@ -195,23 +195,13 @@ impl HelpView {
|
||||
}
|
||||
|
||||
fn move_selection(&mut self, delta: isize) {
|
||||
if self.filtered.is_empty() {
|
||||
self.selected = 0;
|
||||
return;
|
||||
}
|
||||
let len = self.filtered.len() as isize;
|
||||
let next = (self.selected as isize + delta).clamp(0, len - 1) as usize;
|
||||
self.selected = next;
|
||||
// #4755: help list wraps at both ends (same as other modal lists).
|
||||
self.selected =
|
||||
crate::tui::list_nav::wrap_index(self.selected, self.filtered.len(), delta);
|
||||
}
|
||||
|
||||
fn move_selection_wrapping(&mut self, delta: isize) {
|
||||
if self.filtered.is_empty() {
|
||||
self.selected = 0;
|
||||
return;
|
||||
}
|
||||
let len = self.filtered.len() as isize;
|
||||
let next = (self.selected as isize + delta).rem_euclid(len) as usize;
|
||||
self.selected = next;
|
||||
self.move_selection(delta);
|
||||
}
|
||||
|
||||
fn render_rows(&self) -> Vec<HelpRenderRow> {
|
||||
|
||||
@@ -2148,12 +2148,7 @@ impl ConfigView {
|
||||
.iter()
|
||||
.position(|idx| *idx == self.selected)
|
||||
.unwrap_or(0);
|
||||
let max = matches.len().saturating_sub(1);
|
||||
let next = if delta.is_negative() {
|
||||
current.saturating_sub(delta.unsigned_abs())
|
||||
} else {
|
||||
(current + delta as usize).min(max)
|
||||
};
|
||||
let next = crate::tui::list_nav::wrap_index(current, matches.len(), delta);
|
||||
|
||||
self.selected = matches[next];
|
||||
let visible_rows = self.visible_rows_cached();
|
||||
|
||||
@@ -52,16 +52,11 @@ impl ModePickerView {
|
||||
}
|
||||
|
||||
fn move_up(&mut self) {
|
||||
if self.cursor > 0 {
|
||||
self.cursor -= 1;
|
||||
}
|
||||
self.cursor = crate::tui::list_nav::wrap_index(self.cursor, VISIBLE_MODES.len(), -1);
|
||||
}
|
||||
|
||||
fn move_down(&mut self) {
|
||||
let max = VISIBLE_MODES.len().saturating_sub(1);
|
||||
if self.cursor < max {
|
||||
self.cursor += 1;
|
||||
}
|
||||
self.cursor = crate::tui::list_nav::wrap_index(self.cursor, VISIBLE_MODES.len(), 1);
|
||||
}
|
||||
|
||||
fn select_by_number(&mut self, number: char) -> Option<ViewAction> {
|
||||
|
||||
@@ -536,7 +536,7 @@ fn write_panic_dump(
|
||||
location: &std::panic::Location<'_>,
|
||||
message: &str,
|
||||
) -> std::io::Result<()> {
|
||||
let home = dirs::home_dir().ok_or_else(|| {
|
||||
let home = crate::config::effective_home_dir().ok_or_else(|| {
|
||||
std::io::Error::new(std::io::ErrorKind::NotFound, "home directory not found")
|
||||
})?;
|
||||
// Prefer .codewhale, fall back to .deepseek
|
||||
@@ -662,11 +662,11 @@ pub fn url_encode(input: &str) -> String {
|
||||
/// resolve correctly across processes.
|
||||
#[must_use]
|
||||
pub fn display_path(path: &Path) -> String {
|
||||
display_path_with_home(path, dirs::home_dir().as_deref())
|
||||
display_path_with_home(path, crate::config::effective_home_dir().as_deref())
|
||||
}
|
||||
|
||||
/// Like [`display_path`] but takes an explicit home directory instead of
|
||||
/// reading `$HOME` / `dirs::home_dir()`. Used in tests and anywhere the
|
||||
/// reading `$HOME` / `crate::config::effective_home_dir()`. Used in tests and anywhere the
|
||||
/// caller already has the home path available.
|
||||
///
|
||||
/// The home-relative suffix is rejoined with the platform separator
|
||||
@@ -1092,7 +1092,7 @@ mod spawn_supervised_tests {
|
||||
/// A spawned task that panics does not propagate the panic to the
|
||||
/// parent task — `spawn_supervised` catches it. Verified in isolation
|
||||
/// from the on-disk crash-dump path so the test is portable across
|
||||
/// macOS / Linux / Windows (where `dirs::home_dir()` reads
|
||||
/// macOS / Linux / Windows (where `crate::config::effective_home_dir()` reads
|
||||
/// `USERPROFILE`, not `HOME`, so env-mutation tricks don't redirect
|
||||
/// the dump on Windows).
|
||||
#[tokio::test]
|
||||
@@ -1143,7 +1143,7 @@ mod spawn_supervised_tests {
|
||||
|
||||
/// `write_panic_dump_to` writes a properly-formatted crash log into
|
||||
/// the supplied directory. Tested separately from `spawn_supervised`
|
||||
/// because env-mutation redirection of `dirs::home_dir()` doesn't
|
||||
/// because env-mutation redirection of `crate::config::effective_home_dir()` doesn't
|
||||
/// work on Windows.
|
||||
#[test]
|
||||
fn write_panic_dump_writes_named_log() {
|
||||
|
||||
@@ -192,7 +192,7 @@ pub fn auth_file_path() -> PathBuf {
|
||||
return codewhale_config::resolve_external_credential_path(&path).unwrap_or(path);
|
||||
}
|
||||
}
|
||||
let path = dirs::home_dir()
|
||||
let path = crate::config::effective_home_dir()
|
||||
.unwrap_or_else(|| PathBuf::from("."))
|
||||
.join(".grok")
|
||||
.join("auth.json");
|
||||
|
||||
Reference in New Issue
Block a user