aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorOgulcan Celik <ogulcancelik@gmail.com>2026-05-06 22:53:56 +0300
committerOgulcan Celik <ogulcancelik@gmail.com>2026-05-06 22:53:56 +0300
commit34df89ae655076b0493a1d8833f6790ca9d5c3ff (patch)
tree3b2246673fbf12f4c68a2d6528932a71a9b40b85
parent9f94974de704646ba9b45b1cfc09cba6139e8924 (diff)
fix: persist agent panel scope in config
-rw-r--r--CHANGELOG.md3
-rw-r--r--CONFIGURATION.md5
-rw-r--r--src/app/config_io.rs26
-rw-r--r--src/app/input/mod.rs4
-rw-r--r--src/app/input/sidebar.rs10
-rw-r--r--src/app/mod.rs71
-rw-r--r--src/app/state.rs4
-rw-r--r--src/config.rs5
-rw-r--r--src/config/model.rs30
-rw-r--r--src/main.rs3
-rw-r--r--src/persist/snapshot.rs7
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);
}