From c73f037265a1aa04c600c23df45c43ebde4b142e Mon Sep 17 00:00:00 2001 From: ProofOfConcept Date: Thu, 9 Apr 2026 00:51:10 -0400 Subject: [PATCH] Spacebar toggle for all agents, persist to config, scan agent directory - Scan agents directory for all .agent files instead of hardcoded list - Persist enabled state to ~/.consciousness/agent-enabled.json - Spacebar on F3 agent list toggles selected agent on/off - Both subconscious and unconscious agents support toggle - Disabled agents shown dimmed with "off" indicator - New agents default to disabled (safe default) Co-Authored-By: Proof of Concept --- src/mind/mod.rs | 2 +- src/mind/subconscious.rs | 7 ++++ src/mind/unconscious.rs | 74 ++++++++++++++++++++++++++-------------- src/subconscious/defs.rs | 2 +- src/user/mod.rs | 15 ++++++++ src/user/subconscious.rs | 26 +++++++++++++- 6 files changed, 98 insertions(+), 28 deletions(-) diff --git a/src/mind/mod.rs b/src/mind/mod.rs index 765190f..49be124 100644 --- a/src/mind/mod.rs +++ b/src/mind/mod.rs @@ -258,7 +258,7 @@ pub struct Mind { pub agent: Arc, pub shared: Arc, pub config: SessionConfig, - subconscious: Arc>, + pub subconscious: Arc>, pub unconscious: Arc>, turn_tx: mpsc::Sender<(Result, StreamTarget)>, turn_watch: tokio::sync::watch::Sender, diff --git a/src/mind/subconscious.rs b/src/mind/subconscious.rs index f800210..0528c86 100644 --- a/src/mind/subconscious.rs +++ b/src/mind/subconscious.rs @@ -439,6 +439,13 @@ impl Subconscious { } } + /// Toggle an agent on/off by name. Returns new enabled state. + pub fn toggle(&mut self, name: &str) -> Option { + let agent = self.agents.iter_mut().find(|a| a.name == name)?; + agent.auto.enabled = !agent.auto.enabled; + Some(agent.auto.enabled) + } + pub fn walked(&self) -> Vec { self.state.get("walked") .map(|s| s.lines().map(|l| l.trim().to_string()).filter(|l| !l.is_empty()).collect()) diff --git a/src/mind/unconscious.rs b/src/mind/unconscious.rs index adfe72b..82e8128 100644 --- a/src/mind/unconscious.rs +++ b/src/mind/unconscious.rs @@ -2,26 +2,36 @@ // // Standalone agents that operate on the memory graph without needing // conversation context. Each agent runs in a loop: finish one run, -// wait a cooldown, start the next. Agents can be toggled on/off. +// wait a cooldown, start the next. Agents can be toggled on/off, +// persisted to ~/.consciousness/agent-enabled.json. use std::time::{Duration, Instant}; +use std::collections::HashMap; use crate::agent::oneshot::{AutoAgent, AutoStep}; use crate::agent::tools; use crate::subconscious::defs; -/// Agent types to run. Each must have a matching .agent file. -const AGENTS: &[&str] = &[ - "organize", - "linker", - "distill", - "split", - "separator", -]; - /// Cooldown between consecutive runs of the same agent. const COOLDOWN: Duration = Duration::from_secs(120); +fn config_path() -> std::path::PathBuf { + dirs::home_dir().unwrap_or_default() + .join(".consciousness/agent-enabled.json") +} + +fn load_enabled_config() -> HashMap { + std::fs::read_to_string(config_path()).ok() + .and_then(|s| serde_json::from_str(&s).ok()) + .unwrap_or_default() +} + +fn save_enabled_config(map: &HashMap) { + if let Ok(json) = serde_json::to_string_pretty(map) { + let _ = std::fs::write(config_path(), json); + } +} + struct UnconsciousAgent { name: String, enabled: bool, @@ -31,16 +41,6 @@ struct UnconsciousAgent { } impl UnconsciousAgent { - fn new(name: &str) -> Self { - Self { - name: name.to_string(), - enabled: true, - handle: None, - last_run: None, - runs: 0, - } - } - fn is_running(&self) -> bool { self.handle.as_ref().is_some_and(|h| !h.is_finished()) } @@ -73,10 +73,25 @@ pub struct Unconscious { impl Unconscious { pub fn new() -> Self { - let agents = AGENTS.iter() - .filter(|name| defs::get_def(name).is_some()) - .map(|name| UnconsciousAgent::new(name)) - .collect(); + let enabled_map = load_enabled_config(); + + // Scan all .agent files, exclude subconscious-* and surface-observe + let mut agents: Vec = Vec::new(); + for def in defs::load_defs() { + if def.agent.starts_with("subconscious-") { continue; } + if def.agent == "surface-observe" { continue; } + let enabled = enabled_map.get(&def.agent).copied() + .unwrap_or(false); // new agents default to off + agents.push(UnconsciousAgent { + name: def.agent.clone(), + enabled, + handle: None, + last_run: None, + runs: 0, + }); + } + agents.sort_by(|a, b| a.name.cmp(&b.name)); + let mut s = Self { agents, max_concurrent: 2, graph_health: None, @@ -90,7 +105,16 @@ impl Unconscious { pub fn toggle(&mut self, name: &str) -> Option { let agent = self.agents.iter_mut().find(|a| a.name == name)?; agent.enabled = !agent.enabled; - Some(agent.enabled) + let new_state = agent.enabled; + self.save_enabled(); + Some(new_state) + } + + fn save_enabled(&self) { + let map: HashMap = self.agents.iter() + .map(|a| (a.name.clone(), a.enabled)) + .collect(); + save_enabled_config(&map); } pub fn snapshots(&self) -> Vec { diff --git a/src/subconscious/defs.rs b/src/subconscious/defs.rs index 9d2b136..7039c6a 100644 --- a/src/subconscious/defs.rs +++ b/src/subconscious/defs.rs @@ -163,7 +163,7 @@ pub fn agents_dir() -> PathBuf { } /// Load all agent definitions. -fn load_defs() -> Vec { +pub fn load_defs() -> Vec { let dir = agents_dir(); let Ok(entries) = std::fs::read_dir(&dir) else { return Vec::new() }; diff --git a/src/user/mod.rs b/src/user/mod.rs index 1261bd2..04a690a 100644 --- a/src/user/mod.rs +++ b/src/user/mod.rs @@ -109,6 +109,8 @@ struct App { agent_state: Vec, unconscious_state: Vec, graph_health: Option, + /// Agent toggle requests from UI — consumed by mind loop. + pub agent_toggles: Vec, walked_count: usize, channel_status: Vec, idle_info: Option, @@ -134,6 +136,7 @@ impl App { agent_state: Vec::new(), unconscious_state: Vec::new(), graph_health: None, + agent_toggles: Vec::new(), walked_count: 0, channel_status: Vec::new(), idle_info: None, } @@ -374,6 +377,18 @@ async fn run( idle_state.decay_ewma(); app.update_idle(&idle_state); app.agent_state = mind.subconscious_snapshots().await; + { + let toggles: Vec = app.agent_toggles.drain(..).collect(); + if !toggles.is_empty() { + let mut sub = mind.subconscious.lock().await; + let mut unc = mind.unconscious.lock().await; + for name in &toggles { + if sub.toggle(name).is_none() { + unc.toggle(name); + } + } + } + } { let unc = mind.unconscious.lock().await; app.unconscious_state = unc.snapshots(); diff --git a/src/user/subconscious.rs b/src/user/subconscious.rs index e81ecf0..59ac237 100644 --- a/src/user/subconscious.rs +++ b/src/user/subconscious.rs @@ -79,6 +79,11 @@ impl ScreenView for SubconsciousScreen { self.list_state.select_next(); self.reset_pane_state(); } + KeyCode::Char(' ') => { + if let Some(name) = self.selected_agent_name(app) { + app.agent_toggles.push(name); + } + } _ => {} } Pane::Outputs => self.output_tree.handle_nav(code, &output_sections, area.height), @@ -124,6 +129,20 @@ impl ScreenView for SubconsciousScreen { } impl SubconsciousScreen { + /// Map the selected list index to an agent name. + /// Accounts for the separator line between subconscious and unconscious. + fn selected_agent_name(&self, app: &App) -> Option { + let idx = self.selected(); + let sub_count = app.agent_state.len(); + if idx < sub_count { + // Subconscious agent + return Some(app.agent_state[idx].name.clone()); + } + // Skip separator line + let unc_idx = idx.checked_sub(sub_count + 1)?; + app.unconscious_state.get(unc_idx).map(|s| s.name.clone()) + } + fn reset_pane_state(&mut self) { self.output_tree = SectionTree::new(); self.context_tree = SectionTree::new(); @@ -165,7 +184,12 @@ impl SubconsciousScreen { fn draw_list(&mut self, frame: &mut Frame, area: Rect, app: &App) { let mut items: Vec = app.agent_state.iter().map(|snap| { - if snap.running { + if !snap.enabled { + ListItem::from(Line::from(vec![ + Span::styled(&snap.name, Style::default().fg(Color::DarkGray)), + Span::styled(" ○ off", Style::default().fg(Color::DarkGray)), + ])) + } else if snap.running { ListItem::from(Line::from(vec![ Span::styled(&snap.name, Style::default().fg(Color::Green)), Span::styled(" ● ", Style::default().fg(Color::Green)),