返回 CodeWhale
settings_schema.rs
根目录 / crates / config / src / settings_schema.rs
1 //! One declaration per setting: type, default, and an optional `ui` block.
2 //!
3 //! # What this replaces
4 //!
5 //! This table is the single source that the settings surface projects. It
6 //! replaces, and is the reason for deleting, these hand-maintained key tables
7 //! that used to live beside `ConfigView` in `crates/tui/src/tui/views/mod.rs`:
8 //!
9 //! - the `Vec<ConfigRow>` literal's `section:` field (row → heading),
10 //! and `ConfigCategory::for_section` / `ConfigRowFacts::category`
11 //! (row → rail tab),
12 //! - `config_boolean_key` and `config_integer_key` (key → editor kind),
13 //! - `config_choice_values` (key → enum values),
14 //! - `config_choice_label` / `config_choice_detail` (value → label/detail),
15 //! - `config_label_message` (key → label string),
16 //! - `config_hint_for_key` (key → description string).
17 //!
18 //! # The rule
19 //!
20 //! A setting is declared once, here. `ui: Some(..)` puts it on the settings
21 //! screen; `ui: None` keeps the declaration (type, default, and the fact that
22 //! the key is known) without giving it a row. Visibility is a property of the
23 //! declaration, not of the renderer.
24 //!
25 //! `label`, `description`, and the per-value `label`/`description` are
26 //! *message keys*, not prose: the localization pack owns the text in fifteen
27 //! languages, this table owns which string a setting shows. An empty key means
28 //! "no string" — the surface humanizes the setting key instead.
29 //!
30 //! `tab` and `group` are ids the settings screen resolves to its rail
31 //! categories and section headings. Declaration order is render order:
32 //! distinct tabs appear in the order they are first declared, groups in the
33 //! order they are first declared within a tab, and rows in declaration order
34 //! within a group.
35 //!
36 //! Two settings take their values from a runtime registry rather than this
37 //! table (`theme` from the shipped palettes, `locale` from the shipped packs).
38 //! They are declared `String`; the surface supplies the live value list.
39 //! `reasoning_effort` declares the canonical nine-spelling effort vocabulary
40 //! here so `/config` and `/effort` cannot disagree; the live settings screen
41 //! still narrows that list to the active route's rungs.
42
43 /// One selectable value of a [`SettingKind::Enum`] (or a boolean override).
44 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
45 pub struct SettingOption {
46 pub value: &'static str,
47 /// Message key for the value's label; empty means "show the raw value".
48 pub label: &'static str,
49 /// Message key for the value's one-line detail; empty means none.
50 pub description: &'static str,
51 }
52
53 impl SettingOption {
54 const fn new(value: &'static str, label: &'static str, description: &'static str) -> Self {
55 Self {
56 value,
57 label,
58 description,
59 }
60 }
61 }
62
63 /// The value type of a setting, and for closed value sets, the values.
64 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
65 pub enum SettingKind {
66 /// `true` / `false`. An empty option slice means the surface's default
67 /// on/off labels; a non-empty one overrides them per value.
68 Bool(&'static [SettingOption]),
69 Int,
70 /// A fractional number such as a percent threshold. Values are served and
71 /// accepted in plain decimal form; bounds live in the write validator.
72 Float,
73 Enum(&'static [SettingOption]),
74 String,
75 }
76
77 /// What a declared settings row is. `Setting` is a writable preference;
78 /// `Action` opens another surface (the provider/model pickers, module
79 /// links); `Diagnostic` is a read-only receipt or managed-policy fact;
80 /// `Session` is editable for the running session but does not persist
81 /// through the config store.
82 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
83 pub enum SettingRowKind {
84 Setting,
85 Action,
86 Diagnostic,
87 Session,
88 }
89
90 /// Where a setting appears, and what it says. Absent ⇒ no row.
91 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
92 pub struct SettingUi {
93 pub tab: &'static str,
94 pub group: &'static str,
95 /// Message key for the row label; empty ⇒ humanize the setting key.
96 pub label: &'static str,
97 /// Message key for the row's description sentence.
98 pub description: &'static str,
99 /// Whether the row is a writable preference, a link to another surface,
100 /// a read-only receipt, or a session-scoped override. Surfaces that
101 /// cannot open the target or scope the write render non-`Setting` rows
102 /// read-only instead of guessing.
103 pub row: SettingRowKind,
104 }
105
106 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
107 pub struct SettingDef {
108 pub key: &'static str,
109 pub kind: SettingKind,
110 /// The value in force when nothing is configured, as it is written to
111 /// disk. Empty for session-owned rows, actions, and receipts.
112 pub default: &'static str,
113 pub ui: Option<SettingUi>,
114 }
115
116 impl SettingDef {
117 /// The closed value set, when this setting has one. `None` means the
118 /// surface supplies values (free text, an integer, or a runtime registry).
119 pub fn values(&self) -> Option<Vec<&'static str>> {
120 match self.kind {
121 SettingKind::Bool(_) => Some(vec!["false", "true"]),
122 SettingKind::Enum(options) => Some(options.iter().map(|o| o.value).collect()),
123 SettingKind::Int | SettingKind::String | SettingKind::Float => None,
124 }
125 }
126
127 /// Per-value label/description metadata for `value`, when declared.
128 pub fn option(&self, value: &str) -> Option<&'static SettingOption> {
129 let options = match self.kind {
130 SettingKind::Bool(options) | SettingKind::Enum(options) => options,
131 SettingKind::Int | SettingKind::String | SettingKind::Float => return None,
132 };
133 options.iter().find(|option| option.value == value)
134 }
135
136 pub fn is_bool(&self) -> bool {
137 matches!(self.kind, SettingKind::Bool(_))
138 }
139
140 pub fn is_int(&self) -> bool {
141 matches!(self.kind, SettingKind::Int)
142 }
143
144 pub fn is_float(&self) -> bool {
145 matches!(self.kind, SettingKind::Float)
146 }
147 }
148
149 const NOTIFICATION_SOUNDS: &[SettingOption] = &[
150 SettingOption::new("legacy", "ConfigChoiceNotificationLegacy", ""),
151 SettingOption::new("off", "ConfigValueOff", ""),
152 SettingOption::new("whale", "ConfigChoiceNotificationWhale", ""),
153 SettingOption::new("bell", "ConfigChoiceNotificationBell", ""),
154 SettingOption::new("beep", "ConfigChoiceNotificationBell", ""),
155 SettingOption::new("file", "ConfigChoiceNotificationFile", ""),
156 ];
157
158 const NOTIFICATION_COMPLETION_SOUNDS: &[SettingOption] = &[
159 SettingOption::new("off", "ConfigValueOff", ""),
160 SettingOption::new("whale", "ConfigChoiceNotificationWhale", ""),
161 SettingOption::new("bell", "ConfigChoiceNotificationBell", ""),
162 SettingOption::new("beep", "ConfigChoiceNotificationBell", ""),
163 SettingOption::new("file", "ConfigChoiceNotificationFile", ""),
164 ];
165
166 const NOTIFICATION_CONDITIONS: &[SettingOption] = &[
167 SettingOption::new("always", "ConfigChoiceNotificationAlways", ""),
168 SettingOption::new("unfocused", "ConfigChoiceNotificationUnfocused", ""),
169 SettingOption::new("never", "ConfigChoiceNotificationNever", ""),
170 ];
171
172 const NOTIFICATION_METHODS: &[SettingOption] = &[
173 SettingOption::new("auto", "", ""),
174 SettingOption::new("off", "", ""),
175 SettingOption::new("osc9", "", ""),
176 SettingOption::new("bel", "", ""),
177 SettingOption::new("kitty", "", ""),
178 SettingOption::new("ghostty", "", ""),
179 ];
180
181 const NOTIFICATION_SUBAGENTS: &[SettingOption] = &[
182 SettingOption::new("off", "", ""),
183 SettingOption::new("final-only", "", ""),
184 SettingOption::new("always", "", ""),
185 ];
186
187 const ON_OFF: &[SettingOption] = &[];
188
189 const TELEMETRY: &[SettingOption] = &[
190 SettingOption::new("false", "ConfigValueTelemetryOff", ""),
191 SettingOption::new("true", "ConfigValueTelemetryOn", ""),
192 ];
193
194 const LOW_MOTION: &[SettingOption] = &[
195 SettingOption::new("false", "ConfigValueOff", "ConfigChoiceDetailLowMotionOff"),
196 SettingOption::new("true", "ConfigValueOn", "ConfigChoiceDetailLowMotionOn"),
197 ];
198
199 const FANCY_ANIMATIONS: &[SettingOption] = &[
200 SettingOption::new("false", "ConfigValueOff", "ConfigChoiceDetailFancyOff"),
201 SettingOption::new("true", "ConfigValueOn", "ConfigChoiceDetailFancyOn"),
202 ];
203
204 const SHOW_THINKING: &[SettingOption] = &[
205 SettingOption::new(
206 "false",
207 "ConfigValueOff",
208 "ConfigChoiceDetailShowThinkingOff",
209 ),
210 SettingOption::new("true", "ConfigValueOn", "ConfigChoiceDetailShowThinkingOn"),
211 ];
212
213 const THINKING_HIGHLIGHT: &[SettingOption] = &[
214 SettingOption::new(
215 "false",
216 "ConfigValueOff",
217 "ConfigChoiceDetailThinkingHighlightOff",
218 ),
219 SettingOption::new(
220 "true",
221 "ConfigValueOn",
222 "ConfigChoiceDetailThinkingHighlightOn",
223 ),
224 ];
225
226 const PERMISSION_POSTURE: &[SettingOption] = &[
227 SettingOption::new("ask", "ConfigChoiceAsk", "ConfigChoiceDetailAsk"),
228 SettingOption::new(
229 "auto-review",
230 "ConfigChoiceAutoReview",
231 "ConfigChoiceDetailAutoReview",
232 ),
233 SettingOption::new(
234 "full-access",
235 "ConfigChoiceFullAccess",
236 "ConfigChoiceDetailFullAccess",
237 ),
238 ];
239
240 const APPROVAL_MODE: &[SettingOption] = &[
241 SettingOption::new("ask", "ConfigChoiceAsk", "ConfigChoiceDetailAsk"),
242 SettingOption::new(
243 "auto-review",
244 "ConfigChoiceAutoReview",
245 "ConfigChoiceDetailAutoReview",
246 ),
247 SettingOption::new(
248 "full-access",
249 "ConfigChoiceFullAccess",
250 "ConfigChoiceDetailFullAccess",
251 ),
252 ];
253
254 const APPROVAL_POLICY: &[SettingOption] = &[
255 SettingOption::new(
256 "use-tui-default",
257 "ConfigChoiceUseTuiDefault",
258 "ConfigChoiceDetailUseTuiDefault",
259 ),
260 SettingOption::new("ask", "ConfigChoiceAsk", "ConfigChoiceDetailAsk"),
261 SettingOption::new(
262 "auto-review",
263 "ConfigChoiceAutoReview",
264 "ConfigChoiceDetailAutoReview",
265 ),
266 SettingOption::new(
267 "full-access",
268 "ConfigChoiceFullAccess",
269 "ConfigChoiceDetailFullAccess",
270 ),
271 // `never` is a managed-policy value only: it is accepted from config.toml and
272 // shown read-only, never offered by the editor.
273 ];
274
275 const DEFAULT_MODE: &[SettingOption] = &[
276 SettingOption::new(
277 "agent",
278 "ConfigChoiceModeAct",
279 "ConfigChoiceDetailModeAgent",
280 ),
281 SettingOption::new("plan", "ConfigChoiceModePlan", "ConfigChoiceDetailModePlan"),
282 SettingOption::new(
283 "operate",
284 "ConfigChoiceModeOperate",
285 "ConfigChoiceDetailModeOperate",
286 ),
287 ];
288
289 const FOCUS_TEXTURE: &[SettingOption] = &[
290 SettingOption::new("off", "ConfigValueOff", ""),
291 SettingOption::new("scrim", "", ""),
292 SettingOption::new("grain", "", ""),
293 ];
294
295 const INLINE_DIFFS: &[SettingOption] = &[
296 SettingOption::new("full", "ConfigChoiceDiffFull", ""),
297 SettingOption::new("summary", "ConfigChoiceDiffSummary", ""),
298 SettingOption::new("off", "ConfigValueOff", ""),
299 ];
300
301 const STATUS_INDICATOR: &[SettingOption] = &[
302 // `whale` is retired: load migrates whale | 🐳 | 🐋 to the typographic
303 // mark, so the editor no longer offers it.
304 SettingOption::new("cw", "ConfigChoiceStatusCw", ""),
305 SettingOption::new("dots", "ConfigChoiceStatusDots", ""),
306 SettingOption::new("off", "ConfigValueOff", ""),
307 ];
308
309 const SYNCHRONIZED_OUTPUT: &[SettingOption] = &[
310 SettingOption::new("auto", "", ""),
311 SettingOption::new("on", "", ""),
312 SettingOption::new("off", "", ""),
313 ];
314
315 const COST_CURRENCY: &[SettingOption] = &[
316 SettingOption::new("usd", "", ""),
317 SettingOption::new("cny", "", ""),
318 ];
319
320 /// The canonical `ReasoningEffort::as_setting` spellings, in the order
321 /// `auto, off, minimal, low, medium, high, xhigh, ultra, max`.
322 ///
323 /// This is the same vocabulary `codewhale_tui::reasoning_preference::
324 /// ReasoningEffort::parse_strict` accepts (`/effort`), so the settings schema
325 /// and the command cannot drift. Labels stay empty: an undeclared label means
326 /// "show the raw value", and a capitalized literal would also have to be
327 /// localized in every shipped locale to satisfy the schema message-key check.
328 const REASONING_EFFORT: &[SettingOption] = &[
329 SettingOption::new("auto", "", ""),
330 SettingOption::new("off", "", ""),
331 SettingOption::new("minimal", "", ""),
332 SettingOption::new("low", "", ""),
333 SettingOption::new("medium", "", ""),
334 SettingOption::new("high", "", ""),
335 SettingOption::new("xhigh", "", ""),
336 SettingOption::new("ultra", "", ""),
337 SettingOption::new("max", "", ""),
338 ];
339
340 const DENSITY: &[SettingOption] = &[
341 SettingOption::new("compact", "", ""),
342 SettingOption::new("comfortable", "", ""),
343 SettingOption::new("spacious", "", ""),
344 ];
345
346 const TOOL_COLLAPSE: &[SettingOption] = &[
347 SettingOption::new("compact", "", ""),
348 SettingOption::new("expanded", "", ""),
349 SettingOption::new("calm", "", ""),
350 ];
351
352 const VIM_MODE: &[SettingOption] = &[
353 SettingOption::new("normal", "", ""),
354 SettingOption::new("vim", "", ""),
355 ];
356
357 const MENTION_MENU_BEHAVIOR: &[SettingOption] = &[
358 SettingOption::new("fuzzy", "", ""),
359 SettingOption::new("browser", "", ""),
360 ];
361
362 const WORK_SURFACE_PLACEMENT: &[SettingOption] = &[
363 SettingOption::new(
364 "top",
365 "ConfigChoicePlacementTop",
366 "ConfigChoiceDetailPlacementTop",
367 ),
368 SettingOption::new(
369 "bottom",
370 "ConfigChoicePlacementBottom",
371 "ConfigChoiceDetailPlacementBottom",
372 ),
373 SettingOption::new(
374 "left",
375 "ConfigChoicePlacementLeft",
376 "ConfigChoiceDetailPlacementLeft",
377 ),
378 SettingOption::new(
379 "right",
380 "ConfigChoicePlacementRight",
381 "ConfigChoiceDetailPlacementRight",
382 ),
383 SettingOption::new("off", "ConfigValueOff", "ConfigChoiceDetailPlacementOff"),
384 ];
385
386 const RAIL_PANEL: &[SettingOption] = &[
387 // The dock's own grammar is lowercase nouns, so the panels the classic
388 // sidebar never named ride on their raw value (`RailPanel::title`).
389 SettingOption::new(
390 "tasks",
391 "ConfigChoiceRailTasks",
392 "ConfigChoiceDetailRailTasks",
393 ),
394 SettingOption::new(
395 "agents",
396 "ConfigChoiceRailAgents",
397 "ConfigChoiceDetailRailAgents",
398 ),
399 SettingOption::new("background", "", ""),
400 SettingOption::new("files", "", ""),
401 SettingOption::new("notepad", "", ""),
402 SettingOption::new(
403 "context",
404 "ConfigChoiceRailContext",
405 "ConfigChoiceDetailRailContext",
406 ),
407 SettingOption::new("git", "", ""),
408 SettingOption::new("price", "", ""),
409 ];
410
411 /// Rail tab ids.
412 pub const TAB_APPEARANCE: &str = "appearance";
413 pub const TAB_MODELS: &str = "models";
414 pub const TAB_WORK: &str = "work";
415 pub const TAB_TOOLS: &str = "tools";
416 pub const TAB_TRUST: &str = "trust";
417 pub const TAB_MOTION: &str = "motion";
418 pub const TAB_ADVANCED: &str = "advanced";
419
420 const fn ui(
421 tab: &'static str,
422 group: &'static str,
423 label: &'static str,
424 description: &'static str,
425 ) -> Option<SettingUi> {
426 Some(SettingUi {
427 tab,
428 group,
429 label,
430 description,
431 row: SettingRowKind::Setting,
432 })
433 }
434
435 /// A row that opens another surface rather than editing a value in place.
436 const fn ui_action(
437 tab: &'static str,
438 group: &'static str,
439 label: &'static str,
440 description: &'static str,
441 ) -> Option<SettingUi> {
442 Some(SettingUi {
443 tab,
444 group,
445 label,
446 description,
447 row: SettingRowKind::Action,
448 })
449 }
450
451 /// A read-only receipt row — a managed-policy fact, live route value, or
452 /// descriptive pointer, never a writable control.
453 const fn ui_diagnostic(
454 tab: &'static str,
455 group: &'static str,
456 label: &'static str,
457 description: &'static str,
458 ) -> Option<SettingUi> {
459 Some(SettingUi {
460 tab,
461 group,
462 label,
463 description,
464 row: SettingRowKind::Diagnostic,
465 })
466 }
467
468 /// A row editable for the running session only; it does not persist through
469 /// the config store.
470 const fn ui_session(
471 tab: &'static str,
472 group: &'static str,
473 label: &'static str,
474 description: &'static str,
475 ) -> Option<SettingUi> {
476 Some(SettingUi {
477 tab,
478 group,
479 label,
480 description,
481 row: SettingRowKind::Session,
482 })
483 }
484
485 const fn def(
486 key: &'static str,
487 kind: SettingKind,
488 default: &'static str,
489 ui: Option<SettingUi>,
490 ) -> SettingDef {
491 SettingDef {
492 key,
493 kind,
494 default,
495 ui,
496 }
497 }
498
499 /// Every setting the shell knows about, in render order.
500 pub const SETTINGS_SCHEMA: &[SettingDef] = &[
501 // ── appearance ──────────────────────────────────────────────────────
502 def(
503 "theme",
504 SettingKind::String,
505 "shoreline",
506 ui(
507 TAB_APPEARANCE,
508 "display",
509 "ConfigLabelTheme",
510 // Described by its shipped value list, not prose (`config_hint_for_key`).
511 "",
512 ),
513 ),
514 def(
515 "locale",
516 SettingKind::String,
517 "auto",
518 ui(
519 TAB_APPEARANCE,
520 "display",
521 "ConfigLabelLocale",
522 // Described by its shipped value list, not prose (`config_hint_for_key`).
523 "",
524 ),
525 ),
526 def(
527 "background_color",
528 SettingKind::String,
529 "",
530 ui(
531 TAB_APPEARANCE,
532 "display",
533 "ConfigLabelBackground",
534 "ConfigHintBackgroundColor",
535 ),
536 ),
537 // No sentence anywhere justifies this prototype toggle, so it keeps its
538 // declaration and loses its row.
539 def(
540 "focus_texture",
541 SettingKind::Enum(FOCUS_TEXTURE),
542 "off",
543 None,
544 ),
545 def(
546 "calm_mode",
547 SettingKind::Bool(ON_OFF),
548 "true",
549 ui(
550 TAB_APPEARANCE,
551 "display",
552 "ConfigLabelCalmMode",
553 "ConfigHintCalmMode",
554 ),
555 ),
556 def(
557 "show_thinking",
558 SettingKind::Bool(SHOW_THINKING),
559 "false",
560 ui(
561 TAB_APPEARANCE,
562 "display",
563 "ConfigLabelShowThinking",
564 "ConfigHintShowThinking",
565 ),
566 ),
567 def(
568 "thinking_default_expanded",
569 SettingKind::Bool(ON_OFF),
570 "false",
571 ui(
572 TAB_APPEARANCE,
573 "display",
574 "",
575 "ConfigHintThinkingDefaultExpanded",
576 ),
577 ),
578 def(
579 "thinking_preview_lines",
580 SettingKind::Int,
581 "2",
582 ui(
583 TAB_APPEARANCE,
584 "display",
585 "",
586 "ConfigHintThinkingPreviewLines",
587 ),
588 ),
589 def(
590 "thinking_highlight",
591 SettingKind::Bool(THINKING_HIGHLIGHT),
592 "true",
593 ui(
594 TAB_APPEARANCE,
595 "display",
596 "ConfigLabelThinkingHighlight",
597 "ConfigHintThinkingHighlight",
598 ),
599 ),
600 def(
601 "help_expand_groups",
602 SettingKind::Bool(ON_OFF),
603 "false",
604 ui(TAB_APPEARANCE, "display", "", "ConfigHintHelpExpandGroups"),
605 ),
606 def(
607 "contextual_tips",
608 SettingKind::Bool(ON_OFF),
609 "true",
610 ui(
611 TAB_APPEARANCE,
612 "display",
613 "ConfigLabelContextualTips",
614 "ConfigHintContextualTips",
615 ),
616 ),
617 def(
618 "pin_last_prompt",
619 SettingKind::Bool(ON_OFF),
620 "true",
621 ui(TAB_APPEARANCE, "display", "", "ConfigHintPinLastPrompt"),
622 ),
623 def(
624 "show_tool_details",
625 SettingKind::Bool(ON_OFF),
626 "false",
627 ui(
628 TAB_APPEARANCE,
629 "display",
630 "ConfigLabelShowToolDetails",
631 "ConfigHintBooleanValues",
632 ),
633 ),
634 def(
635 "inline_diffs",
636 SettingKind::Enum(INLINE_DIFFS),
637 "full",
638 ui(
639 TAB_APPEARANCE,
640 "display",
641 "ConfigLabelInlineDiffs",
642 "ConfigHintInlineDiffs",
643 ),
644 ),
645 // No sentence: the glyph set is discoverable from the row's own values.
646 def(
647 "status_indicator",
648 SettingKind::Enum(STATUS_INDICATOR),
649 "cw",
650 None,
651 ),
652 def(
653 "synchronized_output",
654 SettingKind::Enum(SYNCHRONIZED_OUTPUT),
655 "auto",
656 ui(
657 TAB_APPEARANCE,
658 "display",
659 "ConfigLabelSynchronizedOutput",
660 "ConfigHintSynchronizedOutput",
661 ),
662 ),
663 def(
664 "cost_currency",
665 SettingKind::Enum(COST_CURRENCY),
666 "usd",
667 ui(
668 TAB_APPEARANCE,
669 "display",
670 "ConfigLabelCostCurrency",
671 "ConfigHintCostCurrency",
672 ),
673 ),
674 def(
675 "transcript_spacing",
676 SettingKind::Enum(DENSITY),
677 "comfortable",
678 ui(
679 TAB_APPEARANCE,
680 "display",
681 "ConfigLabelTranscriptSpacing",
682 "ConfigHintDensity",
683 ),
684 ),
685 def(
686 "tool_collapse",
687 SettingKind::Enum(TOOL_COLLAPSE),
688 "compact",
689 ui(
690 TAB_APPEARANCE,
691 "display",
692 "ConfigLabelToolCollapse",
693 "ConfigHintToolCollapse",
694 ),
695 ),
696 // ── models & providers ──────────────────────────────────────────────
697 def(
698 "provider",
699 SettingKind::String,
700 "",
701 ui_action(
702 TAB_MODELS,
703 "provider",
704 "ConfigLabelProvider",
705 "ConfigHintProvider",
706 ),
707 ),
708 def(
709 "model",
710 SettingKind::String,
711 "",
712 ui_action(TAB_MODELS, "model", "ConfigLabelModel", "ConfigHintModel"),
713 ),
714 def(
715 "reasoning_effort",
716 SettingKind::Enum(REASONING_EFFORT),
717 "",
718 ui(
719 TAB_MODELS,
720 "model",
721 "ConfigLabelReasoningEffort",
722 "ConfigHintReasoningEffort",
723 ),
724 ),
725 // Sub-agent fan-out depth, next to the model rows that drive it. Fleet
726 // membership itself lives in the /fleet menu, so a one-row Fleet tab
727 // would only restate this table.
728 def(
729 "fleet.exec.max_spawn_depth",
730 SettingKind::Int,
731 "3",
732 ui_diagnostic(
733 TAB_MODELS,
734 "model",
735 "ConfigLabelFleetSpawnDepth",
736 "ConfigHintFleetMaxSpawnDepth",
737 ),
738 ),
739 // ── work ────────────────────────────────────────────────────────────
740 def(
741 "composer_density",
742 SettingKind::Enum(DENSITY),
743 "comfortable",
744 ui(
745 TAB_WORK,
746 "composer",
747 "ConfigLabelComposerDensity",
748 "ConfigHintDensity",
749 ),
750 ),
751 def(
752 "composer_border",
753 SettingKind::Bool(ON_OFF),
754 "true",
755 ui(
756 TAB_WORK,
757 "composer",
758 "ConfigLabelComposerBorder",
759 "ConfigHintBooleanValues",
760 ),
761 ),
762 def(
763 "composer_multiline_mode",
764 SettingKind::Bool(ON_OFF),
765 "false",
766 ui(
767 TAB_WORK,
768 "composer",
769 "ConfigLabelComposerMultilineMode",
770 "ConfigHintComposerMultilineMode",
771 ),
772 ),
773 // Settable via `/set`, surfaced through the composer keymap rather than a
774 // settings row.
775 def(
776 "composer_vim_mode",
777 SettingKind::Enum(VIM_MODE),
778 "normal",
779 None,
780 ),
781 // Terminal protocol toggle; available through `/set` but not given a row.
782 def("bracketed_paste", SettingKind::Bool(ON_OFF), "true", None),
783 def(
784 "paste_burst_detection",
785 SettingKind::Bool(ON_OFF),
786 "true",
787 ui(
788 TAB_WORK,
789 "composer",
790 "ConfigLabelPasteBurstDetection",
791 "ConfigHintBooleanValues",
792 ),
793 ),
794 // Mention-completion tuning knobs; exposed via `/set`, not the settings row.
795 def("mention_menu_limit", SettingKind::Int, "128", None),
796 def(
797 "mention_menu_behavior",
798 SettingKind::Enum(MENTION_MENU_BEHAVIOR),
799 "fuzzy",
800 None,
801 ),
802 def("mention_walk_depth", SettingKind::Int, "10", None),
803 // Workspace discovery option for symlinked layouts; advanced, `/set`-only.
804 def(
805 "workspace_follow_symlinks",
806 SettingKind::Bool(ON_OFF),
807 "false",
808 None,
809 ),
810 def(
811 "work_surface_placement",
812 SettingKind::Enum(WORK_SURFACE_PLACEMENT),
813 "bottom",
814 ui(
815 TAB_WORK,
816 "workbar",
817 "ConfigLabelWorkSurfacePlacement",
818 "ConfigHintWorkSurfacePlacement",
819 ),
820 ),
821 def(
822 "work_surface_top_height",
823 SettingKind::Int,
824 "8",
825 ui(
826 TAB_WORK,
827 "workbar",
828 "ConfigLabelTopHeight",
829 "ConfigHintWorkSurfaceTopHeight",
830 ),
831 ),
832 def(
833 "work_surface_side_width",
834 SettingKind::Int,
835 "30",
836 ui(
837 TAB_WORK,
838 "workbar",
839 "ConfigLabelSideWidth",
840 "ConfigHintWorkSurfaceSideWidth",
841 ),
842 ),
843 def(
844 "rail_panel",
845 SettingKind::Enum(RAIL_PANEL),
846 "tasks",
847 ui(TAB_WORK, "workbar", "", "ConfigHintRailPanel"),
848 ),
849 // Sidebar panel toggles; driven by view actions and startup flags, not rows.
850 def("context_panel", SettingKind::Bool(ON_OFF), "false", None),
851 def("sessions_rail", SettingKind::Bool(ON_OFF), "false", None),
852 def(
853 "session_auto_resume",
854 SettingKind::Bool(ON_OFF),
855 "false",
856 None,
857 ),
858 def(
859 "auto_compact",
860 SettingKind::Bool(ON_OFF),
861 "false",
862 ui(
863 TAB_WORK,
864 "history",
865 "ConfigLabelAutoCompact",
866 "ConfigHintBooleanValues",
867 ),
868 ),
869 def(
870 "auto_compact_threshold_percent",
871 SettingKind::Float,
872 "80",
873 ui(
874 TAB_WORK,
875 "history",
876 "ConfigLabelAutoCompactThreshold",
877 "ConfigHintAutoCompactThreshold",
878 ),
879 ),
880 // A computed receipt from auto_compact + threshold; no user-facing row.
881 def("effective_auto_compact", SettingKind::String, "", None),
882 def(
883 "max_history",
884 SettingKind::Int,
885 "100",
886 ui(
887 TAB_WORK,
888 "history",
889 "ConfigLabelMaxHistory",
890 "ConfigHintMaxHistory",
891 ),
892 ),
893 def(
894 "goal_command",
895 SettingKind::String,
896 "",
897 ui_diagnostic(
898 TAB_WORK,
899 "session",
900 "ConfigLabelGoalCommand",
901 "ConfigHintGoalCommand",
902 ),
903 ),
904 def(
905 "workflow",
906 SettingKind::String,
907 "",
908 ui_diagnostic(
909 TAB_WORK,
910 "workflow",
911 "ConfigLabelWorkflow",
912 "ConfigHintWorkflow",
913 ),
914 ),
915 def(
916 "notifications.quiet",
917 SettingKind::Bool(ON_OFF),
918 "false",
919 ui(
920 TAB_WORK,
921 "workflow",
922 "ConfigLabelNotificationQuiet",
923 "ConfigHintNotificationPolicy",
924 ),
925 ),
926 def(
927 "notifications.sound",
928 SettingKind::Enum(NOTIFICATION_SOUNDS),
929 "legacy",
930 ui(
931 TAB_WORK,
932 "workflow",
933 "ConfigLabelNotificationSound",
934 "ConfigHintNotificationSound",
935 ),
936 ),
937 def(
938 "notifications.condition",
939 SettingKind::Enum(NOTIFICATION_CONDITIONS),
940 "unfocused",
941 ui(
942 TAB_WORK,
943 "workflow",
944 "ConfigLabelNotificationCondition",
945 "ConfigHintNotificationPolicy",
946 ),
947 ),
948 def(
949 "notifications.method",
950 SettingKind::Enum(NOTIFICATION_METHODS),
951 "auto",
952 ui(
953 TAB_WORK,
954 "workflow",
955 "ConfigLabelNotificationMethod",
956 "ConfigHintNotificationPolicy",
957 ),
958 ),
959 def(
960 "notifications.threshold_secs",
961 SettingKind::Int,
962 "30",
963 ui(
964 TAB_WORK,
965 "workflow",
966 "ConfigLabelNotificationThreshold",
967 "ConfigHintNotificationPolicy",
968 ),
969 ),
970 def(
971 "notifications.include_summary",
972 SettingKind::Bool(ON_OFF),
973 "false",
974 ui(
975 TAB_WORK,
976 "workflow",
977 "ConfigLabelNotificationSummary",
978 "ConfigHintNotificationPolicy",
979 ),
980 ),
981 def(
982 "notifications.subagent_completion",
983 SettingKind::Enum(NOTIFICATION_SUBAGENTS),
984 "final-only",
985 ui(
986 TAB_WORK,
987 "workflow",
988 "ConfigLabelNotificationSubagents",
989 "ConfigHintNotificationPolicy",
990 ),
991 ),
992 def(
993 "notifications.events.turn-complete",
994 SettingKind::Bool(ON_OFF),
995 "true",
996 ui(
997 TAB_WORK,
998 "workflow",
999 "ConfigLabelNotificationTurnComplete",
1000 "ConfigHintNotificationPolicy",
1001 ),
1002 ),
1003 def(
1004 "notifications.events.subagent-terminal",
1005 SettingKind::Bool(ON_OFF),
1006 "true",
1007 ui(
1008 TAB_WORK,
1009 "workflow",
1010 "ConfigLabelNotificationSubagentTerminal",
1011 "ConfigHintNotificationPolicy",
1012 ),
1013 ),
1014 def(
1015 "notifications.events.approval-needed",
1016 SettingKind::Bool(ON_OFF),
1017 "true",
1018 ui(
1019 TAB_WORK,
1020 "workflow",
1021 "ConfigLabelNotificationApprovalNeeded",
1022 "ConfigHintNotificationPolicy",
1023 ),
1024 ),
1025 def(
1026 "notifications.events.input-needed",
1027 SettingKind::Bool(ON_OFF),
1028 "true",
1029 ui(
1030 TAB_WORK,
1031 "workflow",
1032 "ConfigLabelNotificationInputNeeded",
1033 "ConfigHintNotificationPolicy",
1034 ),
1035 ),
1036 def(
1037 "notifications.events.elevation-needed",
1038 SettingKind::Bool(ON_OFF),
1039 "true",
1040 ui(
1041 TAB_WORK,
1042 "workflow",
1043 "ConfigLabelNotificationElevationNeeded",
1044 "ConfigHintNotificationPolicy",
1045 ),
1046 ),
1047 def(
1048 "notifications.events.model-notify",
1049 SettingKind::Bool(ON_OFF),
1050 "true",
1051 ui(
1052 TAB_WORK,
1053 "workflow",
1054 "ConfigLabelNotificationModelNotify",
1055 "ConfigHintNotificationPolicy",
1056 ),
1057 ),
1058 def(
1059 "notifications.completion_sound",
1060 SettingKind::Enum(NOTIFICATION_COMPLETION_SOUNDS),
1061 "off",
1062 ui(
1063 TAB_WORK,
1064 "workflow",
1065 "ConfigLabelNotificationCompletionSound",
1066 "ConfigHintNotificationLegacy",
1067 ),
1068 ),
1069 def(
1070 "notifications.sound_file",
1071 SettingKind::String,
1072 "",
1073 ui(
1074 TAB_WORK,
1075 "workflow",
1076 "ConfigLabelNotificationSoundFile",
1077 "ConfigHintNotificationSound",
1078 ),
1079 ),
1080 def(
1081 "notifications.event_sound.enabled",
1082 SettingKind::Bool(ON_OFF),
1083 "false",
1084 ui(
1085 TAB_WORK,
1086 "workflow",
1087 "ConfigLabelNotificationEventSoundEnabled",
1088 "ConfigHintNotificationLegacy",
1089 ),
1090 ),
1091 def(
1092 "notifications.event_sound.events",
1093 SettingKind::String,
1094 "[\"turn-complete\", \"approval-needed\"]",
1095 ui(
1096 TAB_WORK,
1097 "workflow",
1098 "ConfigLabelNotificationEventSoundEvents",
1099 "ConfigHintNotificationLegacy",
1100 ),
1101 ),
1102 def(
1103 "notifications.event_sound.min_interval_ms",
1104 SettingKind::Int,
1105 "2000",
1106 ui(
1107 TAB_WORK,
1108 "workflow",
1109 "ConfigLabelNotificationEventSoundInterval",
1110 "ConfigHintNotificationPolicy",
1111 ),
1112 ),
1113 def(
1114 "notifications.event_sound.quiet",
1115 SettingKind::Bool(ON_OFF),
1116 "false",
1117 ui(
1118 TAB_WORK,
1119 "workflow",
1120 "ConfigLabelNotificationEventSoundQuiet",
1121 "ConfigHintNotificationLegacy",
1122 ),
1123 ),
1124 // ── tools & MCP ─────────────────────────────────────────────────────
1125 def(
1126 "mcp_open",
1127 SettingKind::String,
1128 "",
1129 ui_action(TAB_TOOLS, "mcp", "ConfigLabelMcpOpen", "ConfigHintMcpOpen"),
1130 ),
1131 def(
1132 "mcp_reconnect",
1133 SettingKind::String,
1134 "",
1135 ui_action(
1136 TAB_TOOLS,
1137 "mcp",
1138 "ConfigLabelMcpReconnect",
1139 "ConfigHintMcpReconnect",
1140 ),
1141 ),
1142 def(
1143 "mcp_diagnose",
1144 SettingKind::String,
1145 "",
1146 ui_action(
1147 TAB_TOOLS,
1148 "mcp",
1149 "ConfigLabelMcpDiagnose",
1150 "ConfigHintMcpDiagnose",
1151 ),
1152 ),
1153 def(
1154 "plugins_open",
1155 SettingKind::String,
1156 "",
1157 ui_action(
1158 TAB_TOOLS,
1159 "mcp",
1160 "ConfigLabelPluginsOpen",
1161 "ConfigHintPluginsOpen",
1162 ),
1163 ),
1164 def(
1165 "mcp_config_path",
1166 SettingKind::String,
1167 "",
1168 ui(
1169 TAB_TOOLS,
1170 "mcp",
1171 "ConfigLabelMcpConfigPath",
1172 "ConfigHintMcpConfigPath",
1173 ),
1174 ),
1175 // ── trust ───────────────────────────────────────────────────────────
1176 def(
1177 "sandbox_details",
1178 SettingKind::String,
1179 "",
1180 ui_action(
1181 TAB_TRUST,
1182 "permissions",
1183 "SetupStepTrustSandboxTitle",
1184 "SetupStepTrustSandboxWhy",
1185 ),
1186 ),
1187 def(
1188 "approval_mode",
1189 SettingKind::Enum(APPROVAL_MODE),
1190 "",
1191 ui_session(
1192 TAB_TRUST,
1193 "permissions",
1194 "ConfigLabelApprovalMode",
1195 "ConfigHintApprovalMode",
1196 ),
1197 ),
1198 def(
1199 "permission_posture",
1200 SettingKind::Enum(PERMISSION_POSTURE),
1201 "ask",
1202 ui(
1203 TAB_TRUST,
1204 "permissions",
1205 "ConfigLabelPermissionPosture",
1206 "ConfigHintPermissionPosture",
1207 ),
1208 ),
1209 def(
1210 "approval_policy",
1211 SettingKind::Enum(APPROVAL_POLICY),
1212 "ask",
1213 ui(
1214 TAB_TRUST,
1215 "permissions",
1216 "ConfigLabelApprovalPolicy",
1217 "ConfigHintApprovalPolicy",
1218 ),
1219 ),
1220 def(
1221 "managed_approval_policy",
1222 SettingKind::String,
1223 "",
1224 ui_diagnostic(
1225 TAB_TRUST,
1226 "permissions",
1227 "ConfigLabelManagedApprovalPolicy",
1228 "ConfigHintManagedApprovalPolicy",
1229 ),
1230 ),
1231 def(
1232 "default_mode",
1233 SettingKind::Enum(DEFAULT_MODE),
1234 "agent",
1235 ui(
1236 TAB_TRUST,
1237 "permissions",
1238 "ConfigLabelDefaultMode",
1239 "ConfigHintDefaultMode",
1240 ),
1241 ),
1242 def(
1243 "allow_shell",
1244 SettingKind::Bool(ON_OFF),
1245 "true",
1246 ui(
1247 TAB_TRUST,
1248 "permissions",
1249 "ConfigLabelAllowShell",
1250 "ConfigHintAllowShell",
1251 ),
1252 ),
1253 def(
1254 "managed_allow_shell",
1255 SettingKind::String,
1256 "",
1257 ui_diagnostic(
1258 TAB_TRUST,
1259 "permissions",
1260 "ConfigLabelManagedAllowShell",
1261 "ConfigHintManagedAllowShell",
1262 ),
1263 ),
1264 def(
1265 "telemetry",
1266 SettingKind::Bool(TELEMETRY),
1267 "false",
1268 ui(
1269 TAB_TRUST,
1270 "network",
1271 "ConfigLabelTelemetry",
1272 "ConfigHintTelemetry",
1273 ),
1274 ),
1275 // ── motion ──────────────────────────────────────────────────────────
1276 def(
1277 "low_motion",
1278 SettingKind::Bool(LOW_MOTION),
1279 "false",
1280 ui(
1281 TAB_MOTION,
1282 "display",
1283 "ConfigLabelLowMotion",
1284 "ConfigHintLowMotion",
1285 ),
1286 ),
1287 def(
1288 "fancy_animations",
1289 SettingKind::Bool(FANCY_ANIMATIONS),
1290 "true",
1291 ui(
1292 TAB_MOTION,
1293 "display",
1294 "ConfigLabelFancyAnimations",
1295 "ConfigHintFancyAnimations",
1296 ),
1297 ),
1298 // ── advanced ────────────────────────────────────────────────────────
1299 def(
1300 "base_url",
1301 SettingKind::String,
1302 "",
1303 ui_diagnostic(
1304 TAB_ADVANCED,
1305 "provider",
1306 "ConfigLabelBaseUrlDeepseek",
1307 "ConfigHintBaseUrl",
1308 ),
1309 ),
1310 def(
1311 "provider_url",
1312 SettingKind::String,
1313 "",
1314 ui_diagnostic(
1315 TAB_ADVANCED,
1316 "provider",
1317 "ConfigLabelProviderUrl",
1318 "ConfigHintProviderUrl",
1319 ),
1320 ),
1321 def(
1322 "context_window",
1323 SettingKind::Int,
1324 "",
1325 ui_diagnostic(TAB_ADVANCED, "provider", "", "ConfigHintContextWindow"),
1326 ),
1327 def(
1328 "effective_context_window",
1329 SettingKind::String,
1330 "",
1331 ui_diagnostic(
1332 TAB_ADVANCED,
1333 "provider",
1334 "",
1335 "ConfigHintEffectiveContextWindow",
1336 ),
1337 ),
1338 // Trust receipts: which external credential file a provider may read, and
1339 // under what access. Read-only here; `/provider` owns changing them.
1340 def(
1341 "external_credentials.openai-codex",
1342 SettingKind::String,
1343 "",
1344 ui_diagnostic(
1345 TAB_ADVANCED,
1346 "provider",
1347 "",
1348 "ConfigHintExternalCredentials",
1349 ),
1350 ),
1351 def(
1352 "external_credentials.xai",
1353 SettingKind::String,
1354 "",
1355 ui_diagnostic(
1356 TAB_ADVANCED,
1357 "provider",
1358 "",
1359 "ConfigHintExternalCredentials",
1360 ),
1361 ),
1362 // Derived fast-sibling receipt, retired from the table: the /model picker
1363 // already names the fast sibling where a choice actually happens, and no
1364 // backend reads `fast_model` as a persisted key.
1365 def("fast_model", SettingKind::String, "", None),
1366 // A transport timeout; advanced networking, available through `/set` only.
1367 def("stream_chunk_timeout_secs", SettingKind::Int, "900", None),
1368 // DeepSeek-only legacy fallback: the runtime still reads it, but it is
1369 // not a live choice, so it stays settable through `/set` without a row.
1370 def("default_model", SettingKind::String, "", None),
1371 // Beta vision flag: the feature backend stays live, but the row goes —
1372 // feature state is diagnosed where vision runs, not in Advanced.
1373 def("features.vision_model", SettingKind::String, "", None),
1374 def(
1375 "features.subagents",
1376 SettingKind::String,
1377 "",
1378 ui_diagnostic(
1379 TAB_ADVANCED,
1380 "experimental",
1381 "",
1382 "ConfigHintFeatureSubagents",
1383 ),
1384 ),
1385 def(
1386 "features.web_search",
1387 SettingKind::String,
1388 "",
1389 ui_diagnostic(
1390 TAB_ADVANCED,
1391 "experimental",
1392 "",
1393 "ConfigHintFeatureWebSearch",
1394 ),
1395 ),
1396 def(
1397 "features.apply_patch",
1398 SettingKind::String,
1399 "",
1400 ui_diagnostic(
1401 TAB_ADVANCED,
1402 "experimental",
1403 "",
1404 "ConfigHintFeatureApplyPatch",
1405 ),
1406 ),
1407 def(
1408 "features.mcp",
1409 SettingKind::String,
1410 "",
1411 ui_diagnostic(TAB_ADVANCED, "experimental", "", "ConfigHintFeatureMcp"),
1412 ),
1413 def(
1414 "features.exec_policy",
1415 SettingKind::String,
1416 "",
1417 ui_diagnostic(
1418 TAB_ADVANCED,
1419 "experimental",
1420 "",
1421 "ConfigHintFeatureExecPolicy",
1422 ),
1423 ),
1424 // ── persisted field names without a row ─────────────────────────────
1425 // Every field `Settings` serializes to settings.toml has a declaration;
1426 // these have `ui: None` because their value is owned elsewhere:
1427 // route pickers, `/set` aliases with their own canonical row, or
1428 // internal one-way flags. Declaration order is render order, and hidden
1429 // entries render nothing, so they live together at the end.
1430 //
1431 // Canonical names behind a `/set` alias row: `set()` accepts both
1432 // spellings, the row carries the alias.
1433 def("tool_collapse_mode", SettingKind::String, "compact", None),
1434 def("max_input_history", SettingKind::Int, "100", None),
1435 // Written by the route pickers, not by a settings row (the provider /
1436 // model rows persist to config.toml via `set_config_value`).
1437 def("default_provider", SettingKind::String, "", None),
1438 // Trust posture with a `/set` entry point but no row; surfaced where
1439 // sandboxing acts, not as a settings sentence.
1440 def("sandbox_mode", SettingKind::String, "", None),
1441 // Route memory written by the pickers: per-provider defaults, enabled
1442 // chooser sets, and pinned routes. Structured values no row could edit.
1443 def("provider_models", SettingKind::String, "", None),
1444 def("enabled_models", SettingKind::String, "", None),
1445 def("pinned_models", SettingKind::String, "", None),
1446 // One-way internal flags: shown an intro, shown a deprecation, counted
1447 // tip impressions. Read back to suppress repeats, never edited.
1448 def(
1449 "feature_intro_shown",
1450 SettingKind::Bool(ON_OFF),
1451 "false",
1452 None,
1453 ),
1454 def(
1455 "yolo_deprecation_shown",
1456 SettingKind::Bool(ON_OFF),
1457 "false",
1458 None,
1459 ),
1460 // Round 3 work-bar placement migration ran once (`top` → `bottom`).
1461 def(
1462 "work_surface_bottom_migrated",
1463 SettingKind::Bool(ON_OFF),
1464 "false",
1465 None,
1466 ),
1467 def("behavioral_tip_impressions", SettingKind::String, "", None),
1468 // Footer key-hint use counts: a hint retires to its bare state once its
1469 // binding has been used enough times. Written by the footer, never edited.
1470 def("footer_hint_uses", SettingKind::String, "", None),
1471 ];
1472
1473 /// The declaration for `key`, if the shell knows it.
1474 pub fn setting(key: &str) -> Option<&'static SettingDef> {
1475 SETTINGS_SCHEMA.iter().find(|def| def.key == key)
1476 }
1477
1478 /// Position of `key` in declaration order; `None` for unknown keys.
1479 pub fn setting_index(key: &str) -> Option<usize> {
1480 SETTINGS_SCHEMA.iter().position(|def| def.key == key)
1481 }
1482
1483 /// Distinct `ui.tab` ids, in the order they are first declared.
1484 pub fn schema_tabs() -> Vec<&'static str> {
1485 let mut tabs: Vec<&'static str> = Vec::new();
1486 for ui in SETTINGS_SCHEMA.iter().filter_map(|def| def.ui.as_ref()) {
1487 if !tabs.contains(&ui.tab) {
1488 tabs.push(ui.tab);
1489 }
1490 }
1491 tabs
1492 }
1493
1494 /// Distinct `ui.group` ids within `tab`, in the order they are first declared.
1495 pub fn schema_groups(tab: &str) -> Vec<&'static str> {
1496 let mut groups: Vec<&'static str> = Vec::new();
1497 for ui in SETTINGS_SCHEMA
1498 .iter()
1499 .filter_map(|def| def.ui.as_ref())
1500 .filter(|ui| ui.tab == tab)
1501 {
1502 if !groups.contains(&ui.group) {
1503 groups.push(ui.group);
1504 }
1505 }
1506 groups
1507 }
1508
1509 /// Settings declared with a row, in declaration order.
1510 pub fn schema_rows() -> impl Iterator<Item = &'static SettingDef> {
1511 SETTINGS_SCHEMA.iter().filter(|def| def.ui.is_some())
1512 }
1513
1514 #[cfg(test)]
1515 mod tests {
1516 use super::*;
1517 use std::collections::BTreeSet;
1518
1519 #[test]
1520 fn schema_keys_are_unique() {
1521 let mut seen = BTreeSet::new();
1522 for def in SETTINGS_SCHEMA {
1523 assert!(seen.insert(def.key), "duplicate setting key: {}", def.key);
1524 }
1525 assert_eq!(seen.len(), SETTINGS_SCHEMA.len());
1526 }
1527
1528 #[test]
1529 fn every_row_declares_a_tab_group_and_description() {
1530 for def in schema_rows() {
1531 let ui = def.ui.as_ref().expect("schema_rows filters on ui");
1532 assert!(!ui.tab.is_empty(), "{} has an empty tab", def.key);
1533 assert!(!ui.group.is_empty(), "{} has an empty group", def.key);
1534 // theme and locale are described by their shipped value lists,
1535 // which the TUI composes at render time; no prose key exists.
1536 if matches!(def.key, "theme" | "locale") {
1537 continue;
1538 }
1539 assert!(
1540 !ui.description.is_empty(),
1541 "{} has a row but no sentence justifying it",
1542 def.key
1543 );
1544 }
1545 }
1546
1547 #[test]
1548 fn declaration_order_groups_rows_by_tab_then_group() {
1549 // The surface renders runs of rows; a tab or group that reappears
1550 // after another one would paint two headings with the same name.
1551 let mut seen_tabs: Vec<&str> = Vec::new();
1552 let mut seen_pairs: Vec<(&str, &str)> = Vec::new();
1553 let mut current: Option<(&str, &str)> = None;
1554 for def in schema_rows() {
1555 let ui = def.ui.as_ref().expect("row");
1556 if current.map(|(tab, _)| tab) != Some(ui.tab) {
1557 assert!(!seen_tabs.contains(&ui.tab), "tab {} is split", ui.tab);
1558 seen_tabs.push(ui.tab);
1559 }
1560 if current != Some((ui.tab, ui.group)) {
1561 assert!(
1562 !seen_pairs.contains(&(ui.tab, ui.group)),
1563 "group {}/{} is split",
1564 ui.tab,
1565 ui.group
1566 );
1567 seen_pairs.push((ui.tab, ui.group));
1568 }
1569 current = Some((ui.tab, ui.group));
1570 }
1571 assert_eq!(schema_tabs(), seen_tabs);
1572 }
1573
1574 #[test]
1575 fn enum_values_are_unique_and_defaults_are_declared_values() {
1576 for def in SETTINGS_SCHEMA {
1577 if let Some(values) = def.values() {
1578 let unique: BTreeSet<_> = values.iter().collect();
1579 assert_eq!(unique.len(), values.len(), "{} repeats a value", def.key);
1580 if !def.default.is_empty() {
1581 assert!(
1582 values.contains(&def.default),
1583 "{} defaults to {} which is not one of its values",
1584 def.key,
1585 def.default
1586 );
1587 }
1588 }
1589 }
1590 }
1591 }
1592
1592 lines RUST