diff options
| author | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-05-06 22:53:56 +0300 |
|---|---|---|
| committer | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-05-06 22:53:56 +0300 |
| commit | 34df89ae655076b0493a1d8833f6790ca9d5c3ff (patch) | |
| tree | 3b2246673fbf12f4c68a2d6528932a71a9b40b85 | |
| parent | 9f94974de704646ba9b45b1cfc09cba6139e8924 (diff) | |
fix: persist agent panel scope in config
| -rw-r--r-- | CHANGELOG.md | 3 | ||||
| -rw-r--r-- | CONFIGURATION.md | 5 | ||||
| -rw-r--r-- | src/app/config_io.rs | 26 | ||||
| -rw-r--r-- | src/app/input/mod.rs | 4 | ||||
| -rw-r--r-- | src/app/input/sidebar.rs | 10 | ||||
| -rw-r--r-- | src/app/mod.rs | 71 | ||||
| -rw-r--r-- | src/app/state.rs | 4 | ||||
| -rw-r--r-- | src/config.rs | 5 | ||||
| -rw-r--r-- | src/config/model.rs | 30 | ||||
| -rw-r--r-- | src/main.rs | 3 | ||||
| -rw-r--r-- | src/persist/snapshot.rs | 7 |
11 files changed, 156 insertions, 12 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index 67a44588..8a42e32b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,9 @@ ## Unreleased +### Fixed +- Agent panel scope now defaults to `all` and is saved to config when changed, so choosing `current` or `all` survives session resets and upgrades. + ## [0.5.5] - 2026-05-06 ### Added diff --git a/CONFIGURATION.md b/CONFIGURATION.md index 6a40f71e..d1210554 100644 --- a/CONFIGURATION.md +++ b/CONFIGURATION.md @@ -39,6 +39,7 @@ Reloadable now: - keybindings and prefix - theme, custom theme colors, and legacy `ui.accent` - `ui.confirm_close` +- `ui.agent_panel_scope` - `ui.toast.delivery` - server-side `ui.sound` policy; attached thin clients refresh local sound config after a successful sound-policy change - `advanced.scrollback_limit_bytes` for panes created after reload @@ -212,6 +213,7 @@ for `panel_bg`, you can also use `reset`, `default`, `none`, or `transparent` to [ui] sidebar_width = 26 confirm_close = true +agent_panel_scope = "all" accent = "cyan" ``` @@ -221,8 +223,11 @@ accent = "cyan" |--------|---------|-------------| | `sidebar_width` | `26` | base sidebar width before auto-scaling | | `confirm_close` | `true` | ask before closing a workspace | +| `agent_panel_scope` | `all` | sidebar agent list scope: `current` or `all` | | `accent` | `cyan` | highlight and border color | +Changing the agent panel scope from the sidebar writes `agent_panel_scope` to config so it survives session resets and upgrades. + `accent` accepts: - named colors like `cyan`, `blue`, `magenta` - hex like `#89b4fa` diff --git a/src/app/config_io.rs b/src/app/config_io.rs index 6c51ad9f..a9199561 100644 --- a/src/app/config_io.rs +++ b/src/app/config_io.rs @@ -5,6 +5,11 @@ impl App { where F: FnOnce(&str) -> String, { + #[cfg(test)] + if std::env::var_os(crate::config::CONFIG_PATH_ENV_VAR).is_none() { + return false; + } + let path = crate::config::config_path(); if let Some(parent) = path.parent() { if let Err(err) = std::fs::create_dir_all(parent) { @@ -66,4 +71,25 @@ impl App { self.apply_config_from_disk(false); } } + + pub(super) fn save_agent_panel_scope(&mut self, scope: crate::app::state::AgentPanelScope) { + let value = match scope { + crate::app::state::AgentPanelScope::CurrentWorkspace => { + crate::config::AgentPanelScopeConfig::Current.as_str() + } + crate::app::state::AgentPanelScope::AllWorkspaces => { + crate::config::AgentPanelScopeConfig::All.as_str() + } + }; + if self.update_config_file("agent panel scope", |content| { + crate::config::upsert_section_value( + content, + "ui", + "agent_panel_scope", + &format!("\"{value}\""), + ) + }) { + self.apply_config_from_disk(false); + } + } } diff --git a/src/app/input/mod.rs b/src/app/input/mod.rs index 3beeac6b..56ad09a9 100644 --- a/src/app/input/mod.rs +++ b/src/app/input/mod.rs @@ -148,6 +148,7 @@ impl App { } } + let previous_agent_panel_scope = self.state.agent_panel_scope; if let Some(action) = self.state.handle_mouse(mouse) { match action { SettingsAction::SaveTheme(name) => self.save_theme(&name), @@ -155,6 +156,9 @@ impl App { SettingsAction::SaveToastDelivery(delivery) => self.save_toast_delivery(delivery), } } + if self.state.agent_panel_scope != previous_agent_panel_scope { + self.save_agent_panel_scope(self.state.agent_panel_scope); + } if let Some(content) = self.state.request_clipboard_write.take() { if self diff --git a/src/app/input/sidebar.rs b/src/app/input/sidebar.rs index fe8f14c1..8f6c7792 100644 --- a/src/app/input/sidebar.rs +++ b/src/app/input/sidebar.rs @@ -686,10 +686,16 @@ mod tests { toggle.y, )); - assert_eq!(app.state.agent_panel_scope, AgentPanelScope::AllWorkspaces); + assert_eq!( + app.state.agent_panel_scope, + AgentPanelScope::CurrentWorkspace + ); assert_eq!(app.state.agent_panel_scroll, 0); let snapshot = capture_snapshot(&app.state); - assert_eq!(snapshot.agent_panel_scope, AgentPanelScope::AllWorkspaces); + assert_eq!( + snapshot.agent_panel_scope, + AgentPanelScope::CurrentWorkspace + ); } #[test] diff --git a/src/app/mod.rs b/src/app/mod.rs index 35f502a5..ffef6600 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -115,6 +115,15 @@ fn auto_updates_enabled(no_session: bool) -> bool { !no_session && !cfg!(debug_assertions) } +fn agent_panel_scope_from_config( + scope: crate::config::AgentPanelScopeConfig, +) -> state::AgentPanelScope { + match scope { + crate::config::AgentPanelScopeConfig::Current => state::AgentPanelScope::CurrentWorkspace, + crate::config::AgentPanelScopeConfig::All => state::AgentPanelScope::AllWorkspaces, + } +} + /// Resolve the palette from config: base theme + optional custom overrides. fn resolve_palette(config: &crate::config::Config) -> state::Palette { resolve_palette_with_legacy_accent(config, true) @@ -174,7 +183,7 @@ impl App { workspaces, active, selected, - agent_panel_scope, + _restored_agent_panel_scope, sidebar_width, sidebar_width_source, sidebar_section_split, @@ -243,6 +252,8 @@ impl App { ) }; + let agent_panel_scope = agent_panel_scope_from_config(config.ui.agent_panel_scope); + info!( pane_scrollback_limit_bytes = config.advanced.scrollback_limit_bytes, "using pane scrollback configuration" @@ -635,6 +646,9 @@ impl App { self.state.sidebar_width = config.ui.sidebar_width; } self.state.confirm_close = config.ui.confirm_close; + self.state.agent_panel_scope = + agent_panel_scope_from_config(config.ui.agent_panel_scope); + self.state.agent_panel_scroll = 0; self.state.accent = crate::config::parse_color(&config.ui.accent); if !self.state.local_sound_playback && self.state.sound != config.ui.sound { self.state.request_client_sound_config_reload = true; @@ -892,6 +906,27 @@ mod tests { } #[test] + fn startup_uses_configured_agent_panel_scope() { + let mut config = Config::default(); + config.ui.agent_panel_scope = crate::config::AgentPanelScopeConfig::Current; + let (_api_tx, api_rx) = tokio::sync::mpsc::unbounded_channel(); + + let app = App::new( + &config, + true, + None, + None, + api_rx, + crate::api::EventHub::default(), + ); + + assert_eq!( + app.state.agent_panel_scope, + state::AgentPanelScope::CurrentWorkspace + ); + } + + #[test] fn startup_restores_preview_update_available_from_saved_notes() { let _guard = config_env_lock().lock().unwrap(); let path = temp_config_path("startup-preview-update-available"); @@ -933,7 +968,7 @@ mod tests { std::fs::create_dir_all(path.parent().unwrap()).unwrap(); std::fs::write( &path, - "[keys]\nnew_workspace = \"g\"\nprefix = \"ctrl+a\"\n[ui.toast]\ndelivery = \"herdr\"\n", + "[keys]\nnew_workspace = \"g\"\nprefix = \"ctrl+a\"\n[ui]\nagent_panel_scope = \"current\"\n[ui.toast]\ndelivery = \"herdr\"\n", ) .unwrap(); std::env::set_var(crate::config::CONFIG_PATH_ENV_VAR, &path); @@ -952,6 +987,10 @@ mod tests { app.state.toast_config.delivery, crate::config::ToastDelivery::Herdr ); + assert_eq!( + app.state.agent_panel_scope, + state::AgentPanelScope::CurrentWorkspace + ); assert!(app.state.config_diagnostic.is_none()); let toast = app.state.toast.as_ref().unwrap(); assert_eq!(toast.kind, crate::app::state::ToastKind::UpdateInstalled); @@ -1097,6 +1136,34 @@ mod tests { } #[test] + fn save_agent_panel_scope_persists_then_applies_live_config() { + let _guard = config_env_lock().lock().unwrap(); + let path = temp_config_path("save-agent-panel-scope"); + std::fs::create_dir_all(path.parent().unwrap()).unwrap(); + std::fs::write(&path, "onboarding = false\n").unwrap(); + std::env::set_var(crate::config::CONFIG_PATH_ENV_VAR, &path); + + let mut app = test_app(); + assert_eq!( + app.state.agent_panel_scope, + state::AgentPanelScope::AllWorkspaces + ); + + app.save_agent_panel_scope(state::AgentPanelScope::CurrentWorkspace); + + assert_eq!( + app.state.agent_panel_scope, + state::AgentPanelScope::CurrentWorkspace + ); + let content = std::fs::read_to_string(&path).unwrap(); + assert!(content.contains("agent_panel_scope = \"current\"")); + assert!(app.state.config_diagnostic.is_none()); + + std::env::remove_var(crate::config::CONFIG_PATH_ENV_VAR); + let _ = std::fs::remove_dir_all(path.parent().unwrap()); + } + + #[test] fn reload_config_keeps_current_state_on_invalid_toml() { let _guard = config_env_lock().lock().unwrap(); let path = temp_config_path("reload-config-invalid-toml"); diff --git a/src/app/state.rs b/src/app/state.rs index 97f87f3c..95caa3fe 100644 --- a/src/app/state.rs +++ b/src/app/state.rs @@ -398,7 +398,7 @@ pub enum AgentPanelScope { impl Default for AgentPanelScope { fn default() -> Self { - Self::CurrentWorkspace + Self::AllWorkspaces } } @@ -815,7 +815,7 @@ impl AppState { sidebar_width_auto: false, sidebar_collapsed: false, sidebar_section_split: 0.5, - agent_panel_scope: AgentPanelScope::CurrentWorkspace, + agent_panel_scope: AgentPanelScope::AllWorkspaces, confirm_close: true, pane_scrollback_limit_bytes: crate::config::DEFAULT_SCROLLBACK_LIMIT_BYTES, accent: Color::Cyan, diff --git a/src/config.rs b/src/config.rs index e96191bc..9343321e 100644 --- a/src/config.rs +++ b/src/config.rs @@ -15,7 +15,10 @@ pub use self::{ format_key_combo, CommandKeybindConfig, CustomCommandAction, CustomCommandKeybind, Keybinds, LiveKeybindConfig, }, - model::{Config, ConfigReloadReport, ConfigReloadStatus, ToastConfig, ToastDelivery}, + model::{ + AgentPanelScopeConfig, Config, ConfigReloadReport, ConfigReloadStatus, ToastConfig, + ToastDelivery, + }, sound::SoundConfig, theme::{parse_color, CustomThemeColors, ThemeConfig}, }; diff --git a/src/config/model.rs b/src/config/model.rs index 5a17bea7..7e6c7382 100644 --- a/src/config/model.rs +++ b/src/config/model.rs @@ -11,6 +11,23 @@ pub enum ToastDelivery { Terminal, } +#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize, Default)] +#[serde(rename_all = "lowercase")] +pub enum AgentPanelScopeConfig { + Current, + #[default] + All, +} + +impl AgentPanelScopeConfig { + pub fn as_str(self) -> &'static str { + match self { + Self::Current => "current", + Self::All => "all", + } + } +} + #[derive(Debug, Clone)] pub struct ToastConfig { pub delivery: ToastDelivery, @@ -106,6 +123,8 @@ pub struct UiConfig { pub sidebar_width: u16, /// Ask for confirmation before closing a workspace. Default: true. pub confirm_close: bool, + /// Agent sidebar scope. Saved values are "current" or "all". Default: "all". + pub agent_panel_scope: AgentPanelScopeConfig, /// Accent color for highlights, borders, and navigation UI. /// Accepts hex (#89b4fa), named colors (cyan, blue), or RGB (rgb(137,180,250)). pub accent: String, @@ -161,6 +180,7 @@ impl Default for UiConfig { Self { sidebar_width: 26, confirm_close: true, + agent_panel_scope: AgentPanelScopeConfig::All, accent: "cyan".into(), toast: ToastConfig::default(), sound: SoundConfig::default(), @@ -212,6 +232,16 @@ mod tests { use super::*; #[test] + fn agent_panel_scope_config_parses() { + let toml = r#" +[ui] +agent_panel_scope = "all" +"#; + let config: Config = toml::from_str(toml).unwrap(); + assert_eq!(config.ui.agent_panel_scope, AgentPanelScopeConfig::All); + } + + #[test] fn toast_config_parses() { let toml = r#" [ui.toast] diff --git a/src/main.rs b/src/main.rs index df16b951..45ac3db1 100644 --- a/src/main.rs +++ b/src/main.rs @@ -109,6 +109,9 @@ const DEFAULT_CONFIG: &str = r##"# herdr configuration # Ask for confirmation before closing a workspace # confirm_close = true +# Agent panel scope: "current" or "all". Toggling it in the sidebar saves this setting. +# agent_panel_scope = "all" + # Accent color for highlights, borders, and navigation UI. # Accepts: hex (#89b4fa), named colors (cyan, blue, magenta), or rgb(r,g,b) # accent = "cyan" diff --git a/src/persist/snapshot.rs b/src/persist/snapshot.rs index ff224122..2bcd034e 100644 --- a/src/persist/snapshot.rs +++ b/src/persist/snapshot.rs @@ -449,7 +449,7 @@ mod tests { assert_eq!(snap.workspaces.len(), 2); assert_eq!(snap.active, Some(0)); assert_eq!(snap.selected, 0); - assert_eq!(snap.agent_panel_scope, AgentPanelScope::CurrentWorkspace); + assert_eq!(snap.agent_panel_scope, AgentPanelScope::AllWorkspaces); assert_eq!(snap.sidebar_width, None); assert_eq!(snap.sidebar_section_split, None); assert_eq!(snap.workspaces[0].tabs.len(), 2); @@ -483,10 +483,7 @@ mod tests { let restored = parse_snapshot(&json).unwrap(); - assert_eq!( - restored.agent_panel_scope, - AgentPanelScope::CurrentWorkspace - ); + assert_eq!(restored.agent_panel_scope, AgentPanelScope::AllWorkspaces); assert_eq!(restored.sidebar_width, None); assert_eq!(restored.sidebar_section_split, None); } |
