refactor(runtime): move runtime_log and working_set out of crates/tui into codewhale-runtime [skip ci]

Closes the RS-10 leaf moves for #6937 (CW-6129 decomposition, slices S1 and
S3, built on a Boat VM against a frozen snapshot and reviewed here).

- runtime_log moves to codewhale-runtime (adds tracing-subscriber there);
  its four consumers and the docs path are rewritten by
  scripts/split/move-modules.py with no re-export facade. The one test that
  reached into tui state helpers moves to tui's session_manager test owner.
- working_set moves to codewhale-runtime. summary_block now takes a required
  read_allowed predicate with no default, so the credential-path denylist
  cannot be skipped; tui passes tools::file::working_set_read_allowed, a
  one-line wrapper over the unchanged enforce_read_denylist(path,
  "working_set"). The estimate_tokens test moves to compaction's test owner.
  Two methods widen to pub and the test-only items become
  any(test, feature = "test-support") for tui's tests.
- Generated agent-docs data and the blocking-calls budget json follow.

Checks, frozen 2,190 tracked inputs on 1480e84fd3 plus these paths, unchanged
across the runs: `cargo check -p codewhale-runtime -p codewhale-tui --lib
--tests --locked` with RUSTFLAGS="-D warnings" exit 0; runtime runtime_log and
working_set cases 40 of 40; 312 of 312 focused tui cases (summary_block,
file_mention, mention_completion, runtime_api::workspace, core::engine::preview,
session_manager, compaction, persistence_actor, the two relocated tests);
137 of 137 session_manager cases after restoring one include!d test file to its
unformatted-by-rustfmt original. dead-code budget 164 (at budget), blocking
calls 569 sites / 169 files. Strict production lines (VM count_lines.py):
tui 573,775 -> 572,080, runtime 32,101 -> 33,815.

Not run: clippy, the full suite, the module-graph ratchet, hosted CI.
S2 (artifacts, workspace_trust and friends) is blocked by a runtime<->util
cargo cycle and is not part of this commit.
This commit is contained in:
CodeWhale Bot committed 2026-10-29 20:39:38 -07:00
1 parent 1480e84fd3
commit e5fa43aa82
23 files changed
+146 -116

No files matched your search

Generated
+2
View File
@@ -1373,6 +1373,7 @@ dependencies = [
"dirs 7.0.0",
"fd-lock",
"futures-util",
"ignore",
"jsonwebtoken",
"libc",
"lru",
@@ -1392,6 +1393,7 @@ dependencies = [
"toml 1.1.4+spec-1.1.0",
"toml_edit 0.25.15+spec-1.1.0",
"tracing",
"tracing-subscriber",
"uuid",
"wait-timeout",
"windows",
+2
View File
@@ -61,6 +61,7 @@ codewhale-palette = { path = "../palette", version = "0.10.2", default-features
codewhale-secrets = { path = "../secrets", version = "0.10.2" }
colored = "3.0.0"
fd-lock = "4.0.4"
ignore = "0.4"
lru = "0.18"
regex = "1.11"
serde = { workspace = true, features = ["rc"] }
@@ -72,6 +73,7 @@ tokio.workspace = true
tokio-util = { version = "0.7.16", features = ["io"] }
toml.workspace = true
tracing.workspace = true
tracing-subscriber.workspace = true
uuid.workspace = true
wait-timeout = "0.2"
+2
View File
@@ -38,6 +38,7 @@ pub mod prompt_zones;
pub mod regex_cache;
pub mod resource_telemetry;
pub mod retry_status;
pub mod runtime_log;
pub mod runtime_policy;
pub mod safe_label;
pub mod session_tree;
@@ -49,6 +50,7 @@ pub mod startup_trace;
pub mod test_support;
pub mod tool_history_repair;
pub mod turn_budget;
pub mod working_set;
pub mod workspace_discovery;
pub const CODEWHALE_MAIN_STACK_BYTES: usize = 32 * 1024 * 1024;
@@ -210,7 +210,7 @@ fn open_log_file(path: &Path) -> std::io::Result<std::fs::File> {
}
}
pub(crate) fn log_directory() -> Option<PathBuf> {
pub fn log_directory() -> Option<PathBuf> {
// $CODEWHALE_HOME is a hard override of the base data directory
// (docs/CONFIGURATION.md): when SET, logs live under it and we do NOT fall
// back to the legacy ~/.deepseek path — silent fallback would defeat the
@@ -410,38 +410,6 @@ mod tests {
assert_eq!(mode(&target), 0o644);
}
#[test]
fn whitespace_home_override_is_consistent_across_tui_state_entry_points() {
let _lock = crate::test_support::lock_test_env();
let tmp = tempfile::TempDir::new().expect("temporary root");
let home = tmp.path().join("home");
let userprofile = tmp.path().join("userprofile");
let _home = crate::test_support::EnvVarGuard::set("HOME", &home);
let _userprofile = crate::test_support::EnvVarGuard::set("USERPROFILE", &userprofile);
let _codewhale_home = crate::test_support::EnvVarGuard::set("CODEWHALE_HOME", " \t ");
let _config_path = crate::test_support::EnvVarGuard::remove("CODEWHALE_CONFIG_PATH");
let _legacy_config_path = crate::test_support::EnvVarGuard::remove("DEEPSEEK_CONFIG_PATH");
let primary = home.join(".codewhale");
assert_eq!(codewhale_paths::user_home(), Some(home.clone()));
assert_eq!(
codewhale_runtime::config::workspace_trust_config_candidate_paths(),
vec![
primary.join("config.toml"),
home.join(".deepseek").join("config.toml")
]
);
assert_eq!(log_directory(), Some(primary.join("logs")));
assert_eq!(
crate::automation_manager::default_automations_dir(),
primary.join("automations")
);
assert_eq!(
crate::session_manager::default_sessions_dir().expect("session directory"),
primary.join("sessions")
);
}
fn set_modified(path: &Path, modified: SystemTime) {
let file = OpenOptions::new().write(true).open(path).unwrap();
file.set_times(FileTimes::new().set_modified(modified))
@@ -6,10 +6,10 @@
//! - a compact working-set summary block for the system prompt
//! - pinned message indices that compaction should preserve
use codewhale_models::{ContentBlock, Message};
use codewhale_runtime::workspace_discovery::{
use crate::workspace_discovery::{
DISCOVERY_ALWAYS_DIRS, path_is_excluded_from_discovery, should_skip_unignored_discovery_entry,
};
use codewhale_models::{ContentBlock, Message};
use ignore::WalkBuilder;
use regex::Regex;
use serde::{Deserialize, Serialize};
@@ -30,7 +30,7 @@ use std::sync::OnceLock;
pub struct Workspace {
pub root: PathBuf,
cwd: Option<PathBuf>,
#[cfg(test)]
#[cfg(any(test, feature = "test-support"))]
file_index: OnceLock<HashMap<String, Vec<PathBuf>>>,
completion_walk_depth: Option<usize>,
/// Follow symbolic links during file discovery walks. When `true`,
@@ -105,7 +105,7 @@ impl Workspace {
Self {
root,
cwd,
#[cfg(test)]
#[cfg(any(test, feature = "test-support"))]
file_index: OnceLock::new(),
completion_walk_depth: normalize_completion_walk_depth(walk_depth),
follow_links,
@@ -113,7 +113,7 @@ impl Workspace {
}
/// Two-pass resolution: workspace, then cwd, then fuzzy fallback.
#[cfg(test)]
#[cfg(any(test, feature = "test-support"))]
pub fn resolve(&self, raw_path: &str) -> Result<PathBuf, PathBuf> {
let literal = self.resolve_exact(raw_path);
if literal.is_ok() {
@@ -154,7 +154,7 @@ impl Workspace {
Err(ws_path)
}
#[cfg(test)]
#[cfg(any(test, feature = "test-support"))]
fn fuzzy_resolve(&self, path: &Path) -> Option<PathBuf> {
let needle = path.file_name()?.to_string_lossy().to_lowercase();
if needle.is_empty() {
@@ -165,7 +165,7 @@ impl Workspace {
index.get(&needle).and_then(|paths| paths.first()).cloned()
}
#[cfg(test)]
#[cfg(any(test, feature = "test-support"))]
fn build_file_index(&self) -> HashMap<String, Vec<PathBuf>> {
let mut index: HashMap<String, Vec<PathBuf>> = HashMap::new();
let mut total: usize = 0;
@@ -274,7 +274,7 @@ impl Workspace {
/// Honors `.gitignore`, `.git/info/exclude`, `.ignore`, and
/// `.deepseekignore`. Capped at `limit` results.
#[must_use]
#[cfg(test)]
#[cfg(any(test, feature = "test-support"))]
pub fn completions(&self, partial: &str, limit: usize) -> Vec<String> {
if limit == 0 {
return Vec::new();
@@ -352,7 +352,7 @@ impl Workspace {
/// callers must fall back to [`Workspace::completions`] for path-like
/// needles (starting with `.` or containing a separator).
#[must_use]
#[cfg(test)]
#[cfg(any(test, feature = "test-support"))]
pub fn completion_candidates(&self) -> Vec<String> {
let never_cancelled = || false;
self.completion_candidates_inner(usize::MAX, false, &never_cancelled)
@@ -362,7 +362,7 @@ impl Workspace {
/// is hard-capped and checks `cancelled` between filesystem iterator
/// steps. A single slow iterator step may still return late, but it runs on
/// the discovery worker and can never park terminal input (#4365).
pub(crate) fn completion_discovery_candidates(
pub fn completion_discovery_candidates(
&self,
limit: usize,
cancelled: &dyn Fn() -> bool,
@@ -442,7 +442,7 @@ impl Workspace {
/// returns only that directory's immediate children in case-insensitive
/// alphabetical order.
#[must_use]
#[cfg(test)]
#[cfg(any(test, feature = "test-support"))]
pub fn browser_completions(&self, partial: &str, limit: usize) -> Vec<String> {
if limit == 0 {
return Vec::new();
@@ -457,7 +457,7 @@ impl Workspace {
/// Background-worker directory-browser collection. It stops at `limit`
/// before sorting, so a directory with millions of direct children cannot
/// grow the cache without bound.
pub(crate) fn browser_completion_discovery_candidates(
pub fn browser_completion_discovery_candidates(
&self,
partial: &str,
limit: usize,
@@ -565,7 +565,7 @@ fn normalize_completion_walk_depth(depth: usize) -> Option<usize> {
if depth == 0 { None } else { Some(depth) }
}
#[cfg(test)]
#[cfg(any(test, feature = "test-support"))]
fn child_completion_walk_depth(depth: Option<usize>) -> Option<usize> {
depth.map(|depth| depth.saturating_sub(1))
}
@@ -577,7 +577,7 @@ fn child_completion_walk_depth(depth: Option<usize>) -> Option<usize> {
/// `fuzzy_resolve` call bounded on huge workspaces (#697 reported a
/// ~10s hang on the first turn). For typical projects 50K is well
/// above the actual entry count and the cap is a no-op.
#[cfg(test)]
#[cfg(any(test, feature = "test-support"))]
const FILE_INDEX_MAX_ENTRIES: usize = 50_000;
/// Configure a `WalkBuilder` for workspace discovery: hidden files,
@@ -702,7 +702,7 @@ fn walk_for_completions(
const LOCAL_REFERENCE_SCAN_LIMIT: usize = 4096;
#[cfg(test)]
#[cfg(any(test, feature = "test-support"))]
fn add_local_reference_completions(
root: &Path,
display_root: &Path,
@@ -796,7 +796,7 @@ pub fn rank_completion_candidates(
prefix_hits
}
#[cfg(test)]
#[cfg(any(test, feature = "test-support"))]
fn should_try_local_reference_completion(needle: &str) -> bool {
if needle.is_empty() {
return false;
@@ -812,7 +812,7 @@ fn should_try_local_reference_completion(needle: &str) -> bool {
needle.starts_with('.') || needle.contains('/') || needle.contains('\\')
}
#[cfg(test)]
#[cfg(any(test, feature = "test-support"))]
fn local_reference_paths(
root: &Path,
limit: usize,
@@ -872,7 +872,7 @@ impl Clone for Workspace {
Self {
root: self.root.clone(),
cwd: self.cwd.clone(),
#[cfg(test)]
#[cfg(any(test, feature = "test-support"))]
file_index: OnceLock::new(),
completion_walk_depth: self.completion_walk_depth,
follow_links: self.follow_links,
@@ -1066,7 +1066,11 @@ impl WorkingSet {
/// The block lands in the system prompt before the historical
/// conversation; any byte that drifts here cache-misses everything that
/// follows in DeepSeek's KV prefix cache.
pub fn summary_block(&self, workspace: &Path) -> Option<String> {
pub fn summary_block(
&self,
workspace: &Path,
read_allowed: &dyn Fn(&Path) -> bool,
) -> Option<String> {
// Only stat-verified paths reach the model. Prose observation happily
// records tokens that merely look like paths ("120x40",
// "codewhale-hq/CodeWhale"), and a fabricated Active-paths line teaches the
@@ -1119,7 +1123,12 @@ impl WorkingSet {
if self.cache_maximal_enabled() && !prompt_entries.is_empty() {
let content_entries: Vec<&WorkingSetEntry> =
prompt_entries.iter().map(|(entry, _)| *entry).collect();
self.append_resident_file_contents(&mut lines, workspace, &content_entries);
self.append_resident_file_contents(
&mut lines,
workspace,
&content_entries,
read_allowed,
);
}
Some(lines.join("\n"))
@@ -1150,6 +1159,7 @@ impl WorkingSet {
lines: &mut Vec<String>,
workspace: &Path,
prompt_entries: &[&WorkingSetEntry],
read_allowed: &dyn Fn(&Path) -> bool,
) {
let mut header_pushed = false;
let mut total_bytes: usize = 0;
@@ -1165,7 +1175,7 @@ impl WorkingSet {
}
let abs = workspace.join(&entry.path);
if crate::tools::file::enforce_read_denylist(&abs, "working_set").is_err() {
if !read_allowed(&abs) {
if !header_pushed {
lines.push("### Active file contents (cache-resident)".to_string());
header_pushed = true;
@@ -1628,16 +1638,6 @@ mod tests {
}
}
fn make_message(role: &str, text: &str) -> Message {
Message {
role: Role::from(role),
content: vec![ContentBlock::Text {
text: text.to_string(),
cache_control: None,
}],
}
}
#[test]
fn observe_user_message_tracks_paths() {
let tmp = TempDir::new().expect("tempdir");
@@ -1678,7 +1678,7 @@ mod tests {
let mut ws = WorkingSet::default();
ws.observe_user_message("src/lib.rs", tmp.path());
let block = ws.summary_block(tmp.path()).expect("block");
let block = ws.summary_block(tmp.path(), &|_| true).expect("block");
assert!(block.contains("Repo Working Set"));
assert!(!block.contains("Workspace:"));
@@ -1708,9 +1708,13 @@ mod tests {
let mut ws = WorkingSet::default();
ws.observe_user_message("Edit src/a.rs and src/b.rs", tmp.path());
let before = ws.summary_block(tmp.path()).expect("block before");
let before = ws
.summary_block(tmp.path(), &|_| true)
.expect("block before");
ws.next_turn();
let after = ws.summary_block(tmp.path()).expect("block after");
let after = ws
.summary_block(tmp.path(), &|_| true)
.expect("block after");
assert_byte_identical(
"summary_block must be stable across next_turn when no new paths touched",
@@ -1734,10 +1738,14 @@ mod tests {
let mut ws = WorkingSet::default();
ws.observe_user_message("src/a.rs", tmp.path());
let before = ws.summary_block(tmp.path()).expect("block before");
let before = ws
.summary_block(tmp.path(), &|_| true)
.expect("block before");
ws.observe_user_message("src/c.rs", tmp.path());
let after = ws.summary_block(tmp.path()).expect("block after");
let after = ws
.summary_block(tmp.path(), &|_| true)
.expect("block after");
assert_ne!(before, after, "new path must update the rendered summary");
assert!(after.contains("src/c.rs"));
@@ -1759,7 +1767,7 @@ mod tests {
tmp.path(),
);
let block = ws.summary_block(tmp.path()).expect("block");
let block = ws.summary_block(tmp.path(), &|_| true).expect("block");
assert!(block.contains("- src/real.rs (file)"), "{block}");
assert!(!block.contains("120x40"), "{block}");
assert!(!block.contains("codewhale-hq/CodeWhale"), "{block}");
@@ -1767,7 +1775,7 @@ mod tests {
// A file deleted mid-session falls out on the next render — the same
// filesystem-changed exception #280 makes for newly observed paths.
fs::remove_file(src.join("real.rs")).expect("remove");
let after_delete = ws.summary_block(tmp.path());
let after_delete = ws.summary_block(tmp.path(), &|_| true);
assert!(
after_delete
.as_deref()
@@ -1796,7 +1804,7 @@ mod tests {
let mut ws = WorkingSet::default(); // cache_maximal defaults to false
ws.observe_user_message("src/lib.rs", tmp.path());
let block = ws.summary_block(tmp.path()).expect("block");
let block = ws.summary_block(tmp.path(), &|_| true).expect("block");
assert!(block.contains("src/lib.rs"), "path list still present");
assert!(
@@ -1815,7 +1823,7 @@ mod tests {
let mut ws = cache_maximal_ws();
ws.observe_user_message("src/lib.rs", tmp.path());
let block = ws.summary_block(tmp.path()).expect("block");
let block = ws.summary_block(tmp.path(), &|_| true).expect("block");
assert!(block.contains("Active file contents (cache-resident)"));
assert!(block.contains("<!-- file: src/lib.rs -->"));
@@ -1830,7 +1838,7 @@ mod tests {
let mut ws = cache_maximal_ws();
ws.observe_user_message("look in src/", tmp.path());
let block = ws.summary_block(tmp.path()).expect("block");
let block = ws.summary_block(tmp.path(), &|_| true).expect("block");
// `src` is a dir; it appears in the path list but has no content block.
assert!(!block.contains("<!-- file: src -->"));
@@ -1848,7 +1856,7 @@ mod tests {
ws.config.max_resident_file_bytes = 100;
ws.config.max_total_resident_bytes = 10_000;
ws.observe_user_message("src/big.rs", tmp.path());
let block = ws.summary_block(tmp.path()).expect("block");
let block = ws.summary_block(tmp.path(), &|_| true).expect("block");
assert!(block.contains("truncated for prompt budget"));
// The full 10k body must not be inlined.
@@ -1867,7 +1875,7 @@ mod tests {
ws.config.max_resident_file_bytes = 200;
ws.config.max_total_resident_bytes = 200; // only one file fits
ws.observe_user_message("Edit src/a.rs and src/b.rs", tmp.path());
let block = ws.summary_block(tmp.path()).expect("block");
let block = ws.summary_block(tmp.path(), &|_| true).expect("block");
assert!(
block.contains("omitted from the cache-resident budget"),
@@ -1886,9 +1894,9 @@ mod tests {
let mut ws = cache_maximal_ws();
ws.observe_user_message("src/a.rs", tmp.path());
let before = ws.summary_block(tmp.path()).expect("before");
let before = ws.summary_block(tmp.path(), &|_| true).expect("before");
ws.next_turn();
let after = ws.summary_block(tmp.path()).expect("after");
let after = ws.summary_block(tmp.path(), &|_| true).expect("after");
assert_byte_identical(
"cache-maximal block must be stable while files are unchanged (KV cache hit)",
@@ -1907,10 +1915,10 @@ mod tests {
let mut ws = cache_maximal_ws();
ws.observe_user_message("src/a.rs", tmp.path());
let before = ws.summary_block(tmp.path()).expect("before");
let before = ws.summary_block(tmp.path(), &|_| true).expect("before");
fs::write(&file, "fn a() { todo!() }").expect("rewrite");
let after = ws.summary_block(tmp.path()).expect("after");
let after = ws.summary_block(tmp.path(), &|_| true).expect("after");
assert_ne!(before, after, "editing the file must change the block");
assert!(after.contains("todo!()"));
@@ -1955,13 +1963,6 @@ mod tests {
assert!(a_score >= b_score);
}
#[test]
fn estimate_tokens_is_available_for_future_budgeting() {
use crate::compaction::estimate_tokens;
let messages = vec![make_message("user", "src/main.rs")];
assert!(estimate_tokens(&messages) > 0);
}
#[test]
fn workspace_resolve_respects_cwd_and_workspace() {
let tmp = TempDir::new().unwrap();
+12
View File
@@ -628,3 +628,15 @@ fn pruning_metadata_belongs_to_the_exact_execution() {
.any(|item| item.message_idx == 1)
);
}
#[test]
fn estimate_tokens_is_available_for_future_budgeting() {
let messages = vec![Message {
role: codewhale_models::Role::from("user"),
content: vec![ContentBlock::Text {
text: "src/main.rs".to_string(),
cache_control: None,
}],
}];
assert!(estimate_tokens(&messages) > 0);
}
+8 -4
View File
@@ -56,7 +56,6 @@ use crate::tools::subagent::{
use crate::tools::todo::{SharedTodoList, new_shared_todo_list};
use crate::tools::user_input::{UserInputRequest, UserInputResponse};
use crate::tools::{ToolContext, ToolRegistryBuilder};
use crate::working_set::WorkingSet;
use codewhale_agent_contract::role::FleetRole;
use codewhale_agent_contract::worker_profile::WorkerRuntimeProfile;
use codewhale_config::AppMode;
@@ -74,6 +73,7 @@ use codewhale_models::{
};
use codewhale_runtime::config::{Config, DEFAULT_MAX_SUBAGENTS};
use codewhale_runtime::features::{Feature, Features};
use codewhale_runtime::working_set::WorkingSet;
use codewhale_snapshot::{WorkspaceSnapshotKind, WorkspaceSnapshotRef};
use codewhale_util::utils::spawn_supervised;
@@ -194,7 +194,8 @@ impl StructuredState {
subagents: Option<&SharedSubAgentManager>,
active_session_id: &str,
) -> Self {
let working_set_summary = working_set.summary_block(&workspace);
let working_set_summary =
working_set.summary_block(&workspace, &crate::tools::file::working_set_read_allowed);
let subagent_snapshots = if let Some(handle) = subagents {
let mut guard = handle.write().await;
@@ -4999,7 +5000,10 @@ impl Engine {
} = snapshot;
let today = chrono::Local::now().format("%Y-%m-%d").to_string();
let working_set_summary = working_set
.summary_block(&self.config.workspace)
.summary_block(
&self.config.workspace,
&crate::tools::file::working_set_read_allowed,
)
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty());
@@ -10600,7 +10604,7 @@ pub(crate) struct TurnMetadataSnapshot<'a> {
pub(crate) prompt_context: &'a NextTurnPromptContext,
pub(crate) system_prompt: Option<&'a SystemPrompt>,
pub(crate) approval_mode: ApprovalMode,
pub(crate) working_set: &'a crate::working_set::WorkingSet,
pub(crate) working_set: &'a codewhale_runtime::working_set::WorkingSet,
pub(crate) policy_narrowing: Option<&'a PolicyNarrowingEvent>,
pub(crate) messages: &'a [Message],
}
+8 -8
View File
@@ -2310,10 +2310,10 @@ async fn building_a_manifest_writes_no_engine_state() {
let model_before = engine.session.model.clone();
let system_prompt_before = system_prompt_hash(engine.session.system_prompt.as_ref());
let system_hash_before = engine.session.last_system_prompt_hash;
let working_set_before = engine
.session
.working_set
.summary_block(&engine.config.workspace);
let working_set_before = engine.session.working_set.summary_block(
&engine.config.workspace,
&crate::tools::file::working_set_read_allowed,
);
let provider_before = engine.api_provider;
let mode_before = engine.current_mode;
let narrowing_before = format!("{:?}", engine.last_policy_narrowing);
@@ -2345,10 +2345,10 @@ async fn building_a_manifest_writes_no_engine_state() {
);
assert_eq!(engine.session.last_system_prompt_hash, system_hash_before);
assert_eq!(
engine
.session
.working_set
.summary_block(&engine.config.workspace),
engine.session.working_set.summary_block(
&engine.config.workspace,
&crate::tools::file::working_set_read_allowed,
),
working_set_before,
"the hypothetical message is observed on a clone, never on the session"
);
+1 -1
View File
@@ -3,11 +3,11 @@
//! Tracks conversation history, token usage, and session metadata.
use crate::project_context::ProjectContext;
use crate::working_set::WorkingSet;
use codewhale_core::prefix_cache::PrefixStabilityManager;
use codewhale_execpolicy::ApprovalMode;
use codewhale_models::{Message, SystemPrompt, Usage};
use codewhale_runtime::prompt_zones::{AppendLog, FrozenPrefix};
use codewhale_runtime::working_set::WorkingSet;
use std::collections::{HashMap, HashSet, VecDeque};
use std::path::PathBuf;
+1 -1
View File
@@ -43,7 +43,7 @@ impl DoctorPathReport {
.context("could not resolve the canonical settings path")?;
let sessions = codewhale_config::resolve_state_dir("sessions")
.context("could not resolve the sessions path")?;
let logs = crate::runtime_log::log_directory()
let logs = codewhale_runtime::runtime_log::log_directory()
.context("could not resolve the runtime log directory")?;
let automations = crate::automation_manager::default_automations_dir();
let task_manager_root = crate::task_manager::default_tasks_dir();
-2
View File
@@ -92,7 +92,6 @@ mod route_runtime;
mod runtime_api;
mod runtime_chat_relay;
mod runtime_handoff;
mod runtime_log;
mod runtime_threads;
#[cfg(test)]
mod sandbox_stay_tests;
@@ -131,7 +130,6 @@ mod vision;
mod voice;
pub mod whats_new;
mod work_graph;
mod working_set;
mod workspace_lease;
mod workspace_trust;
+2 -2
View File
@@ -213,7 +213,7 @@ fn is_symlink_refusal(error: &std::io::Error) -> bool {
/// entries so one listing covers every text log the runtime writes.
fn log_sources() -> Vec<(PathBuf, Vec<PathBuf>)> {
let mut sources = Vec::new();
if let Some(dir) = crate::runtime_log::log_directory() {
if let Some(dir) = codewhale_runtime::runtime_log::log_directory() {
let mut singles = Vec::new();
if let Ok(home) = codewhale_config::codewhale_home() {
for name in ["audit.log", "audit.log.1"] {
@@ -308,7 +308,7 @@ pub(super) async fn read_log(
.map_err(|error| ApiError::internal(format!("home unavailable: {error}")))?;
return read_named_window(&home, &name, query);
}
let dir = crate::runtime_log::log_directory()
let dir = codewhale_runtime::runtime_log::log_directory()
.ok_or_else(|| ApiError::not_found("no log directory"))?;
read_named_window(&dir, &name, query)
})
+1 -1
View File
@@ -56,7 +56,7 @@ fn collect_workspace_file_suggestions(
query: &str,
limit: usize,
) -> Result<Vec<String>, ApiError> {
use crate::working_set::{Workspace, rank_completion_candidates};
use codewhale_runtime::working_set::{Workspace, rank_completion_candidates};
let root = root
.canonicalize()
@@ -311,3 +311,38 @@
);
}
}
#[test]
fn whitespace_home_override_is_consistent_across_tui_state_entry_points() {
let _lock = crate::test_support::lock_test_env();
let tmp = tempfile::TempDir::new().expect("temporary root");
let home = tmp.path().join("home");
let userprofile = tmp.path().join("userprofile");
let _home = crate::test_support::EnvVarGuard::set("HOME", &home);
let _userprofile = crate::test_support::EnvVarGuard::set("USERPROFILE", &userprofile);
let _codewhale_home = crate::test_support::EnvVarGuard::set("CODEWHALE_HOME", " \t ");
let _config_path = crate::test_support::EnvVarGuard::remove("CODEWHALE_CONFIG_PATH");
let _legacy_config_path = crate::test_support::EnvVarGuard::remove("DEEPSEEK_CONFIG_PATH");
let primary = home.join(".codewhale");
assert_eq!(codewhale_paths::user_home(), Some(home.clone()));
assert_eq!(
codewhale_runtime::config::workspace_trust_config_candidate_paths(),
vec![
primary.join("config.toml"),
home.join(".deepseek").join("config.toml")
]
);
assert_eq!(
codewhale_runtime::runtime_log::log_directory(),
Some(primary.join("logs"))
);
assert_eq!(
crate::automation_manager::default_automations_dir(),
primary.join("automations")
);
assert_eq!(
default_sessions_dir().expect("session directory"),
primary.join("sessions")
);
}
+4
View File
@@ -457,6 +457,10 @@ pub(crate) fn enforce_read_denylist(path: &Path, tool: &str) -> Result<(), ToolE
}
}
pub(crate) fn working_set_read_allowed(path: &Path) -> bool {
enforce_read_denylist(path, "working_set").is_ok()
}
/// Return whether `read_file` must refuse a CodeWhale-owned credential file.
///
/// This is deliberately scoped to the active config, the two conventional
+1 -1
View File
@@ -321,7 +321,7 @@ impl RenderDebugLog {
return None;
}
let log_dir = crate::runtime_log::log_directory()?;
let log_dir = codewhale_runtime::runtime_log::log_directory()?;
if let Err(err) = fs::create_dir_all(&log_dir) {
tracing::debug!(?err, "failed to create TUI render debug log directory");
return None;
+4 -2
View File
@@ -28,11 +28,11 @@ use std::path::{Path, PathBuf};
use crate::tui::app::{App, MentionCompletionCache};
use crate::tui::git_mention::{self, GitMentionCache, GitMentionKind};
use crate::tui::mention_completion::{MentionDiscoveryBehavior, MentionDiscoveryKey};
use crate::working_set::Workspace;
use codewhale_core::{
ContextReference, ContextReferenceKind, ContextReferenceSource, MediaAttachmentReference,
media_attachment_references,
};
use codewhale_runtime::working_set::Workspace;
/// Maximum number of `@`-mentions whose contents are inlined into one user
/// message. Beyond this we stop appending blocks but the raw `@token` text
@@ -239,7 +239,9 @@ fn mention_menu_entries(app: &mut App, partial: &str, limit: usize) -> (Vec<Stri
};
let entries = match &discovery_key.behavior {
MentionDiscoveryBehavior::Fuzzy => {
let ranked = crate::working_set::rank_completion_candidates(candidates, partial, limit);
let ranked = codewhale_runtime::working_set::rank_completion_candidates(
candidates, partial, limit,
);
super::file_frecency::rerank_by_frecency(ranked)
}
MentionDiscoveryBehavior::Browser { .. } => {
+1 -1
View File
@@ -14,7 +14,7 @@ use std::time::{Duration, Instant};
use parking_lot::{Condvar, Mutex};
use crate::working_set::Workspace;
use codewhale_runtime::working_set::Workspace;
/// Keep discovery memory and background work bounded even when the workspace
/// is an unignored drive root. The popup itself renders far fewer rows, but a
+1 -1
View File
@@ -1029,7 +1029,7 @@ pub async fn run_tui(
// messages reach the user's terminal. We accept the init failing (e.g.,
// read-only $HOME) and continue without the redirect rather than refusing
// to start the TUI.
let _tui_log_guard = match crate::runtime_log::init() {
let _tui_log_guard = match codewhale_runtime::runtime_log::init() {
Ok(guard) => Some(guard),
Err(err) => {
tracing::warn!(target: "runtime_log", ?err, "TUI log init failed; stderr leaks may render as scroll-demon");
+1 -1
View File
@@ -34,12 +34,12 @@ use crate::tui::shell_key_routing::{
};
use crate::tui::views::ConfigView;
use crate::tui::views::{HelpView, ModalView, ViewAction};
use crate::working_set::Workspace;
use codewhale_config::ProviderKind;
use codewhale_config::descriptors::defaults::{
DEFAULT_OPENROUTER_MODEL, DEFAULT_TEXT_MODEL, DEFAULT_ZAI_MODEL,
};
use codewhale_runtime::config::{Config, ProviderConfig, ProvidersConfig};
use codewhale_runtime::working_set::Workspace;
use codewhale_tui_kit::ui_text::{line_to_plain, truncate_line_to_width};
use crossterm::event::{KeyEvent, MouseButton, MouseEvent, MouseEventKind};
use ratatui::{
+1 -1
View File
@@ -514,7 +514,7 @@
"crates/tui/src/voice.rs": {
"std_fs": 3
},
"crates/tui/src/working_set.rs": {
"crates/runtime/src/working_set.rs": {
"path_canonicalize": 1,
"std_fs": 1
},
+2 -2
View File
@@ -336,8 +336,8 @@
},
{
"path": "docs/CONTRIBUTORS.md",
"sha256": "1a3b7738b6f8c3f3b931eb39c581d5999e63c2aa7bdf393ac7325814a254e565",
"content": "# Contributors\n\nCodewhale is built in the open with a growing community of contributors. Every\nissue report and pull request is real project work — welcome at any experience\nlevel. This is the full per-PR contributor record, grouped into release/time\nbands with the newest band first so it stays scannable. Expand any band to see\neveryone.\n\nFor the live list, see the\n[GitHub contributors page](https://github.com/codewhale-hq/CodeWhale/graphs/contributors),\n[`AUTHOR_MAP`](https://github.com/codewhale-hq/CodeWhale/blob/main/.github/AUTHOR_MAP),\nand [CHANGELOG.md](../CHANGELOG.md).\n\n<!-- contributor-wall:start -->\n## Everyone\n\n[![421 people built Codewhale](assets/contributors.png)](#everyone)\n\n421 people have code, co-authored commits, carried-forward pull requests or credited reports in Codewhale. GitHub's contributor list shows only commit authors whose email is linked to their account, so it counts 203 of them. If your commits are missing there, add your commit email to your GitHub account and they will link.\n\n[0gl20shk0sbt36](https://github.com/0gl20shk0sbt36) · [1Git2Clone](https://github.com/1Git2Clone) · [20bytes](https://github.com/20bytes) · [47Cid](https://github.com/47Cid) · [7jrxt42BxFZo4iAnN4CX](https://github.com/7jrxt42BxFZo4iAnN4CX) · [8sero](https://github.com/8sero) · [A-Corner](https://github.com/A-Corner) · [aboimpinto](https://github.com/aboimpinto) · [adity982](https://github.com/adity982) · [AdityaVG13](https://github.com/AdityaVG13) · [AGSaturn](https://github.com/AGSaturn) · [ai-dengwsh](https://github.com/ai-dengwsh) · [aiapienthusiast](https://github.com/aiapienthusiast) · [Aitensa](https://github.com/Aitensa) · [AiurArtanis](https://github.com/AiurArtanis) · [alitvak69](https://github.com/alitvak69) · [all-lopezg](https://github.com/all-lopezg) · [alvin1](https://github.com/alvin1) · [amuthantamil](https://github.com/amuthantamil) · [AnaheimEX](https://github.com/AnaheimEX) · [Andrea-Bruno](https://github.com/Andrea-Bruno) · [Angel-Hair](https://github.com/Angel-Hair) · [angus-guo](https://github.com/angus-guo) · [angziii](https://github.com/angziii) · [AnonymousUser443](https://github.com/AnonymousUser443) · [antarikshraya](https://github.com/antarikshraya) · [Anyexyz](https://github.com/Anyexyz) · [Apeiron0w0](https://github.com/Apeiron0w0) · [aqilaziz](https://github.com/aqilaziz) · [AresNing](https://github.com/AresNing) · [ArronAI007](https://github.com/ArronAI007) · [Artenx](https://github.com/Artenx) · [asdfg314284230](https://github.com/asdfg314284230) · [Asteria1028](https://github.com/Asteria1028) · [asto18089](https://github.com/asto18089) · [atmosuwiryo](https://github.com/atmosuwiryo) · [axobase001](https://github.com/axobase001) · [aznikline](https://github.com/aznikline) · [baendlorel](https://github.com/baendlorel) · [banqii](https://github.com/banqii) · [Beltran12138](https://github.com/Beltran12138) · [bevis-wong](https://github.com/bevis-wong) · [BH8GCJ](https://github.com/BH8GCJ) · [BigBenLabs](https://github.com/BigBenLabs) · [Biilow](https://github.com/Biilow) · [bistack](https://github.com/bistack) · [BrathonBai](https://github.com/BrathonBai) · [bruce6135](https://github.com/bruce6135) · [BryonGo](https://github.com/BryonGo) · [buiducnhat](https://github.com/buiducnhat) · [buko](https://github.com/buko) · [BX166](https://github.com/BX166) · [c020627](https://github.com/c020627) · [cacdcaecawae](https://github.com/cacdcaecawae) · [caeserchen](https://github.com/caeserchen) · [caiyilian](https://github.com/caiyilian) · [CCChisato](https://github.com/CCChisato) · [ccomma](https://github.com/ccomma) · [cenab](https://github.com/cenab) · [ChaceLyee2101](https://github.com/ChaceLyee2101) · [chatman-media](https://github.com/chatman-media) · [chennest](https://github.com/chennest) · [chinaqy110](https://github.com/chinaqy110) · [chnjames](https://github.com/chnjames) · [chuntseevolving](https://github.com/chuntseevolving) · [ci4ic4](https://github.com/ci4ic4) · [cian-lien](https://github.com/cian-lien) · [cmdcorp6534](https://github.com/cmdcorp6534) · [cmyyy](https://github.com/cmyyy) · [codepgq](https://github.com/codepgq) · [Colorful-glassblock](https://github.com/Colorful-glassblock) · [ComeFromTheMars](https://github.com/ComeFromTheMars) · [CrepuscularIRIS](https://github.com/CrepuscularIRIS) · [ct-jaryn](https://github.com/ct-jaryn) · [ctxyao](https://github.com/ctxyao) · [cy2311](https://github.com/cy2311) · [cyq1017](https://github.com/cyq1017) · [czf0718](https://github.com/czf0718) · [dajiaohuang](https://github.com/dajiaohuang) · [DarrellThomas](https://github.com/DarrellThomas) · [demian-welt](https://github.com/demian-welt) · [Dev-next-gen](https://github.com/Dev-next-gen) · [dfwqdyl-ui](https://github.com/dfwqdyl-ui) · [DI-HUO-MING-YI](https://github.com/DI-HUO-MING-YI) · [DieMoe233](https://github.com/DieMoe233) · [DingYong4223Line truncated
"sha256": "36c0cea36d608952bcb7439f60b9106725cc0a049bcfa62d3c0cb9c8979fb806",
"content": "# Contributors\n\nCodewhale is built in the open with a growing community of contributors. Every\nissue report and pull request is real project work — welcome at any experience\nlevel. This is the full per-PR contributor record, grouped into release/time\nbands with the newest band first so it stays scannable. Expand any band to see\neveryone.\n\nFor the live list, see the\n[GitHub contributors page](https://github.com/codewhale-hq/CodeWhale/graphs/contributors),\n[`AUTHOR_MAP`](https://github.com/codewhale-hq/CodeWhale/blob/main/.github/AUTHOR_MAP),\nand [CHANGELOG.md](../CHANGELOG.md).\n\n<!-- contributor-wall:start -->\n## Everyone\n\n[![421 people built Codewhale](assets/contributors.png)](#everyone)\n\n421 people have code, co-authored commits, carried-forward pull requests or credited reports in Codewhale. GitHub's contributor list shows only commit authors whose email is linked to their account, so it counts 203 of them. If your commits are missing there, add your commit email to your GitHub account and they will link.\n\n[0gl20shk0sbt36](https://github.com/0gl20shk0sbt36) · [1Git2Clone](https://github.com/1Git2Clone) · [20bytes](https://github.com/20bytes) · [47Cid](https://github.com/47Cid) · [7jrxt42BxFZo4iAnN4CX](https://github.com/7jrxt42BxFZo4iAnN4CX) · [8sero](https://github.com/8sero) · [A-Corner](https://github.com/A-Corner) · [aboimpinto](https://github.com/aboimpinto) · [adity982](https://github.com/adity982) · [AdityaVG13](https://github.com/AdityaVG13) · [AGSaturn](https://github.com/AGSaturn) · [ai-dengwsh](https://github.com/ai-dengwsh) · [aiapienthusiast](https://github.com/aiapienthusiast) · [Aitensa](https://github.com/Aitensa) · [AiurArtanis](https://github.com/AiurArtanis) · [alitvak69](https://github.com/alitvak69) · [all-lopezg](https://github.com/all-lopezg) · [alvin1](https://github.com/alvin1) · [amuthantamil](https://github.com/amuthantamil) · [AnaheimEX](https://github.com/AnaheimEX) · [Andrea-Bruno](https://github.com/Andrea-Bruno) · [Angel-Hair](https://github.com/Angel-Hair) · [angus-guo](https://github.com/angus-guo) · [angziii](https://github.com/angziii) · [AnonymousUser443](https://github.com/AnonymousUser443) · [antarikshraya](https://github.com/antarikshraya) · [Anyexyz](https://github.com/Anyexyz) · [Apeiron0w0](https://github.com/Apeiron0w0) · [aqilaziz](https://github.com/aqilaziz) · [AresNing](https://github.com/AresNing) · [ArronAI007](https://github.com/ArronAI007) · [Artenx](https://github.com/Artenx) · [asdfg314284230](https://github.com/asdfg314284230) · [Asteria1028](https://github.com/Asteria1028) · [asto18089](https://github.com/asto18089) · [atmosuwiryo](https://github.com/atmosuwiryo) · [axobase001](https://github.com/axobase001) · [aznikline](https://github.com/aznikline) · [baendlorel](https://github.com/baendlorel) · [banqii](https://github.com/banqii) · [Beltran12138](https://github.com/Beltran12138) · [bevis-wong](https://github.com/bevis-wong) · [BH8GCJ](https://github.com/BH8GCJ) · [BigBenLabs](https://github.com/BigBenLabs) · [Biilow](https://github.com/Biilow) · [bistack](https://github.com/bistack) · [BrathonBai](https://github.com/BrathonBai) · [bruce6135](https://github.com/bruce6135) · [BryonGo](https://github.com/BryonGo) · [buiducnhat](https://github.com/buiducnhat) · [buko](https://github.com/buko) · [BX166](https://github.com/BX166) · [c020627](https://github.com/c020627) · [cacdcaecawae](https://github.com/cacdcaecawae) · [caeserchen](https://github.com/caeserchen) · [caiyilian](https://github.com/caiyilian) · [CCChisato](https://github.com/CCChisato) · [ccomma](https://github.com/ccomma) · [cenab](https://github.com/cenab) · [ChaceLyee2101](https://github.com/ChaceLyee2101) · [chatman-media](https://github.com/chatman-media) · [chennest](https://github.com/chennest) · [chinaqy110](https://github.com/chinaqy110) · [chnjames](https://github.com/chnjames) · [chuntseevolving](https://github.com/chuntseevolving) · [ci4ic4](https://github.com/ci4ic4) · [cian-lien](https://github.com/cian-lien) · [cmdcorp6534](https://github.com/cmdcorp6534) · [cmyyy](https://github.com/cmyyy) · [codepgq](https://github.com/codepgq) · [Colorful-glassblock](https://github.com/Colorful-glassblock) · [ComeFromTheMars](https://github.com/ComeFromTheMars) · [CrepuscularIRIS](https://github.com/CrepuscularIRIS) · [ct-jaryn](https://github.com/ct-jaryn) · [ctxyao](https://github.com/ctxyao) · [cy2311](https://github.com/cy2311) · [cyq1017](https://github.com/cyq1017) · [czf0718](https://github.com/czf0718) · [dajiaohuang](https://github.com/dajiaohuang) · [DarrellThomas](https://github.com/DarrellThomas) · [demian-welt](https://github.com/demian-welt) · [Dev-next-gen](https://github.com/Dev-next-gen) · [dfwqdyl-ui](https://github.com/dfwqdyl-ui) · [DI-HUO-MING-YI](https://github.com/DI-HUO-MING-YI) · [DieMoe233](https://github.com/DieMoe233) · [DingYong4223Line truncated
},
{
"path": "docs/RELEASE_CHECKLIST.md",
@@ -4,7 +4,7 @@ import type { DocsTroubleshootingDict } from "../types";
* English reference dictionary for `app/[locale]/docs/troubleshooting/page.tsx`
* ("Fix a problem"). Error texts and fixes come from docs/INSTALL.md §13
* (every one was hit while writing that guide), docs/OPERATIONS_RUNBOOK.md,
* docs/KEYBINDINGS.md (Ctrl-B), crates/tui/src/runtime_log.rs (log path),
* docs/KEYBINDINGS.md (Ctrl-B), crates/runtime/src/runtime_log.rs (log path),
* and docs/DOCKER.md.
*/
export const docsTroubleshooting: DocsTroubleshootingDict = {