1#![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 #[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 #[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 #[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 #[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 #[serde(
102 default,
103 deserialize_with = "floored",
104 skip_serializing_if = "Option::is_none"
105 )]
106 pub editor_padding_x: Option<u8>,
107 #[serde(
109 default,
110 deserialize_with = "whole",
111 skip_serializing_if = "Option::is_none"
112 )]
113 pub output_pad: Option<u8>,
114 #[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 #[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 #[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 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
163fn 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 (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
183fn 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#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
198#[serde(untagged)]
199pub enum BoolOr<T> {
200 Bool(bool),
201 Other(T),
202}
203
204#[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
213fn 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#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
220#[serde(rename_all = "lowercase")]
221pub enum Auto {
222 Auto,
223}
224
225#[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 #[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#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
360#[serde(untagged)]
361pub enum PackageSource {
362 Source(String),
364 Filtered(FilteredPackage),
365}
366
367#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
368pub struct FilteredPackage {
369 pub source: String,
370 #[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 #[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#[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
488impl<'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 #[default]
556 Auto,
557 Always,
558}