//! Individual setting setters with persistence effects and toasts. use super::ui::{refresh_open_settings_modals, save_success_toast}; use crate::app::actions::Effect; use crate::app::app_view::{ActiveView, AppView}; use agent_client_protocol as acp; /// Set multiline input mode — swap Enter and Shift+Enter behavior. /// /// PAGER-OWNED: ephemeral, no `Effect::PersistSetting`. On the agent /// view this is per-session (`AgentView::multiline_mode`); on the /// dashboard it lives on `DashboardState::multiline_mode`. Idempotent. pub(in crate::app::dispatch) fn set_multiline_mode(app: &mut AppView, new: bool) -> Vec { if matches!(app.active_view, ActiveView::AgentDashboard) { let Some(d) = app.dashboard.as_mut() else { return vec![]; }; if d.multiline_mode == new { return vec![]; } d.multiline_mode = new; tracing::info!( target: "settings", key = "multiline_mode", value = new, surface = "dashboard", "setting changed", ); app.show_toast(&save_success_toast("Multiline", new)); return vec![]; } let ActiveView::Agent(id) = app.active_view else { return vec![]; }; let Some(agent) = app.agents.get_mut(&id) else { return vec![]; }; if agent.multiline_mode == new { return vec![]; } agent.multiline_mode = new; refresh_open_settings_modals(app); tracing::info!( target: "settings", key = "multiline_mode", value = new, "setting changed", ); app.show_toast(&save_success_toast("Multiline", new)); vec![] } /// State-only mutation for `render_mermaid`: update the process-wide cache so /// every block picks up the new value on the next frame. The cache is the /// render-path source of truth; the disk write goes through `PersistSetting`. pub(super) fn set_render_mermaid_inner(kind: crate::appearance::RenderMermaid) { crate::appearance::cache::set_render_mermaid(kind); } /// Set how ` ```mermaid ` code blocks render (auto/on/off). /// /// SHELL-OWNED: persisted to `[ui].render_mermaid` via `Effect::PersistSetting` /// (parity with `vim_mode`), with the cache mirror updated optimistically. pub(in crate::app::dispatch) fn set_render_mermaid( app: &mut AppView, kind: crate::appearance::RenderMermaid, ) -> Vec { let prev = crate::appearance::cache::load_render_mermaid(); if prev == kind { return vec![]; } set_render_mermaid_inner(kind); refresh_open_settings_modals(app); tracing::info!( target: "settings", key = "render_mermaid", value = kind.as_canonical(), "setting changed", ); app.show_toast(&format!("\u{2713} Mermaid: {}", kind.as_canonical())); vec![Effect::PersistSetting { key: "render_mermaid", value: crate::settings::SettingValue::Enum(kind.as_canonical()), rollback_value: crate::settings::SettingValue::Enum(prev.as_canonical()), }] } /// Mirror the canonical mode into `app.current_ui` so `current_value_for` stays /// in sync. Called by the commit path AND by [`apply_setting_rollback`](super::ui::apply_setting_rollback). pub(super) fn set_hunk_tracker_mode_inner(app: &mut AppView, canonical: &str) { app.current_ui.hunk_tracker_mode = Some(canonical.to_string()); } pub(super) fn set_screen_mode_inner(app: &mut AppView, canonical: &str) { app.current_ui.screen_mode = Some(canonical.to_string()); } /// Persist `[ui].screen_mode` (`fullscreen` | `minimal`). Restart-required. /// /// Unset is *displayed* as Fullscreen but is not an explicit on-disk value — /// choosing Fullscreen when missing must still write, or legacy pager.toml / /// leaky-terminal paths can keep applying after the user confirmed Fullscreen. pub(in crate::app::dispatch) fn set_screen_mode(app: &mut AppView, value: String) -> Vec { let canonical = crate::settings::canonical_screen_mode(Some(&value)); let prev_raw = app.current_ui.screen_mode.as_deref(); let prev = crate::settings::canonical_screen_mode(prev_raw); if screen_mode_raw_matches_canonical(prev_raw, canonical) { return vec![]; } set_screen_mode_inner(app, canonical); refresh_open_settings_modals(app); tracing::info!(target: "settings", key = "screen_mode", value = canonical, "setting changed"); app.show_toast(&format!( "\u{2713} Screen mode: {canonical} (restart to apply)" )); vec![Effect::PersistSetting { key: "screen_mode", value: crate::settings::SettingValue::Enum(canonical), rollback_value: crate::settings::SettingValue::Enum(prev), }] } fn screen_mode_raw_matches_canonical(raw: Option<&str>, canonical: &str) -> bool { let Some(raw) = raw.map(str::trim).filter(|s| !s.is_empty()) else { return false; }; match canonical { "minimal" => raw.eq_ignore_ascii_case("minimal"), "fullscreen" => raw.eq_ignore_ascii_case("fullscreen") || raw.eq_ignore_ascii_case("full"), _ => false, } } /// Set the hunk-tracker mode (registry-driven path). /// /// SHELL-owned, restart-required: persists to `[ui].hunk_tracker_mode` via /// `Effect::PersistSetting`. The agent re-reads the mode only on the next /// session connect, so the toast cues a restart. pub(in crate::app::dispatch) fn set_hunk_tracker_mode( app: &mut AppView, value: String, ) -> Vec { let canonical = crate::settings::canonical_hunk_tracker_mode(Some(&value)); let prev = crate::settings::canonical_hunk_tracker_mode(app.current_ui.hunk_tracker_mode.as_deref()); if prev == canonical { return vec![]; } set_hunk_tracker_mode_inner(app, canonical); refresh_open_settings_modals(app); tracing::info!(target: "settings", key = "hunk_tracker_mode", value = canonical, "setting changed"); app.show_toast(&format!( "\u{2713} Hunk tracker: {canonical} (restart to apply)" )); vec![Effect::PersistSetting { key: "hunk_tracker_mode", value: crate::settings::SettingValue::Enum(canonical), rollback_value: crate::settings::SettingValue::Enum(prev), }] } /// State-only mutation for `vim_mode`. Propagates to every in-process /// agent so background subagents and side panes pick up the change /// without restart. The cache mirror lets new agents created later /// read the same value via `cache::load_vim_mode()` in `AgentView::new`. pub(super) fn set_vim_mode_inner(app: &mut AppView, new: bool) { for agent in app.agents.values_mut() { // Recursive so open subagent views also pick up the change — // otherwise their scrollback j/k stay in the vim-OFF fallback. agent.set_vim_mode_recursive(new); } crate::appearance::cache::set_vim_mode(new); } /// Set vim-mode scrollback keybindings (registry-driven path). /// /// SHELL-OWNED: persisted to `[ui].vim_mode` in config.toml via /// `Effect::PersistSetting`. Propagates to every in-process agent. pub(in crate::app::dispatch) fn set_vim_mode(app: &mut AppView, new: bool) -> Vec { let prev = crate::appearance::cache::load_vim_mode(); if prev == new && app.agents.values().all(|a| a.vim_mode == new) { return vec![]; } set_vim_mode_inner(app, new); refresh_open_settings_modals(app); tracing::info!( target: "settings", key = "vim_mode", value = new, "setting changed", ); app.show_toast(&save_success_toast("Vim scrollback", new)); vec![Effect::PersistSetting { key: "vim_mode", value: crate::settings::SettingValue::Bool(new), rollback_value: crate::settings::SettingValue::Bool(prev), }] } /// Mirror the user-config layer in `current_ui` so the modal reflects it /// (the effective gate is resolved shell-side at session spawn). pub(super) fn set_remember_tool_approvals_inner(app: &mut AppView, new: bool) { app.current_ui.remember_tool_approvals = Some(new); } /// SHELL-owned setter for `remember_tool_approvals`; persists via /// `Effect::PersistSetting`. Applies to new sessions (restart-required). pub(in crate::app::dispatch) fn set_remember_tool_approvals( app: &mut AppView, new: bool, ) -> Vec { let prev_state = app.current_ui.remember_tool_approvals; let prev_effective = prev_state.unwrap_or(false); if prev_effective == new && prev_state.is_some() { return vec![]; } set_remember_tool_approvals_inner(app, new); refresh_open_settings_modals(app); tracing::info!( target: "settings", key = "remember_tool_approvals", value = new, "setting changed", ); app.show_toast(&format!( "{} (restart to apply)", save_success_toast("Remember tool approvals", new), )); vec![Effect::PersistSetting { key: "remember_tool_approvals", value: crate::settings::SettingValue::Bool(new), rollback_value: crate::settings::SettingValue::Bool(prev_effective), }] } /// Mirror the just-written TOML value in `app` so the modal reflects it (the /// effective timeout is re-resolved shell-side at agent build). pub(super) fn set_ask_user_question_timeout_enabled_inner(app: &mut AppView, new: bool) { app.ask_user_question_timeout_enabled = Some(new); } /// SHELL-owned setter for the ask_user_question timeout gate; persists via /// `Effect::PersistSetting`. Applies to new sessions (restart-required). pub(in crate::app::dispatch) fn set_ask_user_question_timeout_enabled( app: &mut AppView, new: bool, ) -> Vec { use kigi_tools::implementations::kigi::ask_user_question; let prev_state = app.ask_user_question_timeout_enabled; let prev_effective = prev_state.unwrap_or(ask_user_question::DEFAULT_ASK_USER_QUESTION_TIMEOUT_ENABLED); if prev_effective == new && prev_state.is_some() { return vec![]; } set_ask_user_question_timeout_enabled_inner(app, new); refresh_open_settings_modals(app); tracing::info!( target: "settings", key = "toolset.ask_user_question.timeout_enabled", value = new, "setting changed", ); app.show_toast(&format!( "{} (restart to apply)", save_success_toast("Ask-Question timeout", new), )); vec![Effect::PersistSetting { key: "toolset.ask_user_question.timeout_enabled", value: crate::settings::SettingValue::Bool(new), rollback_value: crate::settings::SettingValue::Bool(prev_effective), }] } pub(super) fn set_show_thinking_blocks_inner(app: &mut AppView, new: bool) { crate::appearance::cache::set_show_thinking_blocks(new); // Thinking visibility reshapes verb-group runs (shown thoughts claim // into folds) AND dense N-more runs (hidden thoughts drop out of // truncation participation), so expansion ids describe the OLD grouping // shape even with `group_tool_verbs` off — drop them like // `set_group_tool_verbs_inner`. for agent in app.agents.values_mut() { agent.scrollback.clear_group_expansion(); agent.scrollback.invalidate_heights(); for child in agent.subagent_views.values_mut() { child.scrollback.clear_group_expansion(); child.scrollback.invalidate_heights(); } } } /// Set whether agent thinking blocks appear in scrollback. /// /// SHELL-OWNED: cache mirror + `[ui].show_thinking_blocks` via `Effect::PersistSetting`. /// Live hide/show: layout treats thinking as zero-height when off; create-gate /// still skips new thinking while off. pub(in crate::app::dispatch) fn set_show_thinking_blocks( app: &mut AppView, new: bool, ) -> Vec { let prev = crate::appearance::cache::load_show_thinking_blocks(); if prev == new { return vec![]; } set_show_thinking_blocks_inner(app, new); refresh_open_settings_modals(app); tracing::info!( target: "settings", key = "show_thinking_blocks", value = new, "setting changed", ); app.show_toast(&save_success_toast("Thinking blocks", new)); vec![Effect::PersistSetting { key: "show_thinking_blocks", value: crate::settings::SettingValue::Bool(new), rollback_value: crate::settings::SettingValue::Bool(prev), }] } pub(super) fn set_group_tool_verbs_inner(app: &mut AppView, new: bool) { crate::appearance::cache::set_group_tool_verbs(new); // Expansion ids describe the OLD grouping shape; drop them so stale ids // can't reopen a verb slot expanded or mark a coincident dense group // expanded after the re-fold (see `clear_group_expansion`). for agent in app.agents.values_mut() { agent.scrollback.clear_group_expansion(); agent.scrollback.invalidate_heights(); for child in agent.subagent_views.values_mut() { child.scrollback.clear_group_expansion(); child.scrollback.invalidate_heights(); } } } /// Set whether runs of consecutive non-destructive tool calls and subagent /// rows are grouped into one transcript row. /// /// SHELL-OWNED: cache mirror + `[ui].group_tool_verbs` via `Effect::PersistSetting`. pub(in crate::app::dispatch) fn set_group_tool_verbs(app: &mut AppView, new: bool) -> Vec { let prev = crate::appearance::cache::load_group_tool_verbs(); if prev == new { return vec![]; } set_group_tool_verbs_inner(app, new); refresh_open_settings_modals(app); tracing::info!( target: "settings", key = "group_tool_verbs", value = new, "setting changed", ); app.show_toast(&save_success_toast("Group tool calls", new)); vec![Effect::PersistSetting { key: "group_tool_verbs", value: crate::settings::SettingValue::Bool(new), rollback_value: crate::settings::SettingValue::Bool(prev), }] } pub(super) fn set_collapsed_edit_blocks_inner(app: &mut AppView, new: bool) { // Read prev from the cache so a rollback restore walks the same flip. let prev = crate::appearance::cache::load_collapsed_edit_blocks(); crate::appearance::cache::set_collapsed_edit_blocks(new); if prev == new { return; } // Re-materialize on-default Edit rows + repaint the live +N/-M suffix // (the flip policy lives on ScrollbackState). for agent in app.agents.values_mut() { agent.scrollback.apply_collapsed_edit_blocks_flip(prev, new); for child in agent.subagent_views.values_mut() { child.scrollback.apply_collapsed_edit_blocks_flip(prev, new); } } } /// Set whether Edit blocks default to the collapsed one-line `+N/-M` /// diffstat summary (expand for the diff). /// /// SHELL-OWNED: cache mirror + `[ui].collapsed_edit_blocks` via /// `Effect::PersistSetting`. Explicit pager.toml /// `[scrollback.blocks.edit]` shape keys override the flag. pub(in crate::app::dispatch) fn set_collapsed_edit_blocks( app: &mut AppView, new: bool, ) -> Vec { let prev = crate::appearance::cache::load_collapsed_edit_blocks(); if prev == new { return vec![]; } set_collapsed_edit_blocks_inner(app, new); refresh_open_settings_modals(app); tracing::info!( target: "settings", key = "collapsed_edit_blocks", value = new, "setting changed", ); app.show_toast(&save_success_toast("Collapsed edit blocks", new)); vec![Effect::PersistSetting { key: "collapsed_edit_blocks", value: crate::settings::SettingValue::Bool(new), rollback_value: crate::settings::SettingValue::Bool(prev), }] } pub(super) fn set_prompt_suggestions_inner(app: &mut AppView, new: bool) { crate::appearance::cache::set_prompt_suggestions(new); app.current_ui.prompt_suggestions = Some(new); } /// Set whether the predicted-next-prompt ghost (tab autocomplete) is offered. /// /// SHELL-OWNED: cache mirror + `[ui].prompt_suggestions` via /// `Effect::PersistSetting`. Read at turn end (fetch gate) and per frame /// (display gate), so toggling applies without a restart. The /// `KIGI_PROMPT_SUGGESTIONS` env var overrides the effective value. pub(in crate::app::dispatch) fn set_prompt_suggestions( app: &mut AppView, new: bool, ) -> Vec { let prev = crate::appearance::cache::load_prompt_suggestions(); if prev == new { return vec![]; } set_prompt_suggestions_inner(app, new); refresh_open_settings_modals(app); tracing::info!( target: "settings", key = "prompt_suggestions", value = new, "setting changed", ); app.show_toast(&save_success_toast("Prompt suggestions", new)); vec![Effect::PersistSetting { key: "prompt_suggestions", value: crate::settings::SettingValue::Bool(new), rollback_value: crate::settings::SettingValue::Bool(prev), }] } pub(super) fn set_keep_text_selection_inner(kind: crate::appearance::TextSelection) { crate::appearance::cache::set_keep_text_selection(kind); } /// Set the unified scrollback text-selection mode (`flash` | `hold` | /// `word_select`): highlight lifetime + double-click action, kept in sync. /// /// SHELL-OWNED: persisted to `[ui].keep_text_selection` via `Effect::PersistSetting`. pub(in crate::app::dispatch) fn set_keep_text_selection( app: &mut AppView, kind: crate::appearance::TextSelection, ) -> Vec { let prev = crate::appearance::cache::load_keep_text_selection(); if prev == kind { return vec![]; } set_keep_text_selection_inner(kind); refresh_open_settings_modals(app); tracing::info!( target: "settings", key = "keep_text_selection", value = kind.as_canonical(), "setting changed", ); app.show_toast(&format!("\u{2713} Text selection: {}", kind.as_canonical())); vec![Effect::PersistSetting { key: "keep_text_selection", value: crate::settings::SettingValue::Enum(kind.as_canonical()), rollback_value: crate::settings::SettingValue::Enum(prev.as_canonical()), }] } /// State-only mutation for `scroll_speed`. Updates the process-wide /// cache and recomputes `app.scroll_config`. pub(super) fn set_scroll_speed_inner(app: &mut AppView, clamped: u8) { crate::appearance::cache::set_scroll_speed(clamped); // Full settings-cache rebuild: preserves the other scroll overrides // (mode/invert/lines) instead of resetting them to profile defaults. app.scroll_config = crate::input::mouse::ScrollConfig::from_settings(); } /// Set mouse-wheel scroll speed (registry-driven path). /// /// SHELL-OWNED: persisted to `[ui].scroll_speed` in config.toml via /// `Effect::PersistSetting`. Clamps to `[1, 100]` to match the /// registry's `Int { min: 1, max: 100 }` bounds. pub(in crate::app::dispatch) fn set_scroll_speed(app: &mut AppView, raw: i64) -> Vec { let clamped = raw.clamp(1, 100) as u8; let prev = crate::appearance::cache::load_scroll_speed(); if prev == clamped { return vec![]; } set_scroll_speed_inner(app, clamped); refresh_open_settings_modals(app); tracing::info!( target: "settings", key = "scroll_speed", value = clamped, "setting changed", ); app.show_toast(&format!("\u{2713} Scroll speed: {clamped}")); vec![Effect::PersistSetting { key: "scroll_speed", value: crate::settings::SettingValue::Int(clamped as i64), rollback_value: crate::settings::SettingValue::Int(prev as i64), }] } /// State-only mutation for `scroll_mode`. Updates the process-wide cache /// and recomputes `app.scroll_config`. pub(super) fn set_scroll_mode_inner(app: &mut AppView, mode: crate::appearance::ScrollMode) { crate::appearance::cache::set_scroll_mode(mode); app.scroll_config = crate::input::mouse::ScrollConfig::from_settings(); } /// Set the scroll input classification (`auto` | `wheel` | `trackpad`). /// /// SHELL-OWNED: persisted to `[ui].scroll_mode` via `Effect::PersistSetting`. pub(in crate::app::dispatch) fn set_scroll_mode( app: &mut AppView, mode: crate::appearance::ScrollMode, ) -> Vec { let prev = crate::appearance::cache::load_scroll_mode(); if prev == mode { return vec![]; } set_scroll_mode_inner(app, mode); refresh_open_settings_modals(app); tracing::info!( target: "settings", key = "scroll_mode", value = mode.as_canonical(), "setting changed", ); app.show_toast(&format!("\u{2713} Scroll input: {}", mode.as_canonical())); vec![Effect::PersistSetting { key: "scroll_mode", value: crate::settings::SettingValue::Enum(mode.as_canonical()), rollback_value: crate::settings::SettingValue::Enum(prev.as_canonical()), }] } /// State-only mutation for `invert_scroll`. Updates the process-wide cache /// and recomputes `app.scroll_config`. pub(super) fn set_invert_scroll_inner(app: &mut AppView, enabled: bool) { crate::appearance::cache::set_invert_scroll(enabled); app.scroll_config = crate::input::mouse::ScrollConfig::from_settings(); } /// Set whether vertical scroll direction is inverted ("natural" scrolling). /// /// SHELL-OWNED: persisted to `[ui].invert_scroll` via `Effect::PersistSetting`. pub(in crate::app::dispatch) fn set_invert_scroll(app: &mut AppView, new: bool) -> Vec { let prev = crate::appearance::cache::load_invert_scroll(); if prev == new { return vec![]; } set_invert_scroll_inner(app, new); refresh_open_settings_modals(app); tracing::info!( target: "settings", key = "invert_scroll", value = new, "setting changed", ); app.show_toast(&save_success_toast("Invert scroll", new)); vec![Effect::PersistSetting { key: "invert_scroll", value: crate::settings::SettingValue::Bool(new), rollback_value: crate::settings::SettingValue::Bool(prev), }] } /// State-only mutation for `scroll_lines`. Updates the process-wide cache /// and recomputes `app.scroll_config`. pub(super) fn set_scroll_lines_inner(app: &mut AppView, clamped: u8) { crate::appearance::cache::set_scroll_lines(clamped); app.scroll_config = crate::input::mouse::ScrollConfig::from_settings(); } /// Set the lines-per-tick for both wheel and trackpad scrolling. /// /// SHELL-OWNED: persisted to `[ui].scroll_lines` via `Effect::PersistSetting`. /// Clamps to `[1, 10]` to match the registry's `Int { min: 1, max: 10 }` /// bounds. pub(in crate::app::dispatch) fn set_scroll_lines(app: &mut AppView, raw: i64) -> Vec { let clamped = raw.clamp(1, 10) as u8; // `None` (never configured → profile default) counts as changed: an // explicit user value must stick even when it matches the profile. let prev = crate::appearance::cache::load_scroll_lines(); if prev == Some(clamped) { return vec![]; } set_scroll_lines_inner(app, clamped); refresh_open_settings_modals(app); tracing::info!( target: "settings", key = "scroll_lines", value = clamped, "setting changed", ); app.show_toast(&format!("\u{2713} Scroll lines: {clamped}")); vec![Effect::PersistSetting { key: "scroll_lines", value: crate::settings::SettingValue::Int(clamped as i64), // prev=None (never configured) can't round-trip through // SettingValue::Int — rolling back to an explicit 3 is the same // deliberate one-way door as the `d`-reset path. rollback_value: crate::settings::SettingValue::Int(prev.unwrap_or(3) as i64), }] } pub(super) fn set_respect_manual_folds_inner(app: &mut AppView, new: bool) { let mut config = app.appearance.clone(); config.scrollback.scroll.respect_manual_folds = new; app.set_appearance(config); } /// Set `respect_manual_folds` (registry-driven path). /// /// PAGER-OWNED: live-applied to every agent via `AppView::set_appearance` /// and persisted to `[scrollback.scroll]` in pager.toml via /// `Effect::PersistSetting`. pub(in crate::app::dispatch) fn set_respect_manual_folds( app: &mut AppView, new: bool, ) -> Vec { let prev = app.appearance.scrollback.scroll.respect_manual_folds; if prev == new { return vec![]; } set_respect_manual_folds_inner(app, new); refresh_open_settings_modals(app); tracing::info!( target: "settings", key = "respect_manual_folds", value = new, "setting changed", ); app.show_toast(&save_success_toast("Respect manual folds", new)); vec![Effect::PersistSetting { key: "respect_manual_folds", value: crate::settings::SettingValue::Bool(new), rollback_value: crate::settings::SettingValue::Bool(prev), }] } /// Set the cursor preselection canonical (registry-driven path). /// /// SHELL-OWNED: persisted to `[ui].default_selected_permission` via /// `Effect::PersistSetting`. The `always_allow_all_sessions` canonical is the /// effective default (the cursor falls to the enable-always-approve row). pub(in crate::app::dispatch) fn set_default_selected_permission( app: &mut AppView, new: String, ) -> Vec { use crate::appearance::permission_cursor::DefaultSelectedPermission; // All callers pass a registry canonical; parsing is total (unknown → // `Default`), so a garbage input degrades to the safe "no preselection" // value. `debug_assert` catches a dispatch bug in tests without a parallel // validator on the hot path. let parsed = DefaultSelectedPermission::from_config_value(&new); debug_assert_eq!( parsed.as_canonical(), new.as_str(), "SetDefaultSelectedPermission expects a registry canonical", ); let new_canonical = parsed.as_canonical(); let prev_canonical = app .current_ui .default_selected_permission .as_deref() .map_or(DefaultSelectedPermission::AlwaysAllowAllSessions, |s| { DefaultSelectedPermission::from_config_value(s) }) .as_canonical(); if prev_canonical == new_canonical { return vec![]; } set_default_selected_permission_inner(app, parsed); refresh_open_settings_modals(app); tracing::info!( target: "settings", key = "default_selected_permission", value = new_canonical, "setting changed", ); app.show_toast(&format!( "\u{2713} Default selected permission: {}", parsed.display(), )); vec![Effect::PersistSetting { key: "default_selected_permission", value: crate::settings::SettingValue::Enum(new_canonical), rollback_value: crate::settings::SettingValue::Enum(prev_canonical), }] } /// State + cache mutation for `default_selected_permission`. Called by the /// commit path AND by [`apply_setting_rollback`](super::ui::apply_setting_rollback) on persist failure. pub(super) fn set_default_selected_permission_inner( app: &mut AppView, value: crate::appearance::permission_cursor::DefaultSelectedPermission, ) { // Mirror to in-memory `UiConfig` so `current_value_for` stays in sync // with the cache for the open settings modal. app.current_ui.default_selected_permission = Some(value.as_canonical().to_string()); // Update the process-wide cache so the next permission prompt picks up // the new value without a restart. crate::appearance::permission_cursor::set_default_selected_permission(value); } // Settings setters — unified dispatch for the settings modal and slash // commands. // // SHARED setters use inner/outer split: // - `set_X_inner`: state-only mutation. Called on success AND rollback. // - `set_X`: calls inner, refreshes modals, toasts, emits PersistSetting. // - Rollback (`apply_setting_rollback`) calls inner only — no re-emit. // // PAGER setters (e.g. `set_multiline_mode`) have no persist/rollback, // so they skip the split. /// State-only mutation for `compact_mode`. Updates the in-memory /// `current_ui` snapshot (read by the modal) and the thread-local cache /// used by hot reads with the USER value, then re-derives the render /// value (auto-compact on short terminals) into the appearance snapshot /// and the agents' prompt widgets. Never touches disk; never emits effects. pub(super) fn set_compact_mode_inner(app: &mut AppView, new: bool) { app.current_ui.compact_mode = new; crate::appearance::cache::set(new); app.apply_effective_compact(); } /// Set compact mode. Idempotent: skips if `new == prev`. pub(in crate::app::dispatch) fn set_compact_mode(app: &mut AppView, new: bool) -> Vec { let prev = app.current_ui.compact_mode; if prev == new { return vec![]; } set_compact_mode_inner(app, new); refresh_open_settings_modals(app); tracing::info!(target: "settings", key = "compact_mode", value = new, "setting changed"); // Turning the setting off while the short-terminal derivation holds keeps // the UI compact; say so instead of implying the layout will loosen. if !new && crate::views::agent::effective_compact(false, app.last_known_terminal_rows) { app.show_toast("\u{2713} Compact mode: off (auto-compact active on small terminal)"); } else { app.show_toast(&save_success_toast("Compact mode", new)); } vec![Effect::PersistSetting { key: "compact_mode", value: crate::settings::SettingValue::Bool(new), rollback_value: crate::settings::SettingValue::Bool(prev), }] } /// State-only mutation for `show_timestamps`. Idempotent fast path /// mirrors `set_compact_mode_inner`. pub(super) fn set_timestamps_inner(app: &mut AppView, new: bool) { app.current_ui.show_timestamps = Some(new); if app.appearance.show_timestamps == new { crate::appearance::cache::set_timestamps(new); return; } let mut config = app.appearance.clone(); config.show_timestamps = new; app.set_appearance(config); crate::appearance::cache::set_timestamps(new); } pub(in crate::app::dispatch) fn set_timestamps(app: &mut AppView, new: bool) -> Vec { let prev = app.current_ui.show_timestamps.unwrap_or(true); if prev == new { return vec![]; } set_timestamps_inner(app, new); refresh_open_settings_modals(app); tracing::info!(target: "settings", key = "show_timestamps", value = new, "setting changed"); app.show_toast(&save_success_toast("Timestamps", new)); vec![Effect::PersistSetting { key: "show_timestamps", value: crate::settings::SettingValue::Bool(new), rollback_value: crate::settings::SettingValue::Bool(prev), }] } /// State-only mutation for `show_timeline`. Mirrors `set_timestamps_inner`. pub(super) fn set_timeline_inner(app: &mut AppView, new: bool) { app.current_ui.show_timeline = Some(new); if app.appearance.show_timeline == new { crate::appearance::cache::set_show_timeline(new); return; } let mut config = app.appearance.clone(); config.show_timeline = new; app.set_appearance(config); crate::appearance::cache::set_show_timeline(new); } pub(in crate::app::dispatch) fn set_timeline(app: &mut AppView, new: bool) -> Vec { // Gate on the displayed state (`appearance.show_timeline`, what the rail // renders from and what `/timeline` toggles against) — not the separately // hydrated `current_ui`, which could disagree and make the toggle no-op. let prev = app.appearance.show_timeline; if prev == new { return vec![]; } set_timeline_inner(app, new); refresh_open_settings_modals(app); tracing::info!(target: "settings", key = "show_timeline", value = new, "setting changed"); app.show_toast(&save_success_toast("Timeline sidebar", new)); vec![Effect::PersistSetting { key: "show_timeline", value: crate::settings::SettingValue::Bool(new), rollback_value: crate::settings::SettingValue::Bool(prev), }] } /// State-only mutation for `simple_mode`. /// /// Propagates to every agent's `input_mode` so the toggle takes /// effect immediately (not just on new agents). pub(super) fn set_simple_mode_inner(app: &mut AppView, new: bool) { app.current_ui.simple_mode = Some(new); crate::appearance::cache::set_simple_mode(new); // Reconcile every agent's `input_mode` — global toggle. let target_mode = if new { crate::views::agent::InputMode::Simple } else { crate::views::agent::InputMode::Vim }; for agent in app.agents.values_mut() { if agent.input_mode != target_mode { agent.set_input_mode(target_mode); } } } pub(in crate::app::dispatch) fn set_simple_mode(app: &mut AppView, new: bool) -> Vec { let prev = app.current_ui.simple_mode.unwrap_or(true); // No idempotency gate — fans out to every agent's `input_mode`. // A newly-inserted agent may have a stale default, so we always // propagate. The inner is per-agent idempotent. set_simple_mode_inner(app, new); refresh_open_settings_modals(app); tracing::info!(target: "settings", key = "simple_mode", value = new, "setting changed"); // Toast label matches the registry's display label // ("Disable vim input mode"), not the setting's internal name, so // the modal and toast agree. app.show_toast(&save_success_toast("Disable vim input mode", new)); vec![Effect::PersistSetting { key: "simple_mode", value: crate::settings::SettingValue::Bool(new), rollback_value: crate::settings::SettingValue::Bool(prev), }] } // Contextual-hint tips: the `contextual_hints.*` per-tip toggles. // // SHELL-owned: persisted to `[ui.contextual_hints]`. Each setter writes the // user-config Option, then re-resolves ALL tips (env master + remote) and // re-propagates the prompt gates to every agent, so a toggle takes effect at // runtime (not just on next launch). `write` is a non-capturing closure that // coerces to `fn` so the tips share one inner. /// State-only mutation: write one tip's user-config Option, then re-resolve and /// fan the resolved gates out to `app` + every agent prompt. pub(super) fn set_contextual_hint_inner( app: &mut AppView, write: fn(&mut kigi_shell::agent::config::ContextualHints, Option), new: bool, ) { write(&mut app.current_ui.contextual_hints, Some(new)); let resolved = kigi_shell::util::config::resolve_contextual_hints( &app.current_ui.contextual_hints, app.remote_contextual_hints.as_ref(), ); app.apply_contextual_hints(resolved); } /// Shared outer for the per-tip toggles: idempotency gate, state mutation, /// modal refresh, toast, and `Effect::PersistSetting`. fn set_contextual_hint( app: &mut AppView, key: crate::settings::SettingKey, label: &str, prev: Option, write: fn(&mut kigi_shell::agent::config::ContextualHints, Option), new: bool, ) -> Vec { // Skip when the stored user-explicit value already matches (no-op toggle). if prev == Some(new) { return vec![]; } // `None` (inherit) collapses to the default ON for rollback — same lossy // shape the other `Option` settings use. let rollback = prev.unwrap_or(true); set_contextual_hint_inner(app, write, new); refresh_open_settings_modals(app); tracing::info!(target: "settings", key, value = new, "setting changed"); app.show_toast(&save_success_toast(label, new)); vec![Effect::PersistSetting { key, value: crate::settings::SettingValue::Bool(new), rollback_value: crate::settings::SettingValue::Bool(rollback), }] } pub(in crate::app::dispatch) fn set_contextual_hint_undo( app: &mut AppView, new: bool, ) -> Vec { let prev = app.current_ui.contextual_hints.undo; set_contextual_hint( app, "contextual_hints.undo", "Undo hint", prev, |h, v| h.undo = v, new, ) } pub(in crate::app::dispatch) fn set_contextual_hint_plan_mode( app: &mut AppView, new: bool, ) -> Vec { let prev = app.current_ui.contextual_hints.plan_mode; set_contextual_hint( app, "contextual_hints.plan_mode", "Plan mode hint", prev, |h, v| h.plan_mode = v, new, ) } pub(in crate::app::dispatch) fn set_contextual_hint_image_input( app: &mut AppView, new: bool, ) -> Vec { let prev = app.current_ui.contextual_hints.image_input; set_contextual_hint( app, "contextual_hints.image_input", "Image input hint", prev, |h, v| h.image_input = v, new, ) } pub(in crate::app::dispatch) fn set_contextual_hint_send_now( app: &mut AppView, new: bool, ) -> Vec { let prev = app.current_ui.contextual_hints.send_now; set_contextual_hint( app, "contextual_hints.send_now", "Send now hint", prev, |h, v| h.send_now = v, new, ) } pub(in crate::app::dispatch) fn set_contextual_hint_small_screen( app: &mut AppView, new: bool, ) -> Vec { let prev = app.current_ui.contextual_hints.small_screen; set_contextual_hint( app, "contextual_hints.small_screen", "Small screen hint", prev, |h, v| h.small_screen = v, new, ) } pub(in crate::app::dispatch) fn set_contextual_hint_word_select( app: &mut AppView, new: bool, ) -> Vec { let prev = app.current_ui.contextual_hints.word_select; set_contextual_hint( app, "contextual_hints.word_select", "Word select hint", prev, |h, v| h.word_select = v, new, ) } // Theme settings: `theme`, `auto_dark_theme`, `auto_light_theme`. // // Each has a preview/commit split: // - `SetX` (commit): state + visual + persist + toast. // - `PreviewX` (preview): visual only, no persist/toast. // // Auto-theme setters apply visually only when `theme="auto"` AND the // system is in the matching mode; otherwise the value is just stored. // // Unknown names: `error!` in outer (registry skew), `warn!` in inner // (rollback of corrupted config). /// Format a "✓