Skip to main content

rpi_cli/
settings.rs

1//! `~/.rpi/agent/settings.json` — saved user defaults. Mirrors the slice of
2//! pi's `Settings` interface (`packages/coding-agent/src/core/settings-manager.ts`)
3//! that rpi honors: `defaultProvider` / `defaultModel` / `defaultThinkingLevel`
4//! (consumed by `provider::resolve` as pi's `findInitialModel` step 3 — the
5//! saved default, when authed, wins over the built-in fallback), `theme`,
6//! packages, and configurable resource directories.
7//!
8//! pi's `Settings` carries ~40 fields; rpi reads the fields it uses and drops the
9//! rest (serde `default` ignores unknown fields), so a copied pi `settings.json`
10//! parses clean.
11
12use std::collections::HashMap;
13use std::path::{Path, PathBuf};
14
15use crate::config::{self, strip_line_comments, ConfigError};
16
17/// `retry` — upstream's `RetrySettings`. `provider` is parsed but unused: it
18/// configures the provider SDK's own retry loop, which rpi does not run.
19#[derive(serde::Serialize, serde::Deserialize, Clone, Debug, Default)]
20#[serde(rename_all = "camelCase")]
21pub struct RetrySettings {
22    #[serde(default, skip_serializing_if = "Option::is_none")]
23    pub enabled: Option<bool>,
24    #[serde(default, skip_serializing_if = "Option::is_none")]
25    pub max_retries: Option<u32>,
26    #[serde(default, skip_serializing_if = "Option::is_none")]
27    pub base_delay_ms: Option<u64>,
28    #[serde(default, skip_serializing_if = "Option::is_none")]
29    pub max_agent_delay_ms: Option<u64>,
30    /// upstream's nested `retry.provider`. Kept so a copied file round-trips;
31    /// rpi has no provider-SDK retry loop to configure.
32    #[serde(default, skip_serializing_if = "Option::is_none")]
33    pub provider: Option<serde_json::Value>,
34}
35
36/// `compaction` — upstream's `CompactionSettings`.
37#[derive(serde::Serialize, serde::Deserialize, Clone, Debug, Default)]
38#[serde(rename_all = "camelCase")]
39pub struct CompactionSettingsJson {
40    #[serde(default, skip_serializing_if = "Option::is_none")]
41    pub enabled: Option<bool>,
42    #[serde(default, skip_serializing_if = "Option::is_none")]
43    pub reserve_tokens: Option<i64>,
44    #[serde(default, skip_serializing_if = "Option::is_none")]
45    pub keep_recent_tokens: Option<i64>,
46    /// Per-model overrides. Kept for round-tripping; the harness has no
47    /// per-model compaction override.
48    #[serde(default, skip_serializing_if = "Option::is_none")]
49    pub model_overrides: Option<serde_json::Value>,
50}
51
52/// `branchSummary` — upstream's `BranchSummarySettings`.
53#[derive(serde::Serialize, serde::Deserialize, Clone, Debug, Default)]
54#[serde(rename_all = "camelCase")]
55pub struct BranchSummarySettings {
56    #[serde(default, skip_serializing_if = "Option::is_none")]
57    pub reserve_tokens: Option<i64>,
58    /// When true, upstream skips its "Summarize branch?" prompt and defaults to
59    /// *no* summary. rpi has no such prompt, so this is what makes the key mean
60    /// something: `Some(true)` suppresses the summary.
61    #[serde(default, skip_serializing_if = "Option::is_none")]
62    pub skip_prompt: Option<bool>,
63}
64
65/// `steeringMode` / `followUpMode` — upstream's queue drain modes.
66#[derive(serde::Serialize, serde::Deserialize, Clone, Copy, Debug, PartialEq, Eq)]
67#[serde(rename_all = "kebab-case")]
68pub enum QueueModeSetting {
69    All,
70    OneAtATime,
71}
72
73impl From<QueueModeSetting> for rpi_agent::QueueMode {
74    fn from(mode: QueueModeSetting) -> Self {
75        match mode {
76            QueueModeSetting::All => rpi_agent::QueueMode::All,
77            QueueModeSetting::OneAtATime => rpi_agent::QueueMode::OneAtATime,
78        }
79    }
80}
81
82/// The honored subset of pi's `Settings`. Unknown fields are ignored.
83#[derive(serde::Deserialize, Default, Clone, Debug)]
84#[serde(rename_all = "camelCase")]
85pub struct Settings {
86    /// Saved default provider id (v1 honors only `"anthropic"`; an absent or
87    /// anthropic value allows the saved default-model lookup).
88    #[serde(default)]
89    pub default_provider: Option<String>,
90    /// Saved default model id. When present and the model is authed,
91    /// `provider::resolve` selects it (pi `findInitialModel` step 3).
92    #[serde(default)]
93    pub default_model: Option<String>,
94    /// Saved default thinking level (a level-name string: off/minimal/low/medium/
95    /// high/xhigh/max). Parsed by the caller via `args::parse_thinking_level`.
96    #[serde(default)]
97    pub default_thinking_level: Option<String>,
98    /// Saved theme name. Surfaced for best-effort TUI theme application.
99    #[serde(default)]
100    pub theme: Option<String>,
101    /// `/scoped-models`: the model ids allowed in the Ctrl+P cycle. Absent /
102    /// empty ⇒ every catalog model cycles (the default). upstream spells this
103    /// `enabledModels`, which is accepted as an alias so a copied `settings.json`
104    /// behaves the same.
105    #[serde(default, alias = "enabledModels")]
106    pub scoped_models: Option<Vec<String>>,
107
108    /// Custom session storage directory. upstream's `sessionDir`, same format as
109    /// the `--session-dir` flag (which wins over it).
110    #[serde(default)]
111    pub session_dir: Option<PathBuf>,
112
113    /// Custom shell binary. upstream's `shellPath`. Parsed for fidelity, but
114    /// **not consumed yet**: nothing in pi-tools carries a shell binary, so
115    /// honouring it means plumbing one through `BashToolOptions` /
116    /// `ShellCaptureOptions` first.
117    #[serde(default)]
118    pub shell_path: Option<String>,
119
120    /// Prefix prepended to every bash command (upstream's `shellCommandPrefix`,
121    /// e.g. `shopt -s expand_aliases`).
122    #[serde(default)]
123    pub shell_command_prefix: Option<String>,
124
125    /// Proxy URL applied as `HTTP_PROXY` / `HTTPS_PROXY` for rpi's own HTTP
126    /// clients. upstream's `httpProxy`.
127    #[serde(default)]
128    pub http_proxy: Option<String>,
129
130    /// Retry policy for assistant turns. Mirrors upstream `retry`; the fields
131    /// map 1:1 onto the harness's `RetryPolicy`.
132    #[serde(default)]
133    pub retry: Option<RetrySettings>,
134
135    /// Compaction settings. Mirrors upstream `compaction` (minus its
136    /// `modelOverrides`, which the harness has no equivalent for).
137    #[serde(default)]
138    pub compaction: Option<CompactionSettingsJson>,
139
140    /// Branch-summary settings. Mirrors upstream `branchSummary`.
141    ///
142    /// Parsed for shape fidelity, but **nothing consumes it yet**: rpi's
143    /// `navigate_tree` ignores its `summarize` argument outright (`_summarize`),
144    /// and `generate_branch_summary` has no production caller. Until
145    /// navigation-with-summary is wired, these keys cannot change behaviour — see
146    /// the doc comment on the accessors.
147    #[serde(default)]
148    pub branch_summary: Option<BranchSummarySettings>,
149
150    /// How queued steering messages are drained. upstream default:
151    /// `"one-at-a-time"` (matches the harness's `QueueMode` default).
152    #[serde(default)]
153    pub steering_mode: Option<QueueModeSetting>,
154
155    /// How queued follow-up messages are drained. upstream default:
156    /// `"one-at-a-time"`.
157    #[serde(default)]
158    pub follow_up_mode: Option<QueueModeSetting>,
159    /// Additional skill directories. Relative paths are resolved against the
160    /// settings file's owner (project root for project settings, agent dir for
161    /// global settings). `skills` is accepted as a compatibility shorthand.
162    #[serde(default, alias = "skills")]
163    pub skill_dirs: Option<Vec<String>>,
164    /// Additional prompt-template directories/files. `prompts` is accepted as
165    /// a compatibility shorthand.
166    #[serde(default, alias = "prompts")]
167    pub prompt_dirs: Option<Vec<String>>,
168    /// Additional Rust cdylib extension directories. `extensions` is accepted
169    /// as a compatibility shorthand.
170    #[serde(default, alias = "extensions")]
171    pub extension_dirs: Option<Vec<String>>,
172    /// upstream keybinding overrides. Values may be a key string, an array
173    /// of key strings, or an empty array to unbind an action.
174    #[serde(default)]
175    pub keybindings: Option<HashMap<String, serde_json::Value>>,
176    /// Action performed by two quick Escape presses while the editor is empty.
177    /// upstream defaults this to `tree`; `none` disables the gesture.
178    #[serde(default)]
179    pub double_escape_action: Option<String>,
180    /// Default tools to enable at startup. When absent, all built-in tools
181    /// are enabled. When present, only the named tools are enabled.
182    #[serde(default)]
183    pub default_tools: Option<Vec<String>>,
184    /// Hide the body of thinking blocks while retaining a compact label.
185    #[serde(default)]
186    pub hide_thinking_block: Option<bool>,
187    /// Suppress the interactive startup header and resource summary.
188    /// Update checks remain enabled, matching upstream.
189    #[serde(default)]
190    pub quiet_startup: Option<bool>,
191    /// Most recent release notes seen in a fresh interactive session (global).
192    #[serde(default)]
193    pub last_changelog_version: Option<String>,
194    /// Show a compact upgrade notice instead of full notes. Pi defaults false.
195    #[serde(default)]
196    pub collapse_changelog: Option<bool>,
197    /// Show the global terminal progress indicator while a run is active.
198    #[serde(default)]
199    pub show_terminal_progress: Option<bool>,
200    /// Horizontal editor padding in terminal columns.
201    #[serde(default)]
202    pub editor_padding_x: Option<usize>,
203    /// Maximum number of autocomplete rows shown above the editor.
204    #[serde(default)]
205    pub autocomplete_max_visible: Option<usize>,
206    /// Automatically copy text to clipboard when selected in fullscreen mode.
207    /// When false, text selection requires explicit copy action.
208    #[serde(default)]
209    pub fullscreen_copy_on_select: Option<bool>,
210    /// Horizontal padding in terminal columns added to each side of the chat
211    /// transcript (user/assistant messages and thinking). upstream `outputPad`
212    /// (its only legal values are 0 and 1).
213    #[serde(default)]
214    pub output_pad: Option<usize>,
215    /// Show transcript notices for prompt-cache costs and provider recovery
216    /// diagnostics. upstream `showCacheMissNotices` (default `false`).
217    #[serde(default)]
218    pub show_cache_miss_notices: Option<bool>,
219    /// Render images inline in the terminal. rpi stores this at the top level
220    /// under `showImages`; upstream nests it as `terminal.showImages`, which
221    /// [`TerminalSettings`] also accepts (see [`Settings::show_images`]).
222    #[serde(default)]
223    pub show_images: Option<bool>,
224    /// upstream's nested `terminal` settings block. rpi keeps its own flat
225    /// keys for the settings it has always written, but reads this block so a
226    /// native `settings.json` is honored.
227    #[serde(default)]
228    pub terminal: Option<TerminalSettings>,
229    /// How long idle pooled HTTP connections are kept before being closed.
230    /// Accepts a millisecond number or the string `"disabled"`.
231    /// `httpIdleTimeout`. rpi surface: `httpIdleTimeout`.
232    #[serde(default, alias = "httpIdleTimeoutMs")]
233    pub http_idle_timeout: Option<serde_json::Value>,
234}
235
236/// upstream's `terminal` settings block (`TerminalSettings`). Only the fields
237/// rpi can honor are modeled; unknown keys are ignored.
238#[derive(serde::Deserialize, Clone, Debug, Default)]
239#[serde(rename_all = "camelCase")]
240pub struct TerminalSettings {
241    /// `terminal.showImages` — render images inline.
242    #[serde(default)]
243    pub show_images: Option<bool>,
244    /// `terminal.showTerminalProgress` — OSC 9;4 terminal progress indicator.
245    #[serde(default)]
246    pub show_terminal_progress: Option<bool>,
247    /// `terminal.clearOnShrink` — clear empty rows when content shrinks.
248    #[serde(default)]
249    pub clear_on_shrink: Option<bool>,
250}
251
252impl Settings {
253    /// Resolved HTTP idle timeout in milliseconds (`None` ⇒ library default,
254    /// `Some(0)` ⇒ disabled). See [`rpi_ai::http::parse_http_idle_timeout_ms`].
255    pub fn http_idle_timeout_ms(&self) -> Option<i64> {
256        self.http_idle_timeout
257            .as_ref()
258            .and_then(rpi_ai::http::parse_http_idle_timeout_ms)
259    }
260
261    /// Retry policy, resolved onto the harness type. Absent fields keep the
262    /// harness defaults, so a partial `retry` object changes only what it names.
263    pub fn retry_policy(&self) -> rpi_harness::types::RetryPolicy {
264        let mut policy = rpi_harness::types::RetryPolicy::default();
265        let Some(retry) = &self.retry else {
266            return policy;
267        };
268        if let Some(enabled) = retry.enabled {
269            policy.enabled = enabled;
270        }
271        if let Some(max_retries) = retry.max_retries {
272            policy.max_retries = max_retries;
273        }
274        if let Some(base_delay_ms) = retry.base_delay_ms {
275            policy.base_delay_ms = base_delay_ms;
276        }
277        if let Some(max_agent_delay_ms) = retry.max_agent_delay_ms {
278            policy.max_agent_delay_ms = max_agent_delay_ms;
279        }
280        policy
281    }
282
283    /// Compaction settings, resolved onto the harness type.
284    pub fn compaction_settings(&self) -> rpi_harness::types::CompactionSettings {
285        let mut settings = rpi_harness::types::CompactionSettings::default();
286        let Some(compaction) = &self.compaction else {
287            return settings;
288        };
289        if let Some(enabled) = compaction.enabled {
290            settings.enabled = enabled;
291        }
292        if let Some(reserve_tokens) = compaction.reserve_tokens {
293            settings.reserve_tokens = reserve_tokens;
294        }
295        if let Some(keep_recent_tokens) = compaction.keep_recent_tokens {
296            settings.keep_recent_tokens = keep_recent_tokens;
297        }
298        settings
299    }
300
301    /// Tokens reserved for a branch summary's prompt + output.
302    ///
303    /// Not consumed yet: `AgentLane::navigate_tree` ignores `summarize`, so no
304    /// branch summary is ever generated from this path. Wiring it means
305    /// implementing navigation-with-summary in the harness, not reading a key.
306    pub fn branch_summary_reserve_tokens(&self) -> Option<i64> {
307        self.branch_summary.as_ref().and_then(|s| s.reserve_tokens)
308    }
309
310    /// Whether branch summaries are suppressed (upstream's `skipPrompt`).
311    ///
312    /// Not consumed yet, for the same reason as
313    /// [`Self::branch_summary_reserve_tokens`].
314    pub fn branch_summary_skipped(&self) -> bool {
315        self.branch_summary
316            .as_ref()
317            .and_then(|s| s.skip_prompt)
318            .unwrap_or(false)
319    }
320
321    /// Default steering-queue drain mode (upstream default: one-at-a-time).
322    pub fn steering_mode(&self) -> rpi_agent::QueueMode {
323        self.steering_mode
324            .map(Into::into)
325            .unwrap_or(rpi_agent::QueueMode::OneAtATime)
326    }
327
328    /// Default follow-up-queue drain mode (upstream default: one-at-a-time).
329    pub fn follow_up_mode(&self) -> rpi_agent::QueueMode {
330        self.follow_up_mode
331            .map(Into::into)
332            .unwrap_or(rpi_agent::QueueMode::OneAtATime)
333    }
334
335    /// Render images inline. rpi's flat `showImages` wins; upstream's nested
336    /// `terminal.showImages` is the fallback.
337    pub fn show_images(&self) -> Option<bool> {
338        self.show_images
339            .or_else(|| self.terminal.as_ref().and_then(|t| t.show_images))
340    }
341
342    /// Terminal progress indicator. rpi's flat `showTerminalProgress` wins;
343    /// upstream's nested `terminal.showTerminalProgress` is the fallback.
344    pub fn show_terminal_progress(&self) -> Option<bool> {
345        self.show_terminal_progress.or_else(|| {
346            self.terminal
347                .as_ref()
348                .and_then(|t| t.show_terminal_progress)
349        })
350    }
351}
352
353/// Load `~/.rpi/agent/settings.json`. Missing file ⇒ `Settings::default()`
354/// (not an error). Malformed JSON ⇒ `ConfigError::Json`. Tolerates `//` line
355/// comments (a copied pi settings.json may contain them).
356pub fn load_settings() -> Result<Settings, ConfigError> {
357    let path = config::settings_path()?;
358    match std::fs::read_to_string(&path) {
359        Ok(text) => parse_settings(&text).map_err(|e| ConfigError::Json {
360            path: path.clone(),
361            source: e,
362        }),
363        Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(Settings::default()),
364        Err(e) => Err(ConfigError::Read { path, source: e }),
365    }
366}
367
368/// Load the single active project settings document. `.rpi/settings.json`
369/// is the only project settings file rpi reads.
370pub fn load_project_settings(cwd: &Path) -> Vec<Settings> {
371    load_project_settings_with_paths(cwd)
372        .into_iter()
373        .map(|(_, settings)| settings)
374        .collect()
375}
376
377/// Load the active project settings together with its source path.
378pub fn load_project_settings_with_paths(cwd: &Path) -> Vec<(PathBuf, Settings)> {
379    load_active_project_settings(cwd)
380        .ok()
381        .flatten()
382        .into_iter()
383        .collect()
384}
385
386/// Load the active project settings while preserving parse/read failures for
387/// callers that must fail closed, such as package install/update preflight.
388pub fn load_active_project_settings(
389    cwd: &Path,
390) -> Result<Option<(PathBuf, Settings)>, ConfigError> {
391    let preferred = cwd.join(".rpi/settings.json");
392    match load_settings_file(&preferred) {
393        Ok(settings) => Ok(Some((preferred, settings))),
394        Err(ConfigError::Read { source, .. }) if source.kind() == std::io::ErrorKind::NotFound => {
395            Ok(None)
396        }
397        Err(error) => Err(error),
398    }
399}
400
401/// Load model-selection settings with upstream's global -> trusted-project
402/// precedence. Only fields consumed during provider resolution are overlaid;
403/// package and resource loading keeps its own scope-aware merge semantics.
404pub fn load_effective_model_settings(
405    cwd: &Path,
406    project_trusted: bool,
407) -> Result<Settings, ConfigError> {
408    let mut effective = load_settings()?;
409    if project_trusted {
410        if let Some((_, project)) = load_active_project_settings(cwd)? {
411            if project.default_provider.is_some() {
412                effective.default_provider = project.default_provider;
413            }
414            if project.default_model.is_some() {
415                effective.default_model = project.default_model;
416            }
417            if project.default_thinking_level.is_some() {
418                effective.default_thinking_level = project.default_thinking_level;
419            }
420            if project.theme.is_some() {
421                effective.theme = project.theme;
422            }
423        }
424    }
425    Ok(effective)
426}
427
428/// Load the project settings document that rpi may safely update.
429pub fn load_project_settings_for_write(cwd: &Path) -> Result<Settings, ConfigError> {
430    Ok(load_active_project_settings(cwd)?
431        .map(|(_, settings)| settings)
432        .unwrap_or_default())
433}
434
435fn load_settings_file(path: &Path) -> Result<Settings, ConfigError> {
436    let text = std::fs::read_to_string(path).map_err(|source| ConfigError::Read {
437        path: path.to_path_buf(),
438        source,
439    })?;
440    parse_settings(&text).map_err(|source| ConfigError::Json {
441        path: path.to_path_buf(),
442        source,
443    })
444}
445
446/// Resolve a configured path relative to its settings owner. Absolute paths
447/// are preserved; empty entries are ignored.
448pub fn resolve_configured_paths(base: &Path, values: &[String]) -> Vec<PathBuf> {
449    values
450        .iter()
451        .map(|value| value.trim())
452        .filter(|value| !value.is_empty())
453        .map(|value| {
454            let path = PathBuf::from(value);
455            if path.is_absolute() {
456                path
457            } else {
458                base.join(path)
459            }
460        })
461        .collect()
462}
463
464fn parse_settings(text: &str) -> Result<Settings, serde_json::Error> {
465    match serde_json::from_str(text) {
466        Ok(s) => Ok(s),
467        Err(first) => {
468            let stripped = strip_line_comments(text);
469            serde_json::from_str(&stripped).map_err(|_| first)
470        }
471    }
472}
473
474/// Parse an existing settings document while retaining fields that rpi does
475/// not model. upstream permits `//` line comments in `settings.json`, so use
476/// the same strict-then-comment-stripped strategy as [`parse_settings`].
477/// Keeping this separate from `parse_settings` is important: deserializing
478/// into [`Settings`] would discard unknown fields before a save/merge.
479fn parse_settings_value(text: &str) -> Result<serde_json::Value, serde_json::Error> {
480    match serde_json::from_str::<serde_json::Value>(text) {
481        Ok(value) => Ok(value),
482        Err(first) => {
483            let stripped = strip_line_comments(text);
484            serde_json::from_str::<serde_json::Value>(&stripped).map_err(|_| first)
485        }
486    }
487}
488
489/// Persist the honored settings back to `~/.rpi/agent/settings.json`.
490/// Unknown pi fields (which `Settings` doesn't model) are **preserved**: the
491/// current file is read as raw JSON, the known fields are overlaid, and the
492/// merged object is written — so a copied pi `settings.json` survives edits
493/// without losing pi-only keys. `//` comments are accepted using the same
494/// parser as [`load_settings`]. Missing file ⇒ a fresh object. Existing files
495/// that cannot be parsed safely are left untouched and reported as errors.
496pub fn save_settings(settings: &Settings) -> Result<(), String> {
497    let path = config::settings_path().map_err(|e| e.to_string())?;
498    save_settings_to_path(settings, &path, None)
499}
500
501/// Persist project-scoped settings under `.rpi/settings.json`.
502pub fn save_project_settings(cwd: &Path, settings: &Settings) -> Result<(), String> {
503    save_settings_to_path(settings, &cwd.join(".rpi/settings.json"), None)
504}
505
506fn save_settings_to_path(
507    settings: &Settings,
508    path: &Path,
509    fallback: Option<&Path>,
510) -> Result<(), String> {
511    // Read the existing file as a raw object to preserve unknown fields.
512    let mut merged = match std::fs::read_to_string(path) {
513        Ok(text) => parse_settings_value(&text).map_err(|error| {
514            format!(
515                "cannot parse existing settings file {}: {error}",
516                path.display()
517            )
518        })?,
519        Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
520            read_fallback_settings_value(path, fallback)?
521                .unwrap_or_else(|| serde_json::Value::Object(Default::default()))
522        }
523        Err(error) => {
524            return Err(format!(
525                "cannot read existing settings file {}: {error}",
526                path.display()
527            ));
528        }
529    };
530    let obj = merged
531        .as_object_mut()
532        .ok_or_else(|| format!("settings file {} is not an object", path.display()))?;
533    for (key, val) in [
534        ("defaultProvider", settings.default_provider.as_ref()),
535        ("defaultModel", settings.default_model.as_ref()),
536        (
537            "defaultThinkingLevel",
538            settings.default_thinking_level.as_ref(),
539        ),
540        ("theme", settings.theme.as_ref()),
541        (
542            "lastChangelogVersion",
543            settings.last_changelog_version.as_ref(),
544        ),
545    ] {
546        match val {
547            Some(v) => {
548                obj.insert(key.to_string(), serde_json::Value::String(v.clone()));
549            }
550            None => {
551                obj.remove(key);
552            }
553        }
554    }
555    match &settings.scoped_models {
556        Some(list) if !list.is_empty() => {
557            obj.insert(
558                "scopedModels".to_string(),
559                serde_json::Value::Array(
560                    list.iter()
561                        .map(|m| serde_json::Value::String(m.clone()))
562                        .collect(),
563                ),
564            );
565        }
566        _ => {
567            obj.remove("scopedModels");
568        }
569    }
570    match &settings.keybindings {
571        Some(bindings) => {
572            obj.insert(
573                "keybindings".to_string(),
574                serde_json::to_value(bindings).map_err(|e| e.to_string())?,
575            );
576        }
577        None => {
578            obj.remove("keybindings");
579        }
580    }
581    match settings.double_escape_action.as_deref() {
582        Some(action) if !action.trim().is_empty() => {
583            obj.insert(
584                "doubleEscapeAction".to_string(),
585                serde_json::Value::String(action.to_string()),
586            );
587        }
588        _ => {
589            obj.remove("doubleEscapeAction");
590        }
591    }
592    for (key, value) in [
593        (
594            "hideThinkingBlock",
595            settings.hide_thinking_block.map(serde_json::Value::Bool),
596        ),
597        (
598            "quietStartup",
599            settings.quiet_startup.map(serde_json::Value::Bool),
600        ),
601        (
602            "collapseChangelog",
603            settings.collapse_changelog.map(serde_json::Value::Bool),
604        ),
605        (
606            "showTerminalProgress",
607            settings.show_terminal_progress.map(serde_json::Value::Bool),
608        ),
609        (
610            "showImages",
611            settings.show_images.map(serde_json::Value::Bool),
612        ),
613        (
614            "showCacheMissNotices",
615            settings
616                .show_cache_miss_notices
617                .map(serde_json::Value::Bool),
618        ),
619        (
620            "fullscreenCopyOnSelect",
621            settings
622                .fullscreen_copy_on_select
623                .map(serde_json::Value::Bool),
624        ),
625        (
626            "editorPaddingX",
627            settings
628                .editor_padding_x
629                .map(|v| serde_json::Value::Number(v.into())),
630        ),
631        (
632            "outputPad",
633            settings
634                .output_pad
635                .map(|v| serde_json::Value::Number(v.into())),
636        ),
637        (
638            "autocompleteMaxVisible",
639            settings
640                .autocomplete_max_visible
641                .map(|v| serde_json::Value::Number(v.into())),
642        ),
643        ("httpIdleTimeout", settings.http_idle_timeout.clone()),
644    ] {
645        match value {
646            Some(value) => {
647                obj.insert(key.to_string(), value);
648            }
649            None => {
650                obj.remove(key);
651            }
652        }
653    }
654    if let Some(parent) = path.parent() {
655        std::fs::create_dir_all(parent).map_err(|e| e.to_string())?;
656    }
657    let text = serde_json::to_string_pretty(&merged).map_err(|e| e.to_string())?;
658    config::atomic_write(path, text.as_bytes()).map_err(|e| e.to_string())
659}
660
661/// When rpi is still reading upstream's settings fallback, seed the first
662/// rpi-owned save from that complete raw object. Otherwise a modeled-field
663/// save could shadow the native file and silently drop fields added by Pi.
664fn read_fallback_settings_value(
665    target: &Path,
666    fallback: Option<&Path>,
667) -> Result<Option<serde_json::Value>, String> {
668    let Some(path) = fallback else {
669        return Ok(None);
670    };
671    if path == target {
672        return Ok(None);
673    }
674    match std::fs::read_to_string(&path) {
675        Ok(text) => parse_settings_value(&text).map(Some).map_err(|error| {
676            format!(
677                "cannot parse fallback settings file {}: {error}",
678                path.display()
679            )
680        }),
681        Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(None),
682        Err(error) => Err(format!(
683            "cannot read fallback settings file {}: {error}",
684            path.display()
685        )),
686    }
687}
688
689#[cfg(test)]
690mod tests {
691    use super::*;
692    use crate::config::test_support::env_lock;
693
694    /// Point `RPI_CODING_AGENT_DIR` at a fresh temp dir for this test.
695    struct TempConfig {
696        _guard: std::sync::MutexGuard<'static, ()>,
697        _tmp: tempfile::TempDir,
698        prev: Option<std::ffi::OsString>,
699    }
700    impl TempConfig {
701        fn new() -> Self {
702            let guard = env_lock().lock().unwrap();
703            let prev = std::env::var_os(config::CONFIG_DIR_ENV);
704            let tmp = tempfile::TempDir::new().unwrap();
705            std::env::set_var(config::CONFIG_DIR_ENV, tmp.path());
706            Self {
707                _guard: guard,
708                _tmp: tmp,
709                prev,
710            }
711        }
712    }
713    impl Drop for TempConfig {
714        fn drop(&mut self) {
715            match self.prev.take() {
716                Some(v) => std::env::set_var(config::CONFIG_DIR_ENV, v),
717                None => std::env::remove_var(config::CONFIG_DIR_ENV),
718            }
719        }
720    }
721
722    #[test]
723    fn missing_settings_is_default() {
724        let _cfg = TempConfig::new();
725        let s = load_settings().unwrap();
726        assert!(s.default_provider.is_none());
727        assert!(s.default_model.is_none());
728        assert!(s.default_thinking_level.is_none());
729        assert!(s.theme.is_none());
730        assert!(s.skill_dirs.is_none());
731        assert!(s.prompt_dirs.is_none());
732        assert!(s.extension_dirs.is_none());
733    }
734
735    #[test]
736    fn reads_honored_fields_and_ignores_unknown() {
737        let _cfg = TempConfig::new();
738        let path = config::settings_path().unwrap();
739        std::fs::create_dir_all(path.parent().unwrap()).unwrap();
740        // A pi-style settings.json with many unknown fields + the 4 we honor.
741        std::fs::write(
742            &path,
743            r#"{
744                "lastChangelogVersion": "1.0.0",
745                "defaultProvider": "anthropic",
746                "defaultModel": "claude-sonnet-5",
747                "defaultThinkingLevel": "high",
748                "theme": "dark",
749                "hideThinkingBlock": true,
750                "quietStartup": true,
751                "showTerminalProgress": false,
752                "editorPaddingX": 3,
753                "autocompleteMaxVisible": 7,
754                "compaction": { "threshold": 100 }
755            }"#,
756        )
757        .unwrap();
758        let s = load_settings().unwrap();
759        assert_eq!(s.default_provider.as_deref(), Some("anthropic"));
760        assert_eq!(s.default_model.as_deref(), Some("claude-sonnet-5"));
761        assert_eq!(s.default_thinking_level.as_deref(), Some("high"));
762        assert_eq!(s.theme.as_deref(), Some("dark"));
763        assert_eq!(s.hide_thinking_block, Some(true));
764        assert_eq!(s.quiet_startup, Some(true));
765        assert_eq!(s.show_terminal_progress, Some(false));
766        assert_eq!(s.editor_padding_x, Some(3));
767        assert_eq!(s.autocomplete_max_visible, Some(7));
768    }
769
770    #[test]
771    fn project_settings_only_use_rpi() {
772        let tmp = tempfile::tempdir().unwrap();
773        std::fs::create_dir_all(tmp.path().join(".rpi")).unwrap();
774        std::fs::write(
775            tmp.path().join(".rpi/settings.json"),
776            r#"{"skillDirs":["rpi-skills"],"extensions":["rpi-ext"]}"#,
777        )
778        .unwrap();
779
780        let settings = load_project_settings(tmp.path());
781        assert_eq!(settings.len(), 1);
782        assert_eq!(
783            settings[0].skill_dirs.as_deref(),
784            Some(["rpi-skills".to_string()].as_slice())
785        );
786        assert_eq!(
787            settings[0].extension_dirs.as_deref(),
788            Some(["rpi-ext".to_string()].as_slice())
789        );
790    }
791
792    #[test]
793    fn malformed_rpi_project_settings_are_not_read() {
794        let tmp = tempfile::tempdir().unwrap();
795        std::fs::create_dir_all(tmp.path().join(".rpi")).unwrap();
796        std::fs::write(tmp.path().join(".rpi/settings.json"), "{ malformed").unwrap();
797
798        assert!(load_project_settings_with_paths(tmp.path()).is_empty());
799        assert!(load_active_project_settings(tmp.path()).is_err());
800    }
801
802    #[test]
803    fn trusted_project_model_defaults_override_global_defaults() {
804        let _cfg = TempConfig::new();
805        let global = config::settings_path().unwrap();
806        std::fs::create_dir_all(global.parent().unwrap()).unwrap();
807        std::fs::write(
808            global,
809            r#"{"defaultProvider":"global","defaultModel":"global-model","theme":"dark"}"#,
810        )
811        .unwrap();
812        let project = tempfile::tempdir().unwrap();
813        std::fs::create_dir_all(project.path().join(".rpi")).unwrap();
814        std::fs::write(
815            project.path().join(".rpi/settings.json"),
816            r#"{"defaultProvider":"project","defaultModel":"project-model"}"#,
817        )
818        .unwrap();
819
820        let trusted = load_effective_model_settings(project.path(), true).unwrap();
821        assert_eq!(trusted.default_provider.as_deref(), Some("project"));
822        assert_eq!(trusted.default_model.as_deref(), Some("project-model"));
823        assert_eq!(trusted.theme.as_deref(), Some("dark"));
824
825        let untrusted = load_effective_model_settings(project.path(), false).unwrap();
826        assert_eq!(untrusted.default_provider.as_deref(), Some("global"));
827        assert_eq!(untrusted.default_model.as_deref(), Some("global-model"));
828    }
829
830    #[test]
831    fn tolerates_line_comments() {
832        let _cfg = TempConfig::new();
833        let path = config::settings_path().unwrap();
834        std::fs::create_dir_all(path.parent().unwrap()).unwrap();
835        std::fs::write(
836            &path,
837            "{\n  // my default\n  \"defaultModel\": \"glm-5\",\n  \"theme\": \"light\"\n}\n",
838        )
839        .unwrap();
840        let s = load_settings().unwrap();
841        assert_eq!(s.default_model.as_deref(), Some("glm-5"));
842        assert_eq!(s.theme.as_deref(), Some("light"));
843    }
844
845    #[test]
846    fn malformed_is_error() {
847        let _cfg = TempConfig::new();
848        let path = config::settings_path().unwrap();
849        std::fs::create_dir_all(path.parent().unwrap()).unwrap();
850        std::fs::write(&path, "{ not json").unwrap();
851        assert!(matches!(load_settings(), Err(ConfigError::Json { .. })));
852    }
853}
854
855#[cfg(test)]
856mod scoped_tests {
857    use super::*;
858    use crate::config::test_support::env_lock;
859
860    fn with_temp_env() -> (tempfile::TempDir, std::sync::MutexGuard<'static, ()>) {
861        let guard = env_lock().lock().unwrap();
862        let tmp = tempfile::TempDir::new().unwrap();
863        std::env::set_var(config::CONFIG_DIR_ENV, tmp.path());
864        (tmp, guard)
865    }
866
867    /// The settings upstream lets users tune must reach the harness types.
868    ///
869    /// These were previously hardcoded defaults, so a `settings.json` copied from
870    /// upstream parsed cleanly but changed nothing — the exact failure mode a
871    /// "parses but is not consumed" field creates.
872
873    /// `sessionDir` selects the session directory, with the CLI flag winning.
874    ///
875    /// upstream's comment: "same format as --session-dir CLI flag". A settings
876    /// key that parses but is ignored is exactly the failure mode this closes.
877    #[test]
878    fn session_dir_setting_selects_the_session_directory() {
879        let settings: Settings =
880            serde_json::from_str(r#"{"sessionDir": "/tmp/from-settings"}"#).unwrap();
881        assert_eq!(
882            settings.session_dir,
883            Some(std::path::PathBuf::from("/tmp/from-settings"))
884        );
885        // Absent ⇒ the caller falls back to its built-in default.
886        assert_eq!(Settings::default().session_dir, None);
887    }
888
889    /// The bash-facing keys must reach the tool options.
890    #[test]
891    fn shell_command_prefix_reaches_the_bash_tool() {
892        let settings: Settings =
893            serde_json::from_str(r#"{"shellCommandPrefix": "shopt -s expand_aliases"}"#).unwrap();
894        assert_eq!(
895            settings.shell_command_prefix.as_deref(),
896            Some("shopt -s expand_aliases")
897        );
898        // `shellPath` is parsed (native's shape round-trips) but nothing consumes
899        // it yet — see its doc comment for why.
900        let with_shell: Settings =
901            serde_json::from_str(r#"{"shellPath": "/usr/bin/zsh"}"#).unwrap();
902        assert_eq!(with_shell.shell_path.as_deref(), Some("/usr/bin/zsh"));
903    }
904
905    /// `httpProxy` is stored in native's shape for the client setup to apply.
906    #[test]
907    fn http_proxy_setting_is_read() {
908        let settings: Settings =
909            serde_json::from_str(r#"{"httpProxy": "http://127.0.0.1:8888"}"#).unwrap();
910        assert_eq!(
911            settings.http_proxy.as_deref(),
912            Some("http://127.0.0.1:8888")
913        );
914        assert_eq!(Settings::default().http_proxy, None);
915    }
916    #[test]
917    fn harness_options_come_from_settings() {
918        let settings: Settings = serde_json::from_str(
919            r#"{
920                "retry": { "enabled": false, "maxRetries": 7, "baseDelayMs": 250, "maxAgentDelayMs": 9000 },
921                "compaction": { "enabled": false, "reserveTokens": 1234, "keepRecentTokens": 4321 },
922                "steeringMode": "all",
923                "followUpMode": "all"
924            }"#,
925        )
926        .expect("settings parse");
927
928        let retry = settings.retry_policy();
929        assert!(!retry.enabled);
930        assert_eq!(retry.max_retries, 7);
931        assert_eq!(retry.base_delay_ms, 250);
932        assert_eq!(retry.max_agent_delay_ms, 9000);
933
934        let compaction = settings.compaction_settings();
935        assert!(!compaction.enabled);
936        assert_eq!(compaction.reserve_tokens, 1234);
937        assert_eq!(compaction.keep_recent_tokens, 4321);
938
939        assert_eq!(settings.steering_mode(), rpi_agent::QueueMode::All);
940        assert_eq!(settings.follow_up_mode(), rpi_agent::QueueMode::All);
941    }
942
943    /// An absent key keeps the harness default rather than a zero value: a partial
944    /// `retry` object must change only what it names.
945    #[test]
946    fn absent_harness_settings_keep_the_harness_defaults() {
947        let empty = Settings::default();
948        let default_retry = rpi_harness::types::RetryPolicy::default();
949        let retry = empty.retry_policy();
950        assert_eq!(retry.enabled, default_retry.enabled);
951        assert_eq!(retry.max_retries, default_retry.max_retries);
952        assert_eq!(retry.base_delay_ms, default_retry.base_delay_ms);
953        assert_eq!(retry.max_agent_delay_ms, default_retry.max_agent_delay_ms);
954        let default_compaction = rpi_harness::types::CompactionSettings::default();
955        let compaction = empty.compaction_settings();
956        assert_eq!(compaction.enabled, default_compaction.enabled);
957        assert_eq!(compaction.reserve_tokens, default_compaction.reserve_tokens);
958        assert_eq!(
959            compaction.keep_recent_tokens,
960            default_compaction.keep_recent_tokens
961        );
962
963        // upstream's documented defaults for the queue modes.
964        assert_eq!(empty.steering_mode(), rpi_agent::QueueMode::OneAtATime);
965        assert_eq!(empty.follow_up_mode(), rpi_agent::QueueMode::OneAtATime);
966
967        // A partial object keeps the defaults it does not name.
968        let partial: Settings = serde_json::from_str(r#"{"retry":{"maxRetries":9}}"#).unwrap();
969        let retry = partial.retry_policy();
970        assert_eq!(retry.max_retries, 9);
971        assert_eq!(retry.base_delay_ms, default_retry.base_delay_ms);
972    }
973
974    /// upstream's key names must be accepted, not only rpi's.
975    ///
976    /// A user copying upstream's `settings.json` writes `enabledModels` and
977    /// `httpIdleTimeoutMs`; before this they were silently ignored because rpi
978    /// spelled them differently.
979    #[test]
980    fn native_settings_key_names_are_accepted() {
981        let settings: Settings = serde_json::from_str(
982            r#"{
983                "enabledModels": ["a", "b"],
984                "httpIdleTimeoutMs": 1500,
985                "branchSummary": { "reserveTokens": 4242, "skipPrompt": true }
986            }"#,
987        )
988        .expect("native key names parse");
989
990        assert_eq!(
991            settings.scoped_models,
992            Some(vec!["a".to_string(), "b".to_string()]),
993            "native's `enabledModels` must map onto rpi's scoped models"
994        );
995        assert_eq!(
996            settings.http_idle_timeout_ms(),
997            Some(1500),
998            "native's `httpIdleTimeoutMs` must be read"
999        );
1000        assert_eq!(settings.branch_summary_reserve_tokens(), Some(4242));
1001        assert!(settings.branch_summary_skipped());
1002    }
1003    #[test]
1004    fn save_load_scoped_models_roundtrip() {
1005        let (_tmp, _guard) = with_temp_env();
1006        let mut s = Settings::default();
1007        s.scoped_models = Some(vec!["a".into(), "b".into()]);
1008        save_settings(&s).unwrap();
1009        let loaded = load_settings().unwrap();
1010        assert_eq!(
1011            loaded.scoped_models,
1012            Some(vec!["a".to_string(), "b".to_string()])
1013        );
1014        // Clearing removes the key.
1015        let mut s2 = load_settings().unwrap();
1016        s2.scoped_models = None;
1017        save_settings(&s2).unwrap();
1018        assert_eq!(load_settings().unwrap().scoped_models, None);
1019    }
1020
1021    #[test]
1022    fn changelog_settings_roundtrip_without_losing_existing_configuration() {
1023        let (_tmp, _guard) = with_temp_env();
1024        let path = config::settings_path().unwrap();
1025        std::fs::write(&path, r#"{"defaultModel":"existing-model","lastChangelogVersion":"0.3.16","collapseChangelog":true,"custom":{"retained":true}}"#).unwrap();
1026        let mut settings = load_settings().unwrap();
1027        assert_eq!(settings.last_changelog_version.as_deref(), Some("0.3.16"));
1028        assert_eq!(settings.collapse_changelog, Some(true));
1029        settings.last_changelog_version = Some("0.3.17".into());
1030        save_settings(&settings).unwrap();
1031        let loaded = load_settings().unwrap();
1032        assert_eq!(loaded.last_changelog_version.as_deref(), Some("0.3.17"));
1033        assert_eq!(loaded.default_model.as_deref(), Some("existing-model"));
1034        assert_eq!(loaded.collapse_changelog, Some(true));
1035        let value: serde_json::Value =
1036            serde_json::from_str(&std::fs::read_to_string(path).unwrap()).unwrap();
1037        assert_eq!(value["custom"]["retained"], true);
1038    }
1039
1040    #[test]
1041    fn save_uses_atomic_sibling_replacement() {
1042        let (_tmp, _guard) = with_temp_env();
1043        let path = config::settings_path().unwrap();
1044        std::fs::write(&path, r#"{"theme":"dark","piOnlyField":true}"#).unwrap();
1045
1046        let mut settings = load_settings().unwrap();
1047        settings.theme = Some("light".into());
1048        save_settings(&settings).unwrap();
1049
1050        let saved: serde_json::Value =
1051            serde_json::from_str(&std::fs::read_to_string(&path).unwrap()).unwrap();
1052        assert_eq!(saved["theme"], "light");
1053        assert_eq!(saved["piOnlyField"], true);
1054        // `config::atomic_write` cleans up its sibling by replacing the
1055        // target in one rename; a successful save leaves no staging file.
1056        let temp = path.with_file_name(format!(
1057            ".{}.tmp",
1058            path.file_name().and_then(|name| name.to_str()).unwrap()
1059        ));
1060        assert!(!temp.exists(), "atomic staging file should not remain");
1061    }
1062
1063    #[test]
1064    fn save_preserves_unknown_fields_and_writes_the_tui_settings() {
1065        // Regression: `fullscreenCopyOnSelect` and `httpIdleTimeout` used to be
1066        // read but never written back, so `/settings` changes silently
1067        // vanished. The newer `/settings` rows (showImages,
1068        // showCacheMissNotices, outputPad) must round-trip too.
1069        let (_tmp, _guard) = with_temp_env();
1070        let path = config::settings_path().unwrap();
1071        let mut settings = Settings::default();
1072        settings.show_images = Some(false);
1073        settings.show_cache_miss_notices = Some(true);
1074        settings.output_pad = Some(0);
1075        settings.fullscreen_copy_on_select = Some(false);
1076        settings.http_idle_timeout = Some(serde_json::json!(30_000));
1077        settings.show_terminal_progress = Some(false);
1078        save_settings(&settings).unwrap();
1079
1080        let raw: serde_json::Value =
1081            serde_json::from_str(&std::fs::read_to_string(&path).unwrap()).unwrap();
1082        assert_eq!(raw["showImages"], false);
1083        assert_eq!(raw["showCacheMissNotices"], true);
1084        assert_eq!(raw["outputPad"], 0);
1085        assert_eq!(raw["fullscreenCopyOnSelect"], false);
1086        assert_eq!(raw["httpIdleTimeout"], 30_000);
1087        assert_eq!(raw["showTerminalProgress"], false);
1088
1089        // And they load back.
1090        let reloaded = load_settings().unwrap();
1091        assert_eq!(reloaded.show_images(), Some(false));
1092        assert_eq!(reloaded.show_cache_miss_notices, Some(true));
1093        assert_eq!(reloaded.output_pad, Some(0));
1094        assert_eq!(reloaded.fullscreen_copy_on_select, Some(false));
1095        assert_eq!(reloaded.http_idle_timeout_ms(), Some(30_000));
1096        assert_eq!(reloaded.show_terminal_progress(), Some(false));
1097    }
1098
1099    #[test]
1100    fn nested_terminal_block_is_accepted_as_a_fallback() {
1101        // upstream nests `showImages` / `showTerminalProgress` under
1102        // `terminal`; rpi's flat keys win when both are present.
1103        let (_tmp, _guard) = with_temp_env();
1104        let path = config::settings_path().unwrap();
1105        std::fs::write(
1106            &path,
1107            r#"{ "terminal": { "showImages": false, "showTerminalProgress": false } }"#,
1108        )
1109        .unwrap();
1110        let settings = load_settings().unwrap();
1111        assert_eq!(settings.show_images(), Some(false));
1112        assert_eq!(settings.show_terminal_progress(), Some(false));
1113
1114        std::fs::write(
1115            &path,
1116            r#"{
1117                "showImages": true,
1118                "terminal": { "showImages": false }
1119            }"#,
1120        )
1121        .unwrap();
1122        let settings = load_settings().unwrap();
1123        assert_eq!(settings.show_images(), Some(true));
1124    }
1125
1126    #[test]
1127    fn save_preserves_unknown_fields() {
1128        let (_tmp, _guard) = with_temp_env();
1129        let path = config::settings_path().unwrap();
1130        std::fs::write(
1131            &path,
1132            r#"{
1133                "piOnlyField": "keep-me",
1134                "theme": "dark"
1135            }"#,
1136        )
1137        .unwrap();
1138        let mut s = load_settings().unwrap();
1139        s.scoped_models = Some(vec!["m1".into()]);
1140        save_settings(&s).unwrap();
1141        let raw: serde_json::Value =
1142            serde_json::from_str(&std::fs::read_to_string(&path).unwrap()).unwrap();
1143        assert_eq!(raw["piOnlyField"], "keep-me");
1144        assert_eq!(raw["scopedModels"][0], "m1");
1145        assert_eq!(raw["theme"], "dark");
1146    }
1147
1148    #[test]
1149    fn save_preserves_unknown_fields_and_packages_with_line_comments() {
1150        let (_tmp, _guard) = with_temp_env();
1151        let path = config::settings_path().unwrap();
1152        let original = r#"{
1153            // upstream permits comments in settings files.
1154            "piOnlyField": { "keep": true },
1155            "theme": "dark"
1156        }
1157        "#;
1158        std::fs::write(&path, original).unwrap();
1159
1160        let mut settings = load_settings().unwrap();
1161        settings.scoped_models = Some(vec!["m1".into()]);
1162        save_settings(&settings).unwrap();
1163
1164        // The comments may be normalized away by pretty-printing, but every
1165        // unknown field must survive the merge.
1166        let raw: serde_json::Value =
1167            serde_json::from_str(&std::fs::read_to_string(&path).unwrap()).unwrap();
1168        assert_eq!(raw["piOnlyField"]["keep"], true);
1169        assert_eq!(raw["theme"], "dark");
1170        assert_eq!(raw["scopedModels"][0], "m1");
1171    }
1172
1173    #[test]
1174    fn save_fails_closed_for_unparseable_existing_settings() {
1175        let (_tmp, _guard) = with_temp_env();
1176        let path = config::settings_path().unwrap();
1177        // This is not recoverable by stripping comments (the closing brace is
1178        // missing). Saving must leave the user's file byte-for-byte intact.
1179        let original = b"{\n  // keep this file intact\n  \"piOnlyField\": \"keep-me\"\n";
1180        std::fs::write(&path, original).unwrap();
1181
1182        let mut settings = Settings::default();
1183        settings.theme = Some("light".into());
1184        let error =
1185            save_settings(&settings).expect_err("malformed settings must not be overwritten");
1186        assert!(error.contains("cannot parse existing settings file"));
1187        assert_eq!(std::fs::read(&path).unwrap(), original);
1188    }
1189}