Skip to main content

yapi_types/
settings.rs

1//! `settings.json`, global and project scope.
2//!
3//! Mirrors the `Settings` interface in `packages/coding-agent/src/core/settings-manager.ts`
4//! in pi `v1.0.0`. Every field is optional. Defaults belong to the code that reads them.
5//! pi edits this file as a document and keeps unknown keys, so writes go through the
6//! document, not through [`Settings`].
7#![allow(
8    missing_docs,
9    reason = "fields mirror pi's TypeScript types; contracts are noted where they differ from the name"
10)]
11
12use indexmap::IndexMap;
13use serde::{Deserialize, Serialize};
14
15use crate::message::ThinkingLevel;
16
17#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
18#[serde(rename_all = "camelCase")]
19pub struct Settings {
20    #[serde(default, skip_serializing_if = "Option::is_none")]
21    pub last_changelog_version: Option<String>,
22    #[serde(default, skip_serializing_if = "Option::is_none")]
23    pub default_provider: Option<String>,
24    #[serde(default, skip_serializing_if = "Option::is_none")]
25    pub default_model: Option<String>,
26    #[serde(default, skip_serializing_if = "Option::is_none")]
27    pub default_thinking_level: Option<ThinkingLevel>,
28    /// Keyed by `provider/modelId`.
29    #[serde(default, skip_serializing_if = "Option::is_none")]
30    pub model_thinking_levels: Option<IndexMap<String, ThinkingLevel>>,
31    #[serde(default, skip_serializing_if = "Option::is_none")]
32    pub transport: Option<Transport>,
33    #[serde(default, skip_serializing_if = "Option::is_none")]
34    pub steering_mode: Option<QueueMode>,
35    #[serde(default, skip_serializing_if = "Option::is_none")]
36    pub follow_up_mode: Option<QueueMode>,
37    #[serde(default, skip_serializing_if = "Option::is_none")]
38    pub theme: Option<String>,
39    #[serde(default, skip_serializing_if = "Option::is_none")]
40    pub compaction: Option<CompactionSettings>,
41    #[serde(default, skip_serializing_if = "Option::is_none")]
42    pub branch_summary: Option<BranchSummarySettings>,
43    #[serde(default, skip_serializing_if = "Option::is_none")]
44    pub retry: Option<RetrySettings>,
45    #[serde(default, skip_serializing_if = "Option::is_none")]
46    pub hide_thinking_block: Option<bool>,
47    #[serde(default, skip_serializing_if = "Option::is_none")]
48    pub show_cache_miss_notices: Option<bool>,
49    #[serde(default, skip_serializing_if = "Option::is_none")]
50    pub external_editor: Option<String>,
51    #[serde(default, skip_serializing_if = "Option::is_none")]
52    pub shell_path: Option<String>,
53    #[serde(default, skip_serializing_if = "Option::is_none")]
54    pub quiet_startup: Option<BoolOr<Header>>,
55    #[serde(default, skip_serializing_if = "Option::is_none")]
56    pub default_project_trust: Option<DefaultProjectTrust>,
57    #[serde(default, skip_serializing_if = "Option::is_none")]
58    pub shell_command_prefix: Option<String>,
59    /// Command for npm operations, in argv form.
60    #[serde(default, skip_serializing_if = "Option::is_none")]
61    pub npm_command: Option<Vec<String>>,
62    #[serde(default, skip_serializing_if = "Option::is_none")]
63    pub collapse_changelog: Option<bool>,
64    #[serde(default, skip_serializing_if = "Option::is_none")]
65    pub enable_install_telemetry: Option<bool>,
66    #[serde(default, skip_serializing_if = "Option::is_none")]
67    pub enable_analytics: Option<bool>,
68    #[serde(default, skip_serializing_if = "Option::is_none")]
69    pub tracking_id: Option<String>,
70    #[serde(default, skip_serializing_if = "Option::is_none")]
71    pub device_id: Option<String>,
72    #[serde(default, skip_serializing_if = "Option::is_none")]
73    pub packages: Option<Vec<PackageSource>>,
74    /// Paths or patterns. `!`, `+` and `-` prefixes filter.
75    #[serde(default, skip_serializing_if = "Option::is_none")]
76    pub extensions: Option<Vec<String>>,
77    #[serde(default, skip_serializing_if = "Option::is_none")]
78    pub skills: Option<Vec<String>>,
79    #[serde(default, skip_serializing_if = "Option::is_none")]
80    pub prompts: Option<Vec<String>>,
81    #[serde(default, skip_serializing_if = "Option::is_none")]
82    pub themes: Option<Vec<String>>,
83    #[serde(default, skip_serializing_if = "Option::is_none")]
84    pub enable_skill_commands: Option<bool>,
85    #[serde(default, skip_serializing_if = "Option::is_none")]
86    pub terminal: Option<TerminalSettings>,
87    #[serde(default, skip_serializing_if = "Option::is_none")]
88    pub images: Option<ImageSettings>,
89    #[serde(default, skip_serializing_if = "Option::is_none")]
90    pub enabled_models: Option<Vec<String>>,
91    /// Tool names. `+name` and `-name` adjust the inherited list.
92    #[serde(default, skip_serializing_if = "Option::is_none")]
93    pub default_tools: Option<Vec<String>>,
94    #[serde(default, skip_serializing_if = "Option::is_none")]
95    pub double_escape_action: Option<DoubleEscapeAction>,
96    #[serde(default, skip_serializing_if = "Option::is_none")]
97    pub tree_filter_mode: Option<TreeFilterMode>,
98    #[serde(default, skip_serializing_if = "Option::is_none")]
99    pub thinking_budgets: Option<ThinkingBudgets>,
100    /// 0 to 3.
101    #[serde(
102        default,
103        deserialize_with = "floored",
104        skip_serializing_if = "Option::is_none"
105    )]
106    pub editor_padding_x: Option<u8>,
107    /// 0 or 1.
108    #[serde(
109        default,
110        deserialize_with = "whole",
111        skip_serializing_if = "Option::is_none"
112    )]
113    pub output_pad: Option<u8>,
114    /// 3 to 20.
115    #[serde(
116        default,
117        deserialize_with = "floored",
118        skip_serializing_if = "Option::is_none"
119    )]
120    pub autocomplete_max_visible: Option<u8>,
121    #[serde(default, skip_serializing_if = "Option::is_none")]
122    pub show_hardware_cursor: Option<bool>,
123    #[serde(default, skip_serializing_if = "Option::is_none")]
124    pub markdown: Option<MarkdownSettings>,
125    #[serde(default, skip_serializing_if = "Option::is_none")]
126    pub warnings: Option<WarningSettings>,
127    #[serde(default, skip_serializing_if = "Option::is_none")]
128    pub codemode: Option<CodemodeSettings>,
129    #[serde(default, skip_serializing_if = "Option::is_none")]
130    pub session_dir: Option<String>,
131    #[serde(default, skip_serializing_if = "Option::is_none")]
132    pub http_proxy: Option<String>,
133    /// Milliseconds. pi also accepts a numeric string or `"disabled"`.
134    #[serde(default, skip_serializing_if = "Option::is_none")]
135    pub http_idle_timeout_ms: Option<NumberOr<String>>,
136    #[serde(default, skip_serializing_if = "Option::is_none")]
137    pub cache_warming: Option<CacheWarming>,
138    /// Milliseconds. pi also accepts a numeric string or `"disabled"`.
139    #[serde(default, skip_serializing_if = "Option::is_none")]
140    pub websocket_connect_timeout_ms: Option<NumberOr<String>>,
141    #[serde(default, skip_serializing_if = "Option::is_none")]
142    pub tui_mode: Option<TuiMode>,
143    #[serde(default, skip_serializing_if = "Option::is_none")]
144    pub fullscreen_exit_output: Option<FullscreenExitOutput>,
145    #[serde(default, skip_serializing_if = "Option::is_none")]
146    pub fullscreen_scrollbar: Option<Scrollbar>,
147    #[serde(default, skip_serializing_if = "Option::is_none")]
148    pub fullscreen_copy_on_select: Option<bool>,
149    #[serde(default, skip_serializing_if = "Option::is_none")]
150    pub fullscreen_wheel_scroll_lines: Option<NumberOr<Auto>>,
151}
152
153impl Settings {
154    /// pi's `getImageAutoResize`: `images.autoResize`, on when unset.
155    pub fn image_auto_resize(&self) -> bool {
156        self.images
157            .as_ref()
158            .and_then(|images| images.auto_resize)
159            .unwrap_or(true)
160    }
161}
162
163/// A count or size in milliseconds, tokens, cells or lines, as pi reads a
164/// JSON number: a whole float such as `1.5e5` is that integer, and a
165/// fraction is rounded down, as pi's consumers of these settings count.
166fn floored<'de, D, T>(deserializer: D) -> Result<Option<T>, D::Error>
167where
168    D: serde::Deserializer<'de>,
169    T: TryFrom<u64>,
170{
171    let number = serde_json::Number::deserialize(deserializer)?;
172    let whole = number.as_u64().or_else(|| {
173        let value = number.as_f64()?.floor();
174        // `u64::MAX as f64` rounds up to 2^64, which `as u64` would saturate.
175        (value >= 0.0 && value < u64::MAX as f64).then_some(value as u64)
176    });
177    whole
178        .and_then(|whole| T::try_from(whole).ok())
179        .map(Some)
180        .ok_or_else(|| serde::de::Error::custom(format!("invalid count: {number}")))
181}
182
183/// [`floored`] for a value that must be whole: a fraction is invalid.
184fn whole<'de, D, T>(deserializer: D) -> Result<Option<T>, D::Error>
185where
186    D: serde::Deserializer<'de>,
187    T: TryFrom<u64>,
188{
189    let number = serde_json::Number::deserialize(deserializer)?;
190    if number.as_f64().is_some_and(|value| value.fract() != 0.0) {
191        return Err(serde::de::Error::custom(format!("not whole: {number}")));
192    }
193    floored(serde_json::Value::Number(number)).map_err(serde::de::Error::custom)
194}
195
196/// A boolean or a specific alternative value.
197#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
198#[serde(untagged)]
199pub enum BoolOr<T> {
200    Bool(bool),
201    Other(T),
202}
203
204/// A number or a specific alternative value.
205#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
206#[serde(untagged)]
207pub enum NumberOr<T> {
208    #[serde(deserialize_with = "number")]
209    Number(u64),
210    Other(T),
211}
212
213/// [`floored`] for [`NumberOr::Number`].
214fn number<'de, D: serde::Deserializer<'de>>(deserializer: D) -> Result<u64, D::Error> {
215    floored(deserializer)?.ok_or_else(|| serde::de::Error::custom("no number"))
216}
217
218/// The literal `"auto"`.
219#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
220#[serde(rename_all = "lowercase")]
221pub enum Auto {
222    Auto,
223}
224
225/// The literal `"header"`.
226#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
227#[serde(rename_all = "lowercase")]
228pub enum Header {
229    Header,
230}
231
232#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
233#[serde(rename_all = "kebab-case")]
234pub enum Transport {
235    Sse,
236    Websocket,
237    WebsocketCached,
238    Auto,
239}
240
241#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
242#[serde(rename_all = "kebab-case")]
243pub enum QueueMode {
244    All,
245    OneAtATime,
246}
247
248#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
249#[serde(rename_all = "camelCase")]
250pub struct CompactionSettings {
251    #[serde(default, skip_serializing_if = "Option::is_none")]
252    pub enabled: Option<bool>,
253    #[serde(
254        default,
255        deserialize_with = "whole",
256        skip_serializing_if = "Option::is_none"
257    )]
258    pub reserve_tokens: Option<u64>,
259    #[serde(
260        default,
261        deserialize_with = "whole",
262        skip_serializing_if = "Option::is_none"
263    )]
264    pub keep_recent_tokens: Option<u64>,
265    /// Keyed by `provider/modelId`.
266    #[serde(default, skip_serializing_if = "Option::is_none")]
267    pub model_overrides: Option<IndexMap<String, CompactionModelOverride>>,
268}
269
270#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
271#[serde(rename_all = "camelCase")]
272pub struct CompactionModelOverride {
273    #[serde(
274        default,
275        deserialize_with = "whole",
276        skip_serializing_if = "Option::is_none"
277    )]
278    pub reserve_tokens: Option<u64>,
279    #[serde(
280        default,
281        deserialize_with = "whole",
282        skip_serializing_if = "Option::is_none"
283    )]
284    pub keep_recent_tokens: Option<u64>,
285}
286
287#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
288#[serde(rename_all = "camelCase")]
289pub struct BranchSummarySettings {
290    #[serde(
291        default,
292        deserialize_with = "floored",
293        skip_serializing_if = "Option::is_none"
294    )]
295    pub reserve_tokens: Option<u64>,
296    #[serde(default, skip_serializing_if = "Option::is_none")]
297    pub skip_prompt: Option<bool>,
298}
299
300#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
301#[serde(rename_all = "camelCase")]
302pub struct RetrySettings {
303    #[serde(default, skip_serializing_if = "Option::is_none")]
304    pub enabled: Option<bool>,
305    #[serde(
306        default,
307        deserialize_with = "floored",
308        skip_serializing_if = "Option::is_none"
309    )]
310    pub max_retries: Option<u32>,
311    #[serde(
312        default,
313        deserialize_with = "floored",
314        skip_serializing_if = "Option::is_none"
315    )]
316    pub base_delay_ms: Option<u64>,
317    #[serde(
318        default,
319        deserialize_with = "floored",
320        skip_serializing_if = "Option::is_none"
321    )]
322    pub max_agent_delay_ms: Option<u64>,
323    #[serde(default, skip_serializing_if = "Option::is_none")]
324    pub provider: Option<ProviderRetrySettings>,
325}
326
327#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
328#[serde(rename_all = "camelCase")]
329pub struct ProviderRetrySettings {
330    #[serde(
331        default,
332        deserialize_with = "floored",
333        skip_serializing_if = "Option::is_none"
334    )]
335    pub timeout_ms: Option<u64>,
336    #[serde(
337        default,
338        deserialize_with = "floored",
339        skip_serializing_if = "Option::is_none"
340    )]
341    pub max_retries: Option<u32>,
342    #[serde(
343        default,
344        deserialize_with = "floored",
345        skip_serializing_if = "Option::is_none"
346    )]
347    pub max_retry_delay_ms: Option<u64>,
348}
349
350#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
351#[serde(rename_all = "kebab-case")]
352pub enum DefaultProjectTrust {
353    Ask,
354    Always,
355    Never,
356}
357
358/// A package to load: a source string, or a source with resource filters.
359#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
360#[serde(untagged)]
361pub enum PackageSource {
362    /// `npm:`, `git:`, a URL, or a local path.
363    Source(String),
364    Filtered(FilteredPackage),
365}
366
367#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
368pub struct FilteredPackage {
369    pub source: String,
370    /// `false` loads only resources selected by `+` patterns.
371    #[serde(default, skip_serializing_if = "Option::is_none")]
372    pub autoload: Option<bool>,
373    #[serde(default, skip_serializing_if = "Option::is_none")]
374    pub extensions: Option<Vec<String>>,
375    #[serde(default, skip_serializing_if = "Option::is_none")]
376    pub skills: Option<Vec<String>>,
377    #[serde(default, skip_serializing_if = "Option::is_none")]
378    pub prompts: Option<Vec<String>>,
379    #[serde(default, skip_serializing_if = "Option::is_none")]
380    pub themes: Option<Vec<String>>,
381}
382
383#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
384#[serde(rename_all = "camelCase")]
385pub struct TerminalSettings {
386    #[serde(default, skip_serializing_if = "Option::is_none")]
387    pub show_images: Option<bool>,
388    #[serde(
389        default,
390        deserialize_with = "floored",
391        skip_serializing_if = "Option::is_none"
392    )]
393    pub image_width_cells: Option<u32>,
394    #[serde(default, skip_serializing_if = "Option::is_none")]
395    pub clear_on_shrink: Option<bool>,
396    #[serde(default, skip_serializing_if = "Option::is_none")]
397    pub show_terminal_progress: Option<bool>,
398    #[serde(default, skip_serializing_if = "Option::is_none")]
399    pub hyperlinks: Option<BoolOr<Auto>>,
400    /// `false` disables images.
401    #[serde(default, skip_serializing_if = "Option::is_none")]
402    pub images: Option<BoolOr<ImageProtocol>>,
403    #[serde(default, skip_serializing_if = "Option::is_none")]
404    pub true_color: Option<BoolOr<Auto>>,
405}
406
407#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
408#[serde(rename_all = "lowercase")]
409pub enum ImageProtocol {
410    Kitty,
411    Iterm2,
412    Auto,
413}
414
415#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
416#[serde(rename_all = "camelCase")]
417pub struct ImageSettings {
418    #[serde(default, skip_serializing_if = "Option::is_none")]
419    pub auto_resize: Option<bool>,
420    #[serde(default, skip_serializing_if = "Option::is_none")]
421    pub block_images: Option<bool>,
422}
423
424#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
425#[serde(rename_all = "kebab-case")]
426pub enum DoubleEscapeAction {
427    Fork,
428    Tree,
429    None,
430}
431
432#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
433#[serde(rename_all = "kebab-case")]
434pub enum TreeFilterMode {
435    Default,
436    NoTools,
437    UserOnly,
438    LabeledOnly,
439    All,
440}
441
442/// Thinking token budgets for providers that take a budget instead of a level.
443#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
444pub struct ThinkingBudgets {
445    #[serde(
446        default,
447        deserialize_with = "floored",
448        skip_serializing_if = "Option::is_none"
449    )]
450    pub minimal: Option<u64>,
451    #[serde(
452        default,
453        deserialize_with = "floored",
454        skip_serializing_if = "Option::is_none"
455    )]
456    pub low: Option<u64>,
457    #[serde(
458        default,
459        deserialize_with = "floored",
460        skip_serializing_if = "Option::is_none"
461    )]
462    pub medium: Option<u64>,
463    #[serde(
464        default,
465        deserialize_with = "floored",
466        skip_serializing_if = "Option::is_none"
467    )]
468    pub high: Option<u64>,
469}
470
471#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
472#[serde(rename_all = "camelCase")]
473pub struct MarkdownSettings {
474    #[serde(default, skip_serializing_if = "Option::is_none")]
475    pub code_block_indent: Option<String>,
476    #[serde(default, skip_serializing_if = "Option::is_none")]
477    pub mermaid: Option<MermaidMode>,
478}
479
480#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
481#[serde(rename_all = "lowercase")]
482pub enum MermaidMode {
483    Off,
484    Final,
485    Streaming,
486}
487
488/// Any value other than `"off"` or `"final"` reads as `Streaming`, as in pi.
489impl<'de> Deserialize<'de> for MermaidMode {
490    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
491        Ok(
492            match serde_json::Value::deserialize(deserializer)?.as_str() {
493                Some("off") => Self::Off,
494                Some("final") => Self::Final,
495                _ => Self::Streaming,
496            },
497        )
498    }
499}
500
501#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
502#[serde(rename_all = "camelCase")]
503pub struct WarningSettings {
504    #[serde(default, skip_serializing_if = "Option::is_none")]
505    pub anthropic_extra_usage: Option<bool>,
506}
507
508#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
509#[serde(rename_all = "camelCase")]
510pub struct CodemodeSettings {
511    #[serde(default, skip_serializing_if = "Option::is_none")]
512    pub mode: Option<CodemodeMode>,
513    #[serde(
514        default,
515        deserialize_with = "floored",
516        skip_serializing_if = "Option::is_none"
517    )]
518    pub inline_budget: Option<u64>,
519}
520
521#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
522#[serde(rename_all = "lowercase")]
523pub enum CodemodeMode {
524    On,
525    Only,
526}
527
528#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
529#[serde(rename_all = "lowercase")]
530pub enum CacheWarming {
531    Off,
532    Streaming,
533    Idle,
534}
535
536#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
537#[serde(rename_all = "lowercase")]
538pub enum TuiMode {
539    Regular,
540    Fullscreen,
541}
542
543#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
544#[serde(rename_all = "kebab-case")]
545pub enum FullscreenExitOutput {
546    Transcript,
547    ResumeHint,
548}
549
550#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
551#[serde(rename_all = "lowercase")]
552pub enum Scrollbar {
553    Hidden,
554    /// pi's default.
555    #[default]
556    Auto,
557    Always,
558}