chore: remove experiments from settings tui

This commit is contained in:
Ogulcan Celik 2026-08-01 03:36:11 +03:00
parent 02fe7d7659
commit 8d190eddda
10 changed files with 10 additions and 368 deletions

View File

@ -6,6 +6,7 @@
- Added `ui.tab_bar_position = "bottom"` to place the desktop tab row below terminal panes.
### Changed
- Experimental options are no longer exposed in the Settings TUI and remain available through the config file.
- Agent status indicators now use the same static workspace marks across the sidebar, navigator, and mobile views, eliminating continuous spinner rendering while agents work.
- Relicensed Herdr from AGPL-3.0-or-later to Apache-2.0.

View File

@ -38,7 +38,7 @@ Herdr サーバーが停止して再起動すると、元のペインのプロ
ペイン画面履歴は、サーバーの完全な再起動後に直近のターミナル内容を復元します。復元されるのは Herdr が表示できるものであって、元のプロセスではありません。
ペイン出力にはシークレット、トークン、プロンプト、コマンド出力が含まれうるため、これはデフォルトで無効です。Settings > Experiments > pane screen history から、または次の設定で有効にします:
ペイン出力にはシークレット、トークン、プロンプト、コマンド出力が含まれうるため、これはデフォルトで無効です。設定ファイルで有効にします:
```toml
[experimental]

View File

@ -38,7 +38,7 @@ Snapshot restore does not preserve running shells, servers, tests, or arbitrary
Pane screen history restores recent terminal contents after a full server restart. It restores what Herdr can show, not the old process.
This is off by default because pane output can include secrets, tokens, prompts, and command output. Enable it from Settings > Experiments > pane screen history or with:
This is off by default because pane output can include secrets, tokens, prompts, and command output. Enable it in the config file:
```toml
[experimental]

View File

@ -38,7 +38,7 @@ herdr
窗格屏幕历史在服务器完全重启后恢复最近的终端内容。它恢复的是 Herdr 能展示的内容,而不是原来的进程。
它默认关闭,因为窗格输出可能包含密钥、令牌、提示词和命令输出。可以在 Settings > Experiments > pane screen history 中开启,或者:
它默认关闭,因为窗格输出可能包含密钥、令牌、提示词和命令输出。可以在配置文件中开启:
```toml
[experimental]

View File

@ -92,27 +92,6 @@ impl App {
}
}
pub(super) fn save_pane_history_persistence(&mut self, enabled: bool) {
if self.update_config_file("pane screen history", |content| {
crate::config::upsert_section_bool(content, "experimental", "pane_history", enabled)
}) {
self.apply_config_from_disk(false);
}
}
pub(super) fn save_switch_ascii_input_source_in_prefix(&mut self, enabled: bool) {
if self.update_config_file("prefix ascii input source", |content| {
crate::config::upsert_section_bool(
content,
"experimental",
"switch_ascii_input_source_in_prefix",
enabled,
)
}) {
self.apply_config_from_disk(false);
}
}
pub(super) fn save_agent_panel_sort(&mut self, sort: crate::app::state::AgentPanelSort) {
let value = match sort {
crate::app::state::AgentPanelSort::Spaces => {

View File

@ -344,12 +344,6 @@ impl App {
SettingsAction::SaveAgentBorderLabels(enabled) => {
self.save_agent_border_labels(enabled)
}
SettingsAction::SavePaneHistory(enabled) => {
self.save_pane_history_persistence(enabled)
}
SettingsAction::SaveSwitchAsciiInputSourceInPrefix(enabled) => {
self.save_switch_ascii_input_source_in_prefix(enabled)
}
SettingsAction::InstallRecommendedIntegrations => {
self.install_recommended_integrations()
}

View File

@ -3,7 +3,7 @@ use ratatui::layout::Rect;
use crate::{
app::{
state::{AppState, ExperimentSetting, SettingsSection, THEME_NAMES},
state::{AppState, SettingsSection, THEME_NAMES},
App, Mode,
},
config::ToastDelivery,
@ -17,25 +17,9 @@ pub(super) enum SettingsAction {
SaveSound(bool),
SaveToastDelivery(ToastDelivery),
SaveAgentBorderLabels(bool),
SavePaneHistory(bool),
SaveSwitchAsciiInputSourceInPrefix(bool),
InstallRecommendedIntegrations,
}
/// Map an Experiments row index to the toggle action that flips it.
fn experiment_toggle_action(state: &AppState, idx: usize) -> Option<SettingsAction> {
match ExperimentSetting::ALL.get(idx).copied()? {
ExperimentSetting::PaneHistory => Some(SettingsAction::SavePaneHistory(
!ExperimentSetting::PaneHistory.enabled(state),
)),
ExperimentSetting::SwitchAsciiInputSourceInPrefix => {
Some(SettingsAction::SaveSwitchAsciiInputSourceInPrefix(
!ExperimentSetting::SwitchAsciiInputSourceInPrefix.enabled(state),
))
}
}
}
impl App {
pub(crate) fn handle_settings_key(&mut self, key: KeyEvent) {
let previous_section = self.state.settings.section;
@ -47,12 +31,6 @@ impl App {
SettingsAction::SaveAgentBorderLabels(enabled) => {
self.save_agent_border_labels(enabled)
}
SettingsAction::SavePaneHistory(enabled) => {
self.save_pane_history_persistence(enabled)
}
SettingsAction::SaveSwitchAsciiInputSourceInPrefix(enabled) => {
self.save_switch_ascii_input_source_in_prefix(enabled)
}
SettingsAction::InstallRecommendedIntegrations => {
self.install_recommended_integrations()
}
@ -171,7 +149,7 @@ pub(super) fn update_settings_state(state: &mut AppState, key: KeyEvent) -> Opti
state.settings.list.selected = usize::from(!state.sound_enabled());
}
KeyCode::BackTab | KeyCode::Left | KeyCode::Char('h') => {
state.settings.section = SettingsSection::Experiments;
state.settings.section = SettingsSection::Integrations;
state.settings.list.selected = 0;
}
_ => match super::modal::modal_action_from_key(&key, super::modal::SETTINGS_ACTIONS) {
@ -251,30 +229,6 @@ pub(super) fn update_settings_state(state: &mut AppState, key: KeyEvent) -> Opti
}
}
},
SettingsSection::Experiments => match key.code {
KeyCode::Up | KeyCode::Char('k') => state.settings.list.move_prev(),
KeyCode::Down | KeyCode::Char('j') => {
state.settings.list.move_next(ExperimentSetting::ALL.len())
}
KeyCode::Enter | KeyCode::Char(' ') => {
return experiment_toggle_action(state, state.settings.list.selected);
}
KeyCode::BackTab | KeyCode::Left | KeyCode::Char('h') => {
state.settings.section = SettingsSection::Integrations;
state.settings.list.selected = 0;
}
KeyCode::Tab | KeyCode::Right | KeyCode::Char('l') => {
state.settings.section = SettingsSection::Theme;
state.settings.list.selected = current_theme_index(&state.theme_name);
}
_ => {
if let Some(super::modal::ModalAction::Close) =
super::modal::modal_action_from_key(&key, super::modal::SETTINGS_ACTIONS)
{
cancel_settings(state);
}
}
},
SettingsSection::Integrations => match key.code {
KeyCode::Enter | KeyCode::Char(' ') if integrations_need_install(state) => {
return Some(SettingsAction::InstallRecommendedIntegrations);
@ -284,8 +238,8 @@ pub(super) fn update_settings_state(state: &mut AppState, key: KeyEvent) -> Opti
state.settings.list.selected = usize::from(!state.agent_border_labels_enabled());
}
KeyCode::Tab | KeyCode::Right | KeyCode::Char('l') => {
state.settings.section = SettingsSection::Experiments;
state.settings.list.selected = 0;
state.settings.section = SettingsSection::Theme;
state.settings.list.selected = current_theme_index(&state.theme_name);
}
_ => match super::modal::modal_action_from_key(&key, super::modal::SETTINGS_ACTIONS) {
Some(super::modal::ModalAction::Apply) => return apply_settings(state),
@ -312,7 +266,6 @@ pub(crate) fn open_settings_at(state: &mut AppState, section: SettingsSection) {
SettingsSection::Sound => usize::from(!state.sound_enabled()),
SettingsSection::Toast => toast_delivery_index(state.toast_delivery()),
SettingsSection::PaneLabels => usize::from(!state.agent_border_labels_enabled()),
SettingsSection::Experiments => 0,
SettingsSection::Integrations => 0,
};
state.mode = Mode::Settings;
@ -407,14 +360,6 @@ impl AppState {
None
}
}
SettingsSection::Experiments => {
let list_y = area.y + 3;
if row >= list_y && row < list_y + ExperimentSetting::ALL.len() as u16 {
Some((row - list_y) as usize)
} else {
None
}
}
SettingsSection::Integrations => None,
}
}
@ -431,7 +376,6 @@ impl AppState {
SettingsSection::PaneLabels => {
usize::from(!self.agent_border_labels_enabled())
}
SettingsSection::Experiments => 0,
SettingsSection::Integrations => 0,
});
return None;
@ -455,7 +399,6 @@ impl AppState {
let enabled = idx == 0;
Some(SettingsAction::SaveAgentBorderLabels(enabled))
}
SettingsSection::Experiments => experiment_toggle_action(self, idx),
SettingsSection::Integrations => None,
};
}
@ -543,46 +486,7 @@ mod tests {
}
#[test]
fn settings_experiments_toggles_pane_history() {
let mut state = state_with_workspaces(&["test"]);
state.pane_history_persistence = false;
open_settings_at(&mut state, SettingsSection::Experiments);
let action = update_settings_state(
&mut state,
KeyEvent::new(KeyCode::Enter, KeyModifiers::empty()),
);
assert_eq!(action, Some(SettingsAction::SavePaneHistory(true)));
assert_eq!(state.mode, Mode::Settings);
}
#[test]
fn settings_experiments_down_then_toggle_switches_ascii_input_source() {
let mut state = state_with_workspaces(&["test"]);
state.switch_ascii_input_source_in_prefix = false;
open_settings_at(&mut state, SettingsSection::Experiments);
update_settings_state(
&mut state,
KeyEvent::new(KeyCode::Down, KeyModifiers::empty()),
);
assert_eq!(state.settings.list.selected, 1);
let action = update_settings_state(
&mut state,
KeyEvent::new(KeyCode::Enter, KeyModifiers::empty()),
);
assert_eq!(
action,
Some(SettingsAction::SaveSwitchAsciiInputSourceInPrefix(true))
);
assert_eq!(state.mode, Mode::Settings);
}
#[test]
fn settings_tab_cycle_places_experiments_last() {
fn settings_tab_cycle_wraps_after_integrations() {
let mut state = state_with_workspaces(&["test"]);
open_settings_at(&mut state, SettingsSection::PaneLabels);
@ -592,24 +496,12 @@ mod tests {
);
assert_eq!(state.settings.section, SettingsSection::Integrations);
update_settings_state(
&mut state,
KeyEvent::new(KeyCode::Tab, KeyModifiers::empty()),
);
assert_eq!(state.settings.section, SettingsSection::Experiments);
update_settings_state(
&mut state,
KeyEvent::new(KeyCode::Tab, KeyModifiers::empty()),
);
assert_eq!(state.settings.section, SettingsSection::Theme);
update_settings_state(
&mut state,
KeyEvent::new(KeyCode::BackTab, KeyModifiers::empty()),
);
assert_eq!(state.settings.section, SettingsSection::Experiments);
update_settings_state(
&mut state,
KeyEvent::new(KeyCode::BackTab, KeyModifiers::empty()),
@ -653,43 +545,6 @@ mod tests {
assert_eq!(app.state.settings.list.selected, 0);
}
#[test]
fn settings_mouse_click_toggles_pane_history() {
let mut app = app_for_mouse_test();
app.state.pane_history_persistence = false;
open_settings_at(&mut app.state, SettingsSection::Experiments);
let area = app.state.settings_content_rect();
let action = app.state.handle_settings_mouse(mouse(
MouseEventKind::Down(crossterm::event::MouseButton::Left),
area.x + 2,
area.y + 3,
));
assert_eq!(action, Some(SettingsAction::SavePaneHistory(true)));
assert_eq!(app.state.settings.list.selected, 0);
}
#[test]
fn settings_mouse_click_toggles_switch_ascii_input_source_row() {
let mut app = app_for_mouse_test();
app.state.switch_ascii_input_source_in_prefix = false;
open_settings_at(&mut app.state, SettingsSection::Experiments);
let area = app.state.settings_content_rect();
let action = app.state.handle_settings_mouse(mouse(
MouseEventKind::Down(crossterm::event::MouseButton::Left),
area.x + 2,
area.y + 4,
));
assert_eq!(
action,
Some(SettingsAction::SaveSwitchAsciiInputSourceInPrefix(true))
);
assert_eq!(app.state.settings.list.selected, 1);
}
#[test]
fn integration_update_badge_only_tracks_outdated_recommendations() {
let mut state = state_with_workspaces(&["test"]);

View File

@ -3326,31 +3326,6 @@ mod tests {
let _ = std::fs::remove_dir_all(path.parent().unwrap());
}
#[test]
fn settings_save_pane_history_persists_then_applies_live_config() {
let _guard = config_env_lock().lock().unwrap();
let path = temp_config_path("settings-save-pane-history");
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!(!app.persist_pane_history);
assert!(!app.state.pane_history_persistence);
app.save_pane_history_persistence(true);
assert!(app.persist_pane_history);
assert!(app.state.pane_history_persistence);
let content = std::fs::read_to_string(&path).unwrap();
assert!(content.contains("[experimental]"));
assert!(content.contains("pane_history = true"));
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();

View File

@ -994,7 +994,6 @@ pub enum SettingsSection {
Sound,
Toast,
PaneLabels,
Experiments,
Integrations,
}
@ -1005,7 +1004,6 @@ impl SettingsSection {
Self::Toast,
Self::PaneLabels,
Self::Integrations,
Self::Experiments,
];
pub fn label(self) -> &'static str {
@ -1014,40 +1012,11 @@ impl SettingsSection {
Self::Sound => "sound",
Self::Toast => "toasts",
Self::PaneLabels => "pane labels",
Self::Experiments => "experiments",
Self::Integrations => "integrations",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum ExperimentSetting {
PaneHistory,
SwitchAsciiInputSourceInPrefix,
}
impl ExperimentSetting {
pub(crate) const ALL: [Self; 2] = [Self::PaneHistory, Self::SwitchAsciiInputSourceInPrefix];
pub(crate) fn label(self) -> &'static str {
match self {
Self::PaneHistory => "pane screen history",
Self::SwitchAsciiInputSourceInPrefix => {
"switch to ascii input source in prefix (macOS/Windows)"
}
}
}
pub(crate) fn enabled(self, state: &AppState) -> bool {
match self {
Self::PaneHistory => state.pane_history_persistence_enabled(),
Self::SwitchAsciiInputSourceInPrefix => {
state.switch_ascii_input_source_in_prefix_enabled()
}
}
}
}
/// All built-in theme names in display order.
pub const THEME_NAMES: &[&str] = &[
"catppuccin",
@ -1624,14 +1593,6 @@ impl AppState {
self.show_agent_labels_on_pane_borders
}
pub fn pane_history_persistence_enabled(&self) -> bool {
self.pane_history_persistence
}
pub fn switch_ascii_input_source_in_prefix_enabled(&self) -> bool {
self.switch_ascii_input_source_in_prefix
}
pub(crate) fn pane_exposes_host_cursor(
&self,
_ws_idx: usize,

View File

@ -11,10 +11,7 @@ use super::widgets::{
render_action_button, render_modal_choice_list, render_panel_shell, ActionButtonSpec,
};
use crate::{
app::{
state::{ExperimentSetting, Palette},
AppState,
},
app::{state::Palette, AppState},
config::ToastDelivery,
};
@ -149,9 +146,6 @@ pub(super) fn render_settings_overlay(app: &AppState, frame: &mut Frame, area: R
app.settings.list.selected,
);
}
SettingsSection::Experiments => {
render_settings_experiments(app, frame, content_area);
}
SettingsSection::Integrations => {
render_settings_integrations(app, frame, content_area);
}
@ -412,120 +406,3 @@ fn render_settings_toggle(
1,
);
}
fn render_settings_experiments(app: &AppState, frame: &mut Frame, area: Rect) {
let p = &app.palette;
let [desc_area, _, list_area] = Layout::vertical([
Constraint::Length(2),
Constraint::Length(1),
Constraint::Min(1),
])
.areas::<3>(area);
super::widgets::render_modal_description(
frame,
desc_area,
"optional features that are off by default",
Style::default().fg(p.overlay1),
);
for (idx, setting) in ExperimentSetting::ALL.iter().copied().enumerate() {
let marker = if setting.enabled(app) { "[✓]" } else { "[ ]" };
let style = if app.settings.list.selected == idx {
Style::default()
.bg(p.surface0)
.fg(p.text)
.add_modifier(Modifier::BOLD)
} else {
Style::default().fg(p.subtext0)
};
let row = Rect::new(list_area.x, list_area.y + idx as u16, list_area.width, 1);
frame.render_widget(
Paragraph::new(format!(" {} {marker}", setting.label())).style(style),
row,
);
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::app::{state::SettingsSection, Mode};
use ratatui::{backend::TestBackend, Terminal};
#[test]
fn experiments_pane_history_uses_settings_checkmark_marker() {
let mut app = AppState::test_new();
app.pane_history_persistence = true;
app.settings.section = SettingsSection::Experiments;
app.settings.list.selected = 0;
app.mode = Mode::Settings;
let mut terminal =
Terminal::new(TestBackend::new(80, 24)).expect("test terminal should initialize");
terminal
.draw(|frame| render_settings_overlay(&app, frame, Rect::new(0, 0, 80, 24)))
.expect("settings overlay should render");
let rendered = terminal
.backend()
.buffer()
.content()
.iter()
.map(|cell| cell.symbol())
.collect::<String>();
assert!(rendered.contains("pane screen history [✓]"));
assert!(!rendered.contains("[x]"));
}
#[test]
fn experiments_pane_history_keeps_empty_checkbox_marker_when_disabled() {
let mut app = AppState::test_new();
app.pane_history_persistence = false;
app.settings.section = SettingsSection::Experiments;
app.settings.list.selected = 0;
app.mode = Mode::Settings;
let mut terminal =
Terminal::new(TestBackend::new(80, 24)).expect("test terminal should initialize");
terminal
.draw(|frame| render_settings_overlay(&app, frame, Rect::new(0, 0, 80, 24)))
.expect("settings overlay should render");
let rendered = terminal
.backend()
.buffer()
.content()
.iter()
.map(|cell| cell.symbol())
.collect::<String>();
assert!(rendered.contains("pane screen history [ ]"));
}
#[test]
fn experiments_renders_switch_ascii_input_source_row() {
let mut app = AppState::test_new();
app.switch_ascii_input_source_in_prefix = true;
app.settings.section = SettingsSection::Experiments;
app.settings.list.selected = 1;
app.mode = Mode::Settings;
let mut terminal =
Terminal::new(TestBackend::new(80, 24)).expect("test terminal should initialize");
terminal
.draw(|frame| render_settings_overlay(&app, frame, Rect::new(0, 0, 80, 24)))
.expect("settings overlay should render");
let rendered = terminal
.backend()
.buffer()
.content()
.iter()
.map(|cell| cell.symbol())
.collect::<String>();
assert!(rendered.contains("switch to ascii input source in prefix (macOS/Windows) [✓]"));
}
}