pi-one-ui 0.6.1 → 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/CHANGELOG.md +41 -1
  2. package/README.en.md +29 -2
  3. package/README.md +29 -2
  4. package/extensions/app/config/context.ts +42 -0
  5. package/extensions/app/config/editor.ts +80 -0
  6. package/extensions/app/config/footer.ts +291 -0
  7. package/extensions/app/config/panel.ts +1 -5
  8. package/extensions/app/config/renderer.ts +1 -6
  9. package/extensions/app/config/shell.ts +358 -1425
  10. package/extensions/app/config/store.ts +22 -1
  11. package/extensions/app/config/values.ts +33 -0
  12. package/extensions/app/config/working-line.ts +100 -0
  13. package/extensions/app/overlay/input-router.ts +56 -32
  14. package/extensions/app/overlay/overlay-manager.ts +183 -21
  15. package/extensions/app/runtime/event-coordinator.ts +90 -42
  16. package/extensions/{features → app/runtime}/flush-docked-bash.ts +12 -2
  17. package/extensions/app/runtime/render-scheduler.ts +35 -31
  18. package/extensions/app/runtime/tui-runtime.ts +315 -312
  19. package/extensions/app/settings/controller.ts +202 -0
  20. package/extensions/app/settings/panel.ts +81 -0
  21. package/extensions/app/settings/presets.ts +43 -0
  22. package/extensions/features/context-inspector/index.ts +83 -662
  23. package/extensions/layouts/context/index.ts +58 -161
  24. package/extensions/layouts/context/message/user-message.ts +1 -1
  25. package/extensions/layouts/context/renderer/index.ts +31 -13
  26. package/extensions/layouts/context/renderer/mouse/interaction.ts +29 -14
  27. package/extensions/layouts/editor/controller.ts +9 -18
  28. package/extensions/layouts/editor/factory.ts +3 -3
  29. package/extensions/layouts/editor/minimalist-editor.ts +3 -0
  30. package/extensions/layouts/footer/controller.ts +30 -19
  31. package/extensions/layouts/footer/footer.ts +2 -2
  32. package/extensions/layouts/overlay/context-inspector.ts +497 -0
  33. package/extensions/{app → layouts}/overlay/selector-border.ts +5 -5
  34. package/extensions/{app → layouts}/overlay/selector-controller.ts +1 -1
  35. package/extensions/layouts/overlay/settings-panel.ts +359 -0
  36. package/extensions/{app/commands → layouts/overlay}/settings-previews.ts +4 -4
  37. package/extensions/layouts/working-line/controller.ts +3 -29
  38. package/extensions/layouts/working-line/working-line.ts +17 -114
  39. package/extensions/shared/working-line-text.ts +108 -0
  40. package/package.json +1 -1
  41. package/extensions/app/host/pi-extension-port.ts +0 -25
  42. package/extensions/app/host/pi-ui-port.ts +0 -52
  43. package/extensions/app/host/tui-capabilities.ts +0 -27
  44. package/extensions/app/ownership/layout-registry.ts +0 -65
  45. package/extensions/app/ownership.ts +0 -32
  46. package/extensions/app/panel.ts +0 -708
  47. package/extensions/app/presets.ts +0 -35
  48. package/extensions/app/runtime/runtime-state.ts +0 -83
  49. /package/extensions/{layouts/working-line → app/config}/working-line-messages.ts +0 -0
  50. /package/extensions/app/{ownership → runtime}/prototype-patch-registry.ts +0 -0
@@ -1,5 +1,3 @@
1
- import { normalizeWorkingLineMessages } from "../../layouts/working-line/working-line.ts";
2
- import { PI_WORKING_LINE_MESSAGES } from "../../layouts/working-line/working-line-messages.ts";
3
1
  import {
4
2
  ICON_GLYPH_KEYS,
5
3
  type IconGlyphs,
@@ -10,206 +8,81 @@ import {
10
8
  resolveConfiguredIcons,
11
9
  } from "../../shared/icons.ts";
12
10
  import { isSupportedColorSpec } from "../../shared/style.ts";
11
+ import {
12
+ normalizeUserMessages,
13
+ type UserMessagesComponentConfig,
14
+ } from "./context.ts";
15
+ import {
16
+ defaultEditor,
17
+ type EditorBorderColorMode,
18
+ type EditorComponentConfig,
19
+ type EditorStyle,
20
+ type MinimalistConfig,
21
+ type MinimalistEditorStyleConfig,
22
+ type ModelLabelSource,
23
+ normalizeEditor,
24
+ parseEditorBorderColorMode,
25
+ parseEditorModelLabel,
26
+ parseEditorStyle,
27
+ } from "./editor.ts";
28
+ import {
29
+ type CompactFooterMaxLines,
30
+ type ContextStyle,
31
+ type ContextThresholds,
32
+ defaultFooter,
33
+ defaultFooterSegments,
34
+ type ExtensionStatusColorMode,
35
+ type ExtensionStatusPlacement,
36
+ type FooterComponentConfig,
37
+ type FooterSegmentsConfig,
38
+ type GitBranchConfig,
39
+ type GitCommitConfig,
40
+ type GitMetricsConfig,
41
+ isExtensionStatusColorMode,
42
+ isExtensionStatusPlacement,
43
+ normalizeFooter,
44
+ type PathDisplayConfig,
45
+ parseCompactFooterMaxLines,
46
+ parseContextStyle,
47
+ parseSeparatorStyle,
48
+ type SeparatorStyle,
49
+ type StarshipFooterStyleConfig,
50
+ } from "./footer.ts";
13
51
  import {
14
52
  type ConfigRecord,
15
53
  configStore,
16
54
  configPath as defaultConfigPath,
17
55
  mutateConfigFile,
18
56
  } from "./store.ts";
57
+ import { booleanValue, overlayKnown, recordValue } from "./values.ts";
58
+ import {
59
+ defaultWorkingLine,
60
+ normalizeWorkingLine,
61
+ type WorkingLineComponentConfig,
62
+ type WorkingLineComponentPatch,
63
+ } from "./working-line.ts";
19
64
 
20
- export type ColorSpec = string;
21
65
  export type { IconMode } from "../../shared/icons.ts";
22
-
23
- export type ContextStyle = "text" | "gauge" | "text+gauge";
24
- export type SeparatorStyle = "pipe" | "dot" | "chevron" | "none";
25
- export type ModelLabelSource = "id" | "name";
26
- export type EditorStyle = "on" | "off";
27
- export type UserMessageStyle =
28
- | "framed"
29
- | "framed-copy-friendly"
30
- | "compact"
31
- | "labeled";
66
+ export * from "./context.ts";
67
+ export * from "./editor.ts";
68
+ export * from "./footer.ts";
69
+ export * from "./working-line.ts";
70
+ export type ColorSpec = string;
32
71
  export type SelectorBorderStyle = "zentui";
33
- export type FooterStyle = "native" | "starship" | "hidden";
34
- export type WorkingLineSpinner =
35
- | "braille"
36
- | "star-bloom"
37
- | "pinwheel"
38
- | "claude-inspired"
39
- | "pulse";
40
- export type WorkingLineTextAnimation = "classic" | "kitt" | "disabled";
72
+ export type SelectorBordersComponentConfig = {
73
+ enabled: boolean;
74
+ style: SelectorBorderStyle;
75
+ };
41
76
  export type ComponentStyleOwner =
42
77
  | "editor"
43
78
  | "userMessages"
44
79
  | "selectorBorders"
45
80
  | "footer";
46
- export type MinimalistPathDisplayMode = "compact" | "project" | "full";
47
- export type EditorBorderColorMode = "static" | "adaptive";
48
- export type CompactFooterMaxLines = 1 | 2 | 3 | "unlimited";
49
-
50
- export const DEFAULT_COMPACT_FOOTER_FORMAT =
51
- "$cwd$wrap(in $session_name)$wrap(on $git_branch) $git_status$wrap$context$wrap_sep$tokens";
52
-
53
- export type ContextThresholds = {
54
- warning: number;
55
- error: number;
56
- };
57
-
58
- export type PathDisplayMode = "basename" | "full";
59
-
60
- export type PathDisplayConfig = {
61
- mode: PathDisplayMode;
62
- /** Trailing directories to show in full mode. 0 = unlimited; clamped to 0..5. */
63
- depth: number;
64
- };
65
-
66
- export type GitBranchMaxLength = "full" | number;
67
-
68
- export type GitBranchConfig = {
69
- maxLength: GitBranchMaxLength;
70
- };
71
-
72
81
  export type UiFeaturesConfig = {
73
82
  editor: boolean;
74
83
  statusLine: boolean;
75
84
  viewportIndicators: boolean;
76
85
  };
77
-
78
- export type FooterSegmentsConfig = {
79
- cwd: boolean;
80
- sessionName: boolean;
81
- gitBranch: boolean;
82
- gitStatus: boolean;
83
- gitCounts: boolean;
84
- gitCommit: boolean;
85
- gitMetrics: boolean;
86
- runtime: boolean;
87
- modelInfo: boolean;
88
- context: boolean;
89
- tokens: boolean;
90
- cost: boolean;
91
- sessionDuration: boolean;
92
- username: boolean;
93
- time: boolean;
94
- os: boolean;
95
- packageVersion: boolean;
96
- };
97
-
98
- export type MinimalistEditorStyleConfig = {
99
- pathDisplay: MinimalistPathDisplayMode;
100
- showSessionName: boolean;
101
- showTimer: boolean;
102
- showCost: boolean;
103
- showGit: boolean;
104
- };
105
-
106
- /** Temporary name retained for existing settings consumers. */
107
- export type MinimalistConfig = MinimalistEditorStyleConfig;
108
-
109
- export type EditorStylesConfig = {
110
- minimalist: MinimalistEditorStyleConfig;
111
- };
112
-
113
- export type EditorComponentConfig = {
114
- style: EditorStyle;
115
- borderColorMode: EditorBorderColorMode;
116
- modelLabel: ModelLabelSource;
117
- viewportIndicators: boolean;
118
- styles: {
119
- minimalist: MinimalistEditorStyleConfig;
120
- };
121
- };
122
-
123
- export type FramedUserMessageStyleConfig = Record<string, never>;
124
- export type FramedCopyFriendlyUserMessageStyleConfig = Record<string, never>;
125
- export type CompactUserMessageStyleConfig = Record<string, never>;
126
- export type LabeledUserMessageStyleConfig = Record<string, never>;
127
-
128
- export type UserMessagesComponentConfig = {
129
- enabled: boolean;
130
- style: UserMessageStyle;
131
- styles: {
132
- framed: FramedUserMessageStyleConfig;
133
- "framed-copy-friendly": FramedCopyFriendlyUserMessageStyleConfig;
134
- compact: CompactUserMessageStyleConfig;
135
- labeled: LabeledUserMessageStyleConfig;
136
- };
137
- };
138
-
139
- export type SelectorBordersComponentConfig = {
140
- enabled: boolean;
141
- style: SelectorBorderStyle;
142
- };
143
-
144
- export type StarshipFooterStyleConfig = {
145
- format: string;
146
- responsive: boolean;
147
- compactFormat: string;
148
- compactMaxLines: CompactFooterMaxLines;
149
- separator: SeparatorStyle;
150
- contextStyle: ContextStyle;
151
- contextThresholds: ContextThresholds;
152
- pathDisplay: PathDisplayConfig;
153
- segments: FooterSegmentsConfig;
154
- gitBranch: GitBranchConfig;
155
- gitCommit: GitCommitConfig;
156
- gitMetrics: GitMetricsConfig;
157
- extensionStatuses: ExtensionStatusesConfig;
158
- };
159
-
160
- export type FooterComponentConfig = {
161
- style: FooterStyle;
162
- modelLabel: ModelLabelSource;
163
- styles: {
164
- starship: StarshipFooterStyleConfig;
165
- };
166
- };
167
-
168
- export type WorkingLineMessagesConfig = {
169
- custom: boolean;
170
- values: string[];
171
- };
172
-
173
- export type WorkingLineSegmentsConfig = {
174
- tool: boolean;
175
- elapsed: boolean;
176
- thought: boolean;
177
- tokens: boolean;
178
- };
179
-
180
- export const DEFAULT_WORKING_LINE_SPINNER_INTERVAL_MS = 100;
181
- export const DEFAULT_WORKING_LINE_TEXT_INTERVAL_MS = 60;
182
- export const MIN_WORKING_LINE_INTERVAL_MS = 30;
183
- export const MAX_WORKING_LINE_INTERVAL_MS = 1000;
184
-
185
- export function isValidWorkingLineIntervalMs(value: unknown): value is number {
186
- return (
187
- typeof value === "number" &&
188
- Number.isSafeInteger(value) &&
189
- value >= MIN_WORKING_LINE_INTERVAL_MS &&
190
- value <= MAX_WORKING_LINE_INTERVAL_MS
191
- );
192
- }
193
-
194
- export type WorkingLineComponentConfig = {
195
- enabled: boolean;
196
- turnSummary: boolean;
197
- spinner: WorkingLineSpinner;
198
- spinnerIntervalMs: number;
199
- animateSpinnerColor: boolean;
200
- textIntervalMs: number;
201
- textAnimation: WorkingLineTextAnimation;
202
- messages: WorkingLineMessagesConfig;
203
- segments: WorkingLineSegmentsConfig;
204
- };
205
-
206
- export type WorkingLineComponentPatch = Partial<
207
- Omit<WorkingLineComponentConfig, "messages" | "segments">
208
- > & {
209
- messages?: Partial<WorkingLineMessagesConfig>;
210
- segments?: Partial<WorkingLineSegmentsConfig>;
211
- };
212
-
213
86
  export type ComponentsConfig = {
214
87
  editor: EditorComponentConfig;
215
88
  userMessages: UserMessagesComponentConfig;
@@ -217,49 +90,6 @@ export type ComponentsConfig = {
217
90
  selectorBorders: SelectorBordersComponentConfig;
218
91
  footer: FooterComponentConfig;
219
92
  };
220
-
221
- export type ExtensionStatusPlacement = "off" | "left" | "middle" | "right";
222
- export type ExtensionStatusColorMode = "zentui" | "original";
223
-
224
- /**
225
- * Starship `git_commit`-style options.
226
- * See https://starship.rs/config/#git-commit
227
- */
228
- export type GitCommitConfig = {
229
- hashLength: number;
230
- onlyDetached: boolean;
231
- showTag: boolean;
232
- };
233
-
234
- /**
235
- * Starship `git_metrics`-style options.
236
- * See https://starship.rs/config/#git-metrics
237
- */
238
- export type GitMetricsConfig = {
239
- onlyNonzero: boolean;
240
- ignoreSubmodules: boolean;
241
- };
242
-
243
- const DEFAULT_EXTENSION_STATUS_PLACEMENT: ExtensionStatusPlacement = "right";
244
- const DEFAULT_EXTENSION_STATUS_COLOR_MODE: ExtensionStatusColorMode = "zentui";
245
-
246
- export type ExtensionStatusesConfig = {
247
- defaultPlacement: ExtensionStatusPlacement;
248
- placements: Record<string, ExtensionStatusPlacement>;
249
- colorModes: Record<string, ExtensionStatusColorMode>;
250
- };
251
-
252
- const DEFAULT_PROJECT_REFRESH_INTERVAL_MS = 30_000;
253
- const MIN_PROJECT_REFRESH_INTERVAL_MS = 5_000;
254
- export const DEFAULT_EDITOR_METADATA_FORMAT = "$model $provider( $thinking)";
255
-
256
- export type ZentuiConfig = {
257
- projectRefreshIntervalMs: number;
258
- icons: ResolvedIcons;
259
- colors: PolishedTuiColors;
260
- components: ComponentsConfig;
261
- };
262
-
263
93
  export type PolishedTuiColors = {
264
94
  cwd?: ColorSpec;
265
95
  sessionName: ColorSpec;
@@ -297,171 +127,17 @@ export type PolishedTuiColors = {
297
127
  workingLineMid?: ColorSpec;
298
128
  workingLineHigh?: ColorSpec;
299
129
  };
300
-
301
- /** Canonical configuration plus a derived flat runtime view for existing callers. */
302
- export type PolishedTuiConfig = ZentuiConfig & {
303
- footerFormat: string;
304
- responsiveFooter: boolean;
305
- compactFooterFormat: string;
306
- compactFooterMaxLines: CompactFooterMaxLines;
307
- editorMetadataFormat: string;
308
- separator: SeparatorStyle;
309
- contextStyle: ContextStyle;
310
- editorModelLabel: ModelLabelSource;
311
- editorStyle: EditorStyle;
312
- editorStyles: EditorStylesConfig;
313
- editorBorderColorMode: EditorBorderColorMode;
314
- contextThresholds: ContextThresholds;
315
- pathDisplay: PathDisplayConfig;
316
- gitBranch: GitBranchConfig;
317
- features: UiFeaturesConfig;
318
- footerSegments: FooterSegmentsConfig;
319
- gitCommit: GitCommitConfig;
320
- gitMetrics: GitMetricsConfig;
321
- extensionStatuses: ExtensionStatusesConfig;
322
- };
323
-
324
- /**
325
- * Canonical footer format variable names. In a `footerFormat` string these
326
- * are written as `$name` or `${name}`.
327
- */
328
- export const FOOTER_FORMAT_VARIABLES = [
329
- "cwd",
330
- "session_name",
331
- "git_branch",
332
- "git_status",
333
- "git_state",
334
- "runtime",
335
- "model",
336
- "provider",
337
- "session_duration",
338
- "username",
339
- "os",
340
- "time",
341
- "context",
342
- "tokens",
343
- "cache_read",
344
- "cache_write",
345
- "cost",
346
- "subscription",
347
- "auto_compaction",
348
- "package",
349
- "package_version",
350
- "git_commit",
351
- "git_tag",
352
- "git_metrics",
353
- "git_added",
354
- "git_deleted",
355
- "sep",
356
- ] as const;
357
-
358
- /**
359
- * Alias → canonical variable name mapping for `footerFormat`.
360
- * `$fill` is special (not a variable) and handled by the parser.
361
- */
362
- export const FOOTER_FORMAT_ALIASES: Record<string, string> = {
363
- directory: "cwd",
364
- branch: "git_branch",
365
- status: "git_status",
366
- state: "git_state",
367
- commit: "git_commit",
368
- tag: "git_tag",
369
- duration: "session_duration",
370
- separator: "sep",
130
+ export type ZentuiConfig = {
131
+ projectRefreshIntervalMs: number;
132
+ icons: ResolvedIcons;
133
+ colors: PolishedTuiColors;
134
+ components: ComponentsConfig;
371
135
  };
372
-
373
- /** Shared configuration file used by the pi-one-ui package. */
136
+ export type PolishedTuiConfig = ZentuiConfig;
374
137
  export const configPath = defaultConfigPath;
375
138
 
376
- const defaultFooterSegments: FooterSegmentsConfig = {
377
- cwd: true,
378
- sessionName: true,
379
- gitBranch: true,
380
- gitStatus: true,
381
- gitCounts: false,
382
- gitCommit: false,
383
- gitMetrics: false,
384
- runtime: true,
385
- modelInfo: false,
386
- context: true,
387
- tokens: true,
388
- cost: true,
389
- sessionDuration: false,
390
- username: false,
391
- time: false,
392
- os: false,
393
- packageVersion: false,
394
- };
395
-
396
- const defaultMinimalistStyle: MinimalistEditorStyleConfig = {
397
- pathDisplay: "compact",
398
- showSessionName: true,
399
- showTimer: true,
400
- showCost: true,
401
- showGit: true,
402
- };
403
-
404
- const defaultStarshipStyle: StarshipFooterStyleConfig = {
405
- format: "",
406
- responsive: true,
407
- compactFormat: DEFAULT_COMPACT_FOOTER_FORMAT,
408
- compactMaxLines: 2,
409
- separator: "pipe",
410
- contextStyle: "text",
411
- contextThresholds: { warning: 70, error: 90 },
412
- pathDisplay: { mode: "basename", depth: 0 },
413
- segments: defaultFooterSegments,
414
- gitBranch: { maxLength: "full" },
415
- gitCommit: { hashLength: 7, onlyDetached: true, showTag: true },
416
- gitMetrics: { onlyNonzero: true, ignoreSubmodules: false },
417
- extensionStatuses: {
418
- defaultPlacement: "right",
419
- placements: {},
420
- colorModes: {},
421
- },
422
- };
423
-
424
- const defaultComponents: ComponentsConfig = {
425
- editor: {
426
- style: "on",
427
- borderColorMode: "static",
428
- modelLabel: "id",
429
- viewportIndicators: true,
430
- styles: {
431
- minimalist: defaultMinimalistStyle,
432
- },
433
- },
434
- userMessages: {
435
- enabled: true,
436
- style: "framed",
437
- styles: {
438
- framed: {},
439
- "framed-copy-friendly": {},
440
- compact: {},
441
- labeled: {},
442
- },
443
- },
444
- workingLine: {
445
- enabled: false,
446
- turnSummary: true,
447
- spinner: "star-bloom",
448
- spinnerIntervalMs: DEFAULT_WORKING_LINE_SPINNER_INTERVAL_MS,
449
- animateSpinnerColor: false,
450
- textIntervalMs: DEFAULT_WORKING_LINE_TEXT_INTERVAL_MS,
451
- textAnimation: "classic",
452
- messages: { custom: true, values: [...PI_WORKING_LINE_MESSAGES] },
453
- segments: { tool: true, elapsed: true, thought: true, tokens: true },
454
- },
455
- selectorBorders: { enabled: true, style: "zentui" },
456
- footer: {
457
- style: "starship",
458
- modelLabel: "id",
459
- styles: { starship: defaultStarshipStyle },
460
- },
461
- };
462
-
463
- export const defaultConfig: PolishedTuiConfig = {
464
- projectRefreshIntervalMs: DEFAULT_PROJECT_REFRESH_INTERVAL_MS,
139
+ export const defaultConfig: ZentuiConfig = {
140
+ projectRefreshIntervalMs: 30_000,
465
141
  icons: { mode: "auto", ...NERD_DEFAULT_ICONS },
466
142
  colors: {
467
143
  sessionName: "bold green",
@@ -484,702 +160,52 @@ export const defaultConfig: PolishedTuiConfig = {
484
160
  time: "bold yellow",
485
161
  os: "bold white",
486
162
  },
487
- components: defaultComponents,
488
- footerFormat: defaultStarshipStyle.format,
489
- responsiveFooter: defaultStarshipStyle.responsive,
490
- compactFooterFormat: defaultStarshipStyle.compactFormat,
491
- compactFooterMaxLines: defaultStarshipStyle.compactMaxLines,
492
- editorMetadataFormat: DEFAULT_EDITOR_METADATA_FORMAT,
493
- separator: defaultStarshipStyle.separator,
494
- contextStyle: defaultStarshipStyle.contextStyle,
495
- editorModelLabel: defaultComponents.editor.modelLabel,
496
- editorStyle: defaultComponents.editor.style,
497
- editorStyles: { minimalist: defaultMinimalistStyle },
498
- editorBorderColorMode: defaultComponents.editor.borderColorMode,
499
- contextThresholds: defaultStarshipStyle.contextThresholds,
500
- pathDisplay: defaultStarshipStyle.pathDisplay,
501
- gitBranch: defaultStarshipStyle.gitBranch,
502
- features: {
503
- editor: true,
504
- statusLine: true,
505
- viewportIndicators: true,
163
+ components: {
164
+ editor: defaultEditor,
165
+ userMessages: normalizeUserMessages(undefined),
166
+ workingLine: defaultWorkingLine,
167
+ selectorBorders: { enabled: true, style: "zentui" },
168
+ footer: defaultFooter,
506
169
  },
507
- footerSegments: defaultFooterSegments,
508
- gitCommit: defaultStarshipStyle.gitCommit,
509
- gitMetrics: defaultStarshipStyle.gitMetrics,
510
- extensionStatuses: defaultStarshipStyle.extensionStatuses,
511
170
  };
512
171
 
513
- function isRecord(value: unknown): value is ConfigRecord {
514
- return value !== null && typeof value === "object" && !Array.isArray(value);
515
- }
516
-
517
- function parseProjectRefreshIntervalMs(value: unknown): number {
518
- if (value === 0) return 0;
519
- if (typeof value !== "number" || !Number.isFinite(value)) {
520
- return DEFAULT_PROJECT_REFRESH_INTERVAL_MS;
521
- }
522
-
523
- const interval = Math.round(value);
524
- if (interval <= 0) return 0;
525
- return Math.max(MIN_PROJECT_REFRESH_INTERVAL_MS, interval);
526
- }
527
-
528
- function clampPercent(value: number): number {
529
- return Math.max(0, Math.min(100, value));
530
- }
531
-
532
- function parseContextStyle(value: unknown): ContextStyle {
533
- if (value === "text" || value === "gauge" || value === "text+gauge")
534
- return value;
535
- return defaultConfig.contextStyle;
536
- }
537
-
538
- function parseEditorModelLabel(
539
- value: unknown,
540
- fallback: ModelLabelSource = defaultComponents.editor.modelLabel,
541
- ): ModelLabelSource {
542
- if (value === "id" || value === "name") return value;
543
- return fallback;
544
- }
545
-
546
- function parseEditorStyle(value: unknown): EditorStyle {
547
- if (value === "on" || value === "off") return value;
548
- // 迁移:旧 opencode / minimalist 样式统一收敛为 on(minimalist)。
549
- if (value === "opencode" || value === "minimalist") return "on";
550
- return defaultComponents.editor.style;
551
- }
552
-
553
- function parseEditorBorderColorMode(value: unknown): EditorBorderColorMode {
554
- if (value === "static" || value === "adaptive") return value;
555
- return defaultConfig.editorBorderColorMode;
556
- }
557
-
558
- export function isSeparatorStyle(value: unknown): value is SeparatorStyle {
559
- return (
560
- value === "pipe" ||
561
- value === "dot" ||
562
- value === "chevron" ||
563
- value === "none"
564
- );
565
- }
566
-
567
- function parseSeparatorStyle(value: unknown): SeparatorStyle {
568
- return isSeparatorStyle(value) ? value : defaultConfig.separator;
569
- }
570
-
571
- function parseContextThresholds(
572
- value: unknown,
573
- defaults: ContextThresholds = defaultStarshipStyle.contextThresholds,
574
- ): ContextThresholds {
575
- if (!isRecord(value)) return { ...defaults };
576
-
577
- const warningRaw = value.warning;
578
- const errorRaw = value.error;
579
- let warning =
580
- typeof warningRaw === "number" && Number.isFinite(warningRaw)
581
- ? clampPercent(Math.round(warningRaw))
582
- : defaults.warning;
583
- let error =
584
- typeof errorRaw === "number" && Number.isFinite(errorRaw)
585
- ? clampPercent(Math.round(errorRaw))
586
- : defaults.error;
587
- if (error < warning) {
588
- const swapped = warning;
589
- warning = error;
590
- error = swapped;
591
- }
592
- return { warning, error };
593
- }
594
-
595
- function parsePathDisplay(value: unknown): PathDisplayConfig {
596
- const defaults = defaultConfig.pathDisplay;
597
- if (!isRecord(value)) return { ...defaults };
598
- const mode =
599
- value.mode === "full" || value.mode === "basename"
600
- ? value.mode
601
- : defaults.mode;
602
- const rawDepth = value.depth;
603
- const depth =
604
- typeof rawDepth === "number" && Number.isFinite(rawDepth) && rawDepth >= 0
605
- ? Math.min(5, Math.floor(rawDepth))
606
- : defaults.depth;
607
- return { mode, depth };
608
- }
609
-
610
- function normalizeGitBranchMaxLength(value: unknown): GitBranchMaxLength {
611
- if (value === "full") return value;
612
- if (typeof value === "number" && Number.isInteger(value) && value > 0)
613
- return value;
614
- return defaultConfig.gitBranch.maxLength;
615
- }
616
-
617
- function parseGitBranchConfig(value: unknown): GitBranchConfig {
618
- const defaults = defaultConfig.gitBranch;
619
- if (!isRecord(value)) return { ...defaults };
620
- return {
621
- maxLength: normalizeGitBranchMaxLength(value.maxLength),
622
- };
623
- }
624
-
625
- function stringValue(
626
- record: Record<string, unknown>,
627
- key: string,
628
- ): string | undefined {
629
- const value = record[key];
630
- return typeof value === "string" ? value : undefined;
631
- }
632
-
633
- function parseCompactFooterMaxLines(value: unknown): CompactFooterMaxLines {
634
- return value === 1 || value === 2 || value === 3 || value === "unlimited"
635
- ? value
636
- : 2;
637
- }
638
-
639
- function colorValue(
640
- record: Record<string, unknown>,
641
- key: string,
642
- ): string | undefined {
643
- const value = stringValue(record, key);
644
- return value !== undefined && isSupportedColorSpec(value) ? value : undefined;
645
- }
646
-
647
- function definedColors(
648
- colors: Partial<
649
- Record<keyof PolishedTuiConfig["colors"], string | undefined>
650
- >,
651
- ): Partial<PolishedTuiConfig["colors"]> {
652
- return Object.fromEntries(
653
- Object.entries(colors).filter(
654
- (entry): entry is [keyof PolishedTuiConfig["colors"], string] =>
655
- typeof entry[1] === "string",
656
- ),
657
- ) as Partial<PolishedTuiConfig["colors"]>;
658
- }
659
-
660
- function normalizeIconOverrides(
661
- record: Record<string, unknown>,
662
- ): Partial<IconGlyphs> {
663
- return Object.fromEntries(
664
- ICON_GLYPH_KEYS.flatMap((key) => {
665
- const value = stringValue(record, key);
666
- return value === undefined ? [] : [[key, value]];
667
- }),
668
- ) as Partial<IconGlyphs>;
669
- }
670
-
671
- function normalizeColors(
672
- record: Record<string, unknown>,
673
- ): Partial<PolishedTuiConfig["colors"]> {
674
- return definedColors({
675
- cwd: colorValue(record, "cwd"),
676
- sessionName: colorValue(record, "sessionName"),
677
- gitBranch: colorValue(record, "gitBranch"),
678
- gitStatus: colorValue(record, "gitStatus"),
679
- contextNormal: colorValue(record, "contextNormal"),
680
- contextWarning: colorValue(record, "contextWarning"),
681
- contextError: colorValue(record, "contextError"),
682
- tokens: colorValue(record, "tokens"),
683
- cost: colorValue(record, "cost"),
684
- separator: colorValue(record, "separator"),
685
- runtimePrefix: colorValue(record, "runtimePrefix"),
686
- extensionStatus: colorValue(record, "extensionStatus"),
687
- sessionDuration: colorValue(record, "sessionDuration"),
688
- packageVersion: colorValue(record, "packageVersion"),
689
- gitCommit: colorValue(record, "gitCommit"),
690
- gitMetricsAdded: colorValue(record, "gitMetricsAdded"),
691
- gitMetricsDeleted: colorValue(record, "gitMetricsDeleted"),
692
- username: colorValue(record, "username"),
693
- time: colorValue(record, "time"),
694
- os: colorValue(record, "os"),
695
- editorAccent: colorValue(record, "editorAccent"),
696
- editorBorder: colorValue(record, "editorBorder"),
697
- editorGitBranch: colorValue(record, "editorGitBranch"),
698
- editorModel: colorValue(record, "editorModel"),
699
- editorProvider: colorValue(record, "editorProvider"),
700
- editorThinking: colorValue(record, "editorThinking"),
701
- editorThinkingMinimal: colorValue(record, "editorThinkingMinimal"),
702
- editorThinkingLow: colorValue(record, "editorThinkingLow"),
703
- editorThinkingMedium: colorValue(record, "editorThinkingMedium"),
704
- editorThinkingHigh: colorValue(record, "editorThinkingHigh"),
705
- editorThinkingXhigh: colorValue(record, "editorThinkingXhigh"),
706
- editorThinkingMax: colorValue(record, "editorThinkingMax"),
707
- workingLineLow: colorValue(record, "workingLineLow"),
708
- workingLineMid: colorValue(record, "workingLineMid"),
709
- workingLineHigh: colorValue(record, "workingLineHigh"),
710
- });
711
- }
712
-
713
- /** Clamp hashLength to Git's valid abbreviation range [4, 40]. */
714
- function normalizeGitHashLength(value: unknown): number {
715
- const parsed = typeof value === "number" ? value : Number(value);
716
- if (!Number.isFinite(parsed)) return defaultConfig.gitCommit.hashLength;
717
- const rounded = Math.round(parsed);
718
- return Math.min(40, Math.max(4, rounded));
719
- }
720
-
721
- function normalizeGitCommitConfig(
722
- record: Record<string, unknown>,
723
- ): GitCommitConfig {
724
- return {
725
- hashLength: normalizeGitHashLength(record.hashLength),
726
- onlyDetached:
727
- typeof record.onlyDetached === "boolean"
728
- ? record.onlyDetached
729
- : defaultConfig.gitCommit.onlyDetached,
730
- showTag:
731
- typeof record.showTag === "boolean"
732
- ? record.showTag
733
- : defaultConfig.gitCommit.showTag,
734
- };
735
- }
736
-
737
- function normalizeGitMetricsConfig(
738
- record: Record<string, unknown>,
739
- ): GitMetricsConfig {
740
- return {
741
- onlyNonzero:
742
- typeof record.onlyNonzero === "boolean"
743
- ? record.onlyNonzero
744
- : defaultConfig.gitMetrics.onlyNonzero,
745
- ignoreSubmodules:
746
- typeof record.ignoreSubmodules === "boolean"
747
- ? record.ignoreSubmodules
748
- : defaultConfig.gitMetrics.ignoreSubmodules,
749
- };
750
- }
751
-
752
- export function isExtensionStatusPlacement(
753
- value: unknown,
754
- ): value is ExtensionStatusPlacement {
755
- return (
756
- value === "off" ||
757
- value === "left" ||
758
- value === "middle" ||
759
- value === "right"
760
- );
761
- }
762
-
763
- export function isExtensionStatusColorMode(
764
- value: unknown,
765
- ): value is ExtensionStatusColorMode {
766
- return value === "zentui" || value === "original";
767
- }
768
-
769
- function normalizeExtensionStatuses(
770
- record: Record<string, unknown>,
771
- ): ExtensionStatusesConfig {
772
- const defaultPlacement = isExtensionStatusPlacement(record.defaultPlacement)
773
- ? record.defaultPlacement
774
- : defaultConfig.extensionStatuses.defaultPlacement;
775
- const placements = isRecord(record.placements)
776
- ? Object.fromEntries(
777
- Object.entries(record.placements).filter(
778
- (entry): entry is [string, ExtensionStatusPlacement] =>
779
- isExtensionStatusPlacement(entry[1]),
780
- ),
781
- )
782
- : {};
783
- const colorModes = isRecord(record.colorModes)
784
- ? Object.fromEntries(
785
- Object.entries(record.colorModes).filter(
786
- (entry): entry is [string, ExtensionStatusColorMode] =>
787
- isExtensionStatusColorMode(entry[1]),
788
- ),
789
- )
790
- : {};
791
-
792
- return {
793
- defaultPlacement,
794
- placements,
795
- colorModes,
796
- };
797
- }
798
-
799
- function isUiFeatureKey(value: string): value is keyof UiFeaturesConfig {
800
- return (
801
- value === "editor" ||
802
- value === "statusLine" ||
803
- value === "viewportIndicators"
804
- );
805
- }
806
-
807
- function isFooterSegmentKey(
808
- value: string,
809
- ): value is keyof FooterSegmentsConfig {
810
- return (
811
- value === "cwd" ||
812
- value === "sessionName" ||
813
- value === "gitBranch" ||
814
- value === "gitStatus" ||
815
- value === "gitCounts" ||
816
- value === "runtime" ||
817
- value === "modelInfo" ||
818
- value === "context" ||
819
- value === "tokens" ||
820
- value === "cost" ||
821
- value === "sessionDuration" ||
822
- value === "username" ||
823
- value === "time" ||
824
- value === "os" ||
825
- value === "packageVersion" ||
826
- value === "gitCommit" ||
827
- value === "gitMetrics"
828
- );
829
- }
830
-
831
- function validUiFeatureEntries(
832
- record: Record<string, unknown>,
833
- ): Partial<UiFeaturesConfig> {
834
- return Object.fromEntries(
835
- Object.entries(record).filter(
836
- (entry): entry is [keyof UiFeaturesConfig, boolean] => {
837
- const [key, value] = entry;
838
- return isUiFeatureKey(key) && typeof value === "boolean";
839
- },
840
- ),
841
- ) as Partial<UiFeaturesConfig>;
842
- }
843
-
844
- function validFooterSegmentEntries(
845
- record: Record<string, unknown>,
846
- ): Partial<FooterSegmentsConfig> {
847
- return Object.fromEntries(
848
- Object.entries(record).filter(
849
- (entry): entry is [keyof FooterSegmentsConfig, boolean] => {
850
- const [key, value] = entry;
851
- return isFooterSegmentKey(key) && typeof value === "boolean";
852
- },
853
- ),
854
- ) as Partial<FooterSegmentsConfig>;
855
- }
856
-
857
- /** Applies one canonical mutation through the shared config writer. */
858
- function mutateConfig(
859
- path: string,
860
- mutate: (record: ConfigRecord) => void,
861
- ): PolishedTuiConfig {
862
- const update = (record: ConfigRecord): void => {
863
- record.version = 1;
864
- mutate(record);
865
- };
866
- const record =
867
- path === configPath
868
- ? configStore.update(update)
869
- : mutateConfigFile(path, update);
870
- return mergeConfig(record);
871
- }
872
-
873
- export function ensureConfigExists(_path = configPath): void {
874
- // Intentionally left as a no-op. Runtime defaults come from `mergeConfig({})`;
875
- // persist the canonical file only after the user changes a setting.
876
- }
877
-
878
- function hasOwn(record: ConfigRecord, key: string): boolean {
879
- return Object.hasOwn(record, key);
880
- }
881
-
882
- function recordValue(value: unknown): ConfigRecord {
883
- return isRecord(value) ? value : {};
884
- }
885
-
886
- function parseBoolean(value: unknown, fallback: boolean): boolean {
887
- return typeof value === "boolean" ? value : fallback;
888
- }
889
-
890
- function parseSelectorBorderStyle(value: unknown): SelectorBorderStyle {
891
- return value === "zentui" ? value : "zentui";
892
- }
893
-
894
- function parseFooterStyle(value: unknown): FooterStyle {
895
- return value === "native" || value === "starship" || value === "hidden"
896
- ? value
897
- : defaultComponents.footer.style;
898
- }
899
-
900
- function parseUserMessageStyle(value: unknown): UserMessageStyle {
901
- return value === "framed" ||
902
- value === "framed-copy-friendly" ||
903
- value === "compact" ||
904
- value === "labeled"
905
- ? value
906
- : defaultComponents.userMessages.style;
907
- }
908
-
909
- function parseNonEmptyString(value: unknown, fallback: string): string {
910
- return typeof value === "string" && value.length > 0 ? value : fallback;
911
- }
912
-
913
- function resolveContextThresholds(
914
- value: unknown,
915
- defaults: ContextThresholds,
916
- ): ContextThresholds {
917
- return parseContextThresholds(value, defaults);
918
- }
919
-
920
- function resolvePathDisplay(value: unknown): PathDisplayConfig {
921
- return parsePathDisplay(value);
922
- }
923
-
924
- function resolveGitBranch(value: unknown): GitBranchConfig {
925
- return parseGitBranchConfig(value);
926
- }
927
-
928
- function resolveGitCommit(value: unknown): GitCommitConfig {
929
- return normalizeGitCommitConfig(recordValue(value));
930
- }
931
-
932
- function resolveGitMetrics(value: unknown): GitMetricsConfig {
933
- return normalizeGitMetricsConfig(recordValue(value));
934
- }
935
-
936
- function resolveExtensionStatuses(value: unknown): ExtensionStatusesConfig {
937
- return normalizeExtensionStatuses(recordValue(value));
938
- }
939
-
940
- const FOOTER_SEGMENT_KEYS = Object.keys(defaultFooterSegments) as Array<
941
- keyof FooterSegmentsConfig
942
- >;
943
-
944
- function resolveFooterSegments(value: unknown): FooterSegmentsConfig {
945
- const record = recordValue(value);
946
- return Object.fromEntries(
947
- FOOTER_SEGMENT_KEYS.map((key) => [
948
- key,
949
- parseBoolean(record[key], defaultFooterSegments[key]),
950
- ]),
951
- ) as FooterSegmentsConfig;
952
- }
953
-
954
- function resolveWorkingLineMessages(
955
- messages: ConfigRecord,
956
- ): WorkingLineMessagesConfig {
957
- return {
958
- custom: parseBoolean(messages.custom, true),
959
- values: hasOwn(messages, "values")
960
- ? normalizeWorkingLineMessages(messages.values)
961
- : [...PI_WORKING_LINE_MESSAGES],
962
- };
963
- }
964
-
965
- function resolveComponents(config: ConfigRecord): ComponentsConfig {
966
- const components = recordValue(config.components);
967
- const editor = recordValue(components.editor);
968
- const editorStyles = recordValue(editor.styles);
969
- const minimalist = recordValue(editorStyles.minimalist);
970
- const userMessages = recordValue(components.userMessages);
971
- const workingLine = recordValue(components.workingLine);
972
- const workingLineMessages = recordValue(workingLine.messages);
973
- const workingLineSegments = recordValue(workingLine.segments);
974
- const selectorBorders = recordValue(components.selectorBorders);
975
- const footer = recordValue(components.footer);
976
- const footerStyles = recordValue(footer.styles);
977
- const starship = recordValue(footerStyles.starship);
978
-
979
- return {
980
- editor: {
981
- // 迁移:旧 enabled:false 收敛为 style:"off"(enabled 字段已移除)。
982
- style:
983
- hasOwn(editor, "enabled") &&
984
- parseBoolean(editor.enabled, true) === false
985
- ? "off"
986
- : parseEditorStyle(editor.style),
987
- borderColorMode: parseEditorBorderColorMode(editor.borderColorMode),
988
- modelLabel: parseEditorModelLabel(
989
- editor.modelLabel,
990
- defaultComponents.editor.modelLabel,
991
- ),
992
- viewportIndicators: parseBoolean(
993
- editor.viewportIndicators,
994
- defaultComponents.editor.viewportIndicators,
995
- ),
996
- styles: {
997
- minimalist: {
998
- pathDisplay:
999
- minimalist.pathDisplay === "compact" ||
1000
- minimalist.pathDisplay === "project" ||
1001
- minimalist.pathDisplay === "full"
1002
- ? minimalist.pathDisplay
1003
- : defaultMinimalistStyle.pathDisplay,
1004
- showSessionName: parseBoolean(
1005
- minimalist.showSessionName,
1006
- defaultMinimalistStyle.showSessionName,
1007
- ),
1008
- showTimer: parseBoolean(
1009
- minimalist.showTimer,
1010
- defaultMinimalistStyle.showTimer,
1011
- ),
1012
- showCost: parseBoolean(
1013
- minimalist.showCost,
1014
- defaultMinimalistStyle.showCost,
1015
- ),
1016
- showGit: parseBoolean(
1017
- minimalist.showGit,
1018
- defaultMinimalistStyle.showGit,
1019
- ),
1020
- },
1021
- },
1022
- },
1023
- userMessages: {
1024
- enabled: parseBoolean(
1025
- userMessages.enabled,
1026
- defaultComponents.userMessages.enabled,
1027
- ),
1028
- style: parseUserMessageStyle(userMessages.style),
1029
- styles: {
1030
- framed: {},
1031
- "framed-copy-friendly": {},
1032
- compact: {},
1033
- labeled: {},
1034
- },
1035
- },
1036
- workingLine: {
1037
- enabled: parseBoolean(
1038
- workingLine.enabled,
1039
- defaultComponents.workingLine.enabled,
1040
- ),
1041
- turnSummary: parseBoolean(
1042
- workingLine.turnSummary,
1043
- defaultComponents.workingLine.turnSummary,
1044
- ),
1045
- spinner:
1046
- workingLine.spinner === "braille" ||
1047
- workingLine.spinner === "star-bloom" ||
1048
- workingLine.spinner === "pinwheel" ||
1049
- workingLine.spinner === "claude-inspired" ||
1050
- workingLine.spinner === "pulse"
1051
- ? workingLine.spinner
1052
- : defaultComponents.workingLine.spinner,
1053
- spinnerIntervalMs: isValidWorkingLineIntervalMs(
1054
- workingLine.spinnerIntervalMs,
1055
- )
1056
- ? workingLine.spinnerIntervalMs
1057
- : defaultComponents.workingLine.spinnerIntervalMs,
1058
- animateSpinnerColor: parseBoolean(
1059
- workingLine.animateSpinnerColor,
1060
- defaultComponents.workingLine.animateSpinnerColor,
1061
- ),
1062
- textIntervalMs: isValidWorkingLineIntervalMs(workingLine.textIntervalMs)
1063
- ? workingLine.textIntervalMs
1064
- : defaultComponents.workingLine.textIntervalMs,
1065
- textAnimation:
1066
- workingLine.textAnimation === "classic" ||
1067
- workingLine.textAnimation === "kitt" ||
1068
- workingLine.textAnimation === "disabled"
1069
- ? workingLine.textAnimation
1070
- : defaultComponents.workingLine.textAnimation,
1071
- messages: resolveWorkingLineMessages(workingLineMessages),
1072
- segments: {
1073
- tool: parseBoolean(
1074
- workingLineSegments.tool,
1075
- defaultComponents.workingLine.segments.tool,
1076
- ),
1077
- elapsed: parseBoolean(
1078
- workingLineSegments.elapsed,
1079
- defaultComponents.workingLine.segments.elapsed,
1080
- ),
1081
- thought: parseBoolean(
1082
- workingLineSegments.thought,
1083
- defaultComponents.workingLine.segments.thought,
1084
- ),
1085
- tokens: parseBoolean(
1086
- workingLineSegments.tokens,
1087
- defaultComponents.workingLine.segments.tokens,
1088
- ),
1089
- },
1090
- },
1091
- selectorBorders: {
1092
- enabled: parseBoolean(
1093
- selectorBorders.enabled,
1094
- defaultComponents.selectorBorders.enabled,
1095
- ),
1096
- style: parseSelectorBorderStyle(selectorBorders.style),
1097
- },
1098
- footer: {
1099
- style: parseFooterStyle(footer.style),
1100
- modelLabel: parseEditorModelLabel(
1101
- footer.modelLabel,
1102
- defaultComponents.footer.modelLabel,
1103
- ),
1104
- styles: {
1105
- starship: {
1106
- format:
1107
- typeof starship.format === "string"
1108
- ? starship.format
1109
- : defaultStarshipStyle.format,
1110
- responsive: parseBoolean(
1111
- starship.responsive,
1112
- defaultStarshipStyle.responsive,
1113
- ),
1114
- compactFormat: parseNonEmptyString(
1115
- starship.compactFormat,
1116
- defaultStarshipStyle.compactFormat,
1117
- ),
1118
- compactMaxLines: parseCompactFooterMaxLines(starship.compactMaxLines),
1119
- separator: parseSeparatorStyle(starship.separator),
1120
- contextStyle: parseContextStyle(starship.contextStyle),
1121
- contextThresholds: resolveContextThresholds(
1122
- starship.contextThresholds,
1123
- defaultStarshipStyle.contextThresholds,
1124
- ),
1125
- pathDisplay: resolvePathDisplay(starship.pathDisplay),
1126
- segments: resolveFooterSegments(starship.segments),
1127
- gitBranch: resolveGitBranch(starship.gitBranch),
1128
- gitCommit: resolveGitCommit(starship.gitCommit),
1129
- gitMetrics: resolveGitMetrics(starship.gitMetrics),
1130
- extensionStatuses: resolveExtensionStatuses(
1131
- starship.extensionStatuses,
1132
- ),
1133
- },
1134
- },
1135
- },
1136
- };
1137
- }
1138
-
1139
- function derivedRuntimeView(config: ZentuiConfig): PolishedTuiConfig {
1140
- const starship = config.components.footer.styles.starship;
1141
- const minimalist = config.components.editor.styles.minimalist;
1142
- return {
1143
- ...config,
1144
- footerFormat: starship.format,
1145
- responsiveFooter: starship.responsive,
1146
- compactFooterFormat: starship.compactFormat,
1147
- compactFooterMaxLines: starship.compactMaxLines,
1148
- separator: starship.separator,
1149
- contextStyle: starship.contextStyle,
1150
- contextThresholds: starship.contextThresholds,
1151
- pathDisplay: starship.pathDisplay,
1152
- gitBranch: starship.gitBranch,
1153
- footerSegments: starship.segments,
1154
- gitCommit: starship.gitCommit,
1155
- gitMetrics: starship.gitMetrics,
1156
- extensionStatuses: starship.extensionStatuses,
1157
- editorStyle: config.components.editor.style,
1158
- editorStyles: { minimalist },
1159
- editorMetadataFormat: DEFAULT_EDITOR_METADATA_FORMAT,
1160
- editorBorderColorMode: config.components.editor.borderColorMode,
1161
- editorModelLabel: config.components.editor.modelLabel,
1162
- features: {
1163
- editor: config.components.editor.style === "on",
1164
- statusLine: config.components.footer.style === "starship",
1165
- viewportIndicators: config.components.editor.viewportIndicators,
1166
- },
1167
- };
1168
- }
1169
-
1170
- const unsupportedComponentStyles = new WeakMap<
1171
- ZentuiConfig,
1172
- ReadonlySet<ComponentStyleOwner>
1173
- >();
1174
-
1175
- // Retired selections migrate to the sole "on" (minimalist) style instead of
1176
- // disabling the custom Editor. opencode itself is retired along with its variants.
1177
- const retiredEditorStyleIds: ReadonlySet<string> = new Set([
1178
- "opencode",
1179
- "opencode-copy-friendly",
1180
- "accent-rail",
1181
- ]);
1182
-
172
+ const colorKeys: readonly (keyof PolishedTuiColors)[] = [
173
+ "cwd",
174
+ "sessionName",
175
+ "gitBranch",
176
+ "gitStatus",
177
+ "contextNormal",
178
+ "contextWarning",
179
+ "contextError",
180
+ "tokens",
181
+ "cost",
182
+ "separator",
183
+ "runtimePrefix",
184
+ "extensionStatus",
185
+ "sessionDuration",
186
+ "packageVersion",
187
+ "gitCommit",
188
+ "gitMetricsAdded",
189
+ "gitMetricsDeleted",
190
+ "username",
191
+ "time",
192
+ "os",
193
+ "editorAccent",
194
+ "editorBorder",
195
+ "editorGitBranch",
196
+ "editorModel",
197
+ "editorProvider",
198
+ "editorThinking",
199
+ "editorThinkingMinimal",
200
+ "editorThinkingLow",
201
+ "editorThinkingMedium",
202
+ "editorThinkingHigh",
203
+ "editorThinkingXhigh",
204
+ "editorThinkingMax",
205
+ "workingLineLow",
206
+ "workingLineMid",
207
+ "workingLineHigh",
208
+ ];
1183
209
  const knownComponentStyleIds: Record<
1184
210
  ComponentStyleOwner,
1185
211
  ReadonlySet<string>
@@ -1194,14 +220,24 @@ const knownComponentStyleIds: Record<
1194
220
  selectorBorders: new Set(["zentui"]),
1195
221
  footer: new Set(["native", "starship", "hidden"]),
1196
222
  };
223
+ const retiredEditorStyleIds = new Set([
224
+ "opencode",
225
+ "opencode-copy-friendly",
226
+ "accent-rail",
227
+ ]);
228
+ const unsupportedComponentStyles = new WeakMap<
229
+ ZentuiConfig,
230
+ ReadonlySet<ComponentStyleOwner>
231
+ >();
232
+ const styleOwners = Object.keys(
233
+ knownComponentStyleIds,
234
+ ) as ComponentStyleOwner[];
1197
235
 
1198
236
  function unsupportedSelectedStyleId(
1199
237
  record: ConfigRecord,
1200
238
  owner: ComponentStyleOwner,
1201
239
  ): string | undefined {
1202
- const component = recordValue(recordValue(record.components)[owner]);
1203
- if (!hasOwn(component, "style")) return undefined;
1204
- const style = component.style;
240
+ const style = recordValue(recordValue(record.components)[owner]).style;
1205
241
  return typeof style === "string" &&
1206
242
  style.trim() &&
1207
243
  !knownComponentStyleIds[owner].has(style) &&
@@ -1217,38 +253,69 @@ export function hasUnsupportedComponentStyle(
1217
253
  return unsupportedComponentStyles.get(config)?.has(owner) ?? false;
1218
254
  }
1219
255
 
1220
- export function mergeConfig(parsed: unknown): PolishedTuiConfig {
1221
- const config = isRecord(parsed) ? parsed : {};
1222
- const iconsRecord = recordValue(config.icons);
1223
- const colorsRecord = recordValue(config.colors);
1224
- const colors = normalizeColors(colorsRecord);
1225
- const canonical: ZentuiConfig = {
1226
- projectRefreshIntervalMs: parseProjectRefreshIntervalMs(
1227
- config.projectRefreshIntervalMs,
1228
- ),
256
+ function resolveComponents(value: unknown): ComponentsConfig {
257
+ const source = recordValue(value);
258
+ const selector = recordValue(source.selectorBorders);
259
+ return {
260
+ editor: normalizeEditor(source.editor),
261
+ userMessages: normalizeUserMessages(source.userMessages),
262
+ workingLine: normalizeWorkingLine(source.workingLine),
263
+ selectorBorders: {
264
+ enabled: booleanValue(selector.enabled, true),
265
+ style: "zentui",
266
+ },
267
+ footer: normalizeFooter(source.footer),
268
+ };
269
+ }
270
+
271
+ /** 各配置领域在此组合,运行时仅使用 canonical 结构。 */
272
+ export function mergeConfig(parsed: unknown): ZentuiConfig {
273
+ const record = recordValue(parsed);
274
+ const icons = recordValue(record.icons);
275
+ const colors = recordValue(record.colors);
276
+ const interval = record.projectRefreshIntervalMs;
277
+ const config: ZentuiConfig = {
278
+ projectRefreshIntervalMs:
279
+ typeof interval === "number" && Number.isFinite(interval)
280
+ ? Math.round(interval) <= 0
281
+ ? 0
282
+ : Math.max(5_000, Math.round(interval))
283
+ : 30_000,
1229
284
  icons: resolveConfiguredIcons(
1230
- normalizeIconMode(iconsRecord.mode),
1231
- normalizeIconOverrides(iconsRecord),
285
+ normalizeIconMode(icons.mode),
286
+ Object.fromEntries(
287
+ ICON_GLYPH_KEYS.filter((key) => typeof icons[key] === "string").map(
288
+ (key) => [key, icons[key]],
289
+ ),
290
+ ) as Partial<IconGlyphs>,
1232
291
  ),
1233
292
  colors: {
1234
293
  ...defaultConfig.colors,
1235
- ...colors,
294
+ ...Object.fromEntries(
295
+ colorKeys
296
+ .filter(
297
+ (key) =>
298
+ typeof colors[key] === "string" &&
299
+ isSupportedColorSpec(colors[key]),
300
+ )
301
+ .map((key) => [key, colors[key]]),
302
+ ),
1236
303
  },
1237
- components: resolveComponents(config),
304
+ components: resolveComponents(record.components),
1238
305
  };
1239
- const view = derivedRuntimeView(canonical);
1240
- const unsupported = new Set<ComponentStyleOwner>();
1241
- for (const owner of [
1242
- "editor",
1243
- "userMessages",
1244
- "selectorBorders",
1245
- "footer",
1246
- ] as const) {
1247
- if (unsupportedSelectedStyleId(config, owner) !== undefined)
1248
- unsupported.add(owner);
306
+ const unsupported = new Set(
307
+ styleOwners.filter(
308
+ (owner) => unsupportedSelectedStyleId(record, owner) !== undefined,
309
+ ),
310
+ );
311
+ if (unsupported.size > 0) {
312
+ unsupportedComponentStyles.set(config, unsupported);
1249
313
  }
1250
- if (unsupported.size > 0) unsupportedComponentStyles.set(view, unsupported);
1251
- return view;
314
+ return config;
315
+ }
316
+
317
+ export function loadConfig(): ZentuiConfig {
318
+ return mergeConfig(configStore.read());
1252
319
  }
1253
320
 
1254
321
  export function getExtensionStatusPlacement(
@@ -1256,113 +323,78 @@ export function getExtensionStatusPlacement(
1256
323
  key: string,
1257
324
  ): ExtensionStatusPlacement {
1258
325
  const statuses = config.components.footer.styles.starship.extensionStatuses;
1259
- if (Object.hasOwn(statuses.placements, key)) {
1260
- const placement = statuses.placements[key];
1261
- if (isExtensionStatusPlacement(placement)) return placement;
326
+ if (
327
+ Object.hasOwn(statuses.placements, key) &&
328
+ isExtensionStatusPlacement(statuses.placements[key])
329
+ ) {
330
+ return statuses.placements[key];
1262
331
  }
1263
332
  return isExtensionStatusPlacement(statuses.defaultPlacement)
1264
333
  ? statuses.defaultPlacement
1265
- : DEFAULT_EXTENSION_STATUS_PLACEMENT;
334
+ : "right";
1266
335
  }
1267
336
 
1268
337
  export function getExtensionStatusColorMode(
1269
338
  config: ZentuiConfig,
1270
339
  key: string,
1271
340
  ): ExtensionStatusColorMode {
1272
- const colorModes =
341
+ const modes =
1273
342
  config.components.footer.styles.starship.extensionStatuses.colorModes;
1274
- if (Object.hasOwn(colorModes, key)) {
1275
- const colorMode = colorModes[key];
1276
- if (isExtensionStatusColorMode(colorMode)) return colorMode;
1277
- }
1278
- return DEFAULT_EXTENSION_STATUS_COLOR_MODE;
343
+ return Object.hasOwn(modes, key) && isExtensionStatusColorMode(modes[key])
344
+ ? modes[key]
345
+ : "zentui";
1279
346
  }
1280
347
 
1281
- /**
1282
- * Loads and normalizes the shared raw configuration for shell consumers.
1283
- */
1284
- export function loadConfig(): PolishedTuiConfig {
1285
- try {
1286
- return mergeConfig(configStore.read());
1287
- } catch {
1288
- return mergeConfig({});
1289
- }
1290
- }
1291
-
1292
- function overlayKnown(raw: unknown, known: unknown): unknown {
1293
- if (!isRecord(known)) return known;
1294
- const output: ConfigRecord = isRecord(raw) ? { ...raw } : {};
1295
- for (const [key, value] of Object.entries(known)) {
1296
- Object.defineProperty(output, key, {
1297
- value: overlayKnown(output[key], value),
1298
- enumerable: true,
1299
- configurable: true,
1300
- writable: true,
1301
- });
348
+ /** 纯配置修改可在一次 ConfigStore.update 中组合成 Preset。 */
349
+ export function mutateComponentsRecord(
350
+ record: ConfigRecord,
351
+ update: (components: ComponentsConfig) => void,
352
+ replacedStyles: readonly ComponentStyleOwner[] = [],
353
+ ): void {
354
+ const preserved = styleOwners.map(
355
+ (owner) => [owner, unsupportedSelectedStyleId(record, owner)] as const,
356
+ );
357
+ const components = resolveComponents(record.components);
358
+ update(components);
359
+ record.version = 1;
360
+ const merged = recordValue(
361
+ overlayKnown(record.components, resolveComponents(components)),
362
+ );
363
+ for (const [owner, style] of preserved) {
364
+ if (style !== undefined && !replacedStyles.includes(owner)) {
365
+ recordValue(merged[owner]).style = style;
366
+ }
1302
367
  }
1303
- return output;
368
+ record.components = merged;
1304
369
  }
1305
370
 
1306
- type PreservedStyleIds = Partial<Record<ComponentStyleOwner, string>>;
1307
-
1308
- function unknownSelectedStyleIds(record: ConfigRecord): PreservedStyleIds {
1309
- return {
1310
- editor: unsupportedSelectedStyleId(record, "editor"),
1311
- userMessages: unsupportedSelectedStyleId(record, "userMessages"),
1312
- selectorBorders: unsupportedSelectedStyleId(record, "selectorBorders"),
1313
- footer: unsupportedSelectedStyleId(record, "footer"),
371
+ function mutateConfig(
372
+ path: string,
373
+ update: (record: ConfigRecord) => void,
374
+ ): ZentuiConfig {
375
+ const mutate = (record: ConfigRecord) => {
376
+ record.version = 1;
377
+ update(record);
1314
378
  };
1315
- }
1316
-
1317
- function restoreUnknownSelectedStyleIds(
1318
- record: ConfigRecord,
1319
- preserved: PreservedStyleIds,
1320
- replacedStyle?: ComponentStyleOwner,
1321
- ): void {
1322
- const components = recordValue(record.components);
1323
- for (const [owner, style] of Object.entries(preserved) as [
1324
- ComponentStyleOwner,
1325
- string,
1326
- ][]) {
1327
- if (style === undefined || owner === replacedStyle) continue;
1328
- const component = recordValue(components[owner]);
1329
- component.style = style;
1330
- components[owner] = component;
1331
- }
1332
- record.components = components;
379
+ return mergeConfig(
380
+ path === configPath
381
+ ? configStore.update(mutate)
382
+ : mutateConfigFile(path, mutate),
383
+ );
1333
384
  }
1334
385
 
1335
386
  function saveComponentsMutation(
1336
387
  update: (components: ComponentsConfig) => void,
1337
388
  path: string,
1338
389
  replacedStyle?: ComponentStyleOwner,
1339
- ): PolishedTuiConfig {
1340
- return mutateConfig(path, (record) => {
1341
- const preservedStyles = unknownSelectedStyleIds(record);
1342
- const components = mergeConfig(record).components;
1343
- update(components);
1344
- const normalized = resolveComponents({ components });
1345
- record.components = overlayKnown(record.components, normalized);
1346
- restoreUnknownSelectedStyleIds(record, preservedStyles, replacedStyle);
1347
- });
1348
- }
1349
-
1350
- function applyEditorComponentPatch(
1351
- component: EditorComponentConfig,
1352
- patch: Partial<
1353
- Pick<
1354
- EditorComponentConfig,
1355
- "style" | "borderColorMode" | "modelLabel" | "viewportIndicators"
1356
- >
1357
- >,
1358
- ): void {
1359
- if (patch.style !== undefined) component.style = patch.style;
1360
- if (patch.borderColorMode !== undefined)
1361
- component.borderColorMode = patch.borderColorMode;
1362
- if (patch.modelLabel !== undefined) component.modelLabel = patch.modelLabel;
1363
- if (patch.viewportIndicators !== undefined) {
1364
- component.viewportIndicators = patch.viewportIndicators;
1365
- }
390
+ ): ZentuiConfig {
391
+ return mutateConfig(path, (record) =>
392
+ mutateComponentsRecord(
393
+ record,
394
+ update,
395
+ replacedStyle ? [replacedStyle] : [],
396
+ ),
397
+ );
1366
398
  }
1367
399
 
1368
400
  export function saveEditorComponentPatch(
@@ -1373,233 +405,173 @@ export function saveEditorComponentPatch(
1373
405
  >
1374
406
  >,
1375
407
  path = configPath,
1376
- ): PolishedTuiConfig {
408
+ ): ZentuiConfig {
1377
409
  return saveComponentsMutation(
1378
- (components) => applyEditorComponentPatch(components.editor, patch),
410
+ (components) => {
411
+ components.editor = overlayKnown(
412
+ components.editor,
413
+ patch,
414
+ ) as EditorComponentConfig;
415
+ },
1379
416
  path,
1380
417
  patch.style !== undefined ? "editor" : undefined,
1381
418
  );
1382
419
  }
1383
-
1384
- function applyMinimalistStylePatch(
1385
- style: MinimalistEditorStyleConfig,
1386
- patch: Partial<MinimalistEditorStyleConfig>,
1387
- ): void {
1388
- if (patch.pathDisplay !== undefined) style.pathDisplay = patch.pathDisplay;
1389
- if (patch.showSessionName !== undefined)
1390
- style.showSessionName = patch.showSessionName;
1391
- if (patch.showTimer !== undefined) style.showTimer = patch.showTimer;
1392
- if (patch.showCost !== undefined) style.showCost = patch.showCost;
1393
- if (patch.showGit !== undefined) style.showGit = patch.showGit;
1394
- }
1395
-
1396
420
  export function saveMinimalistEditorStylePatch(
1397
421
  patch: Partial<MinimalistEditorStyleConfig>,
1398
422
  path = configPath,
1399
- ): PolishedTuiConfig {
1400
- return saveComponentsMutation(
1401
- (components) =>
1402
- applyMinimalistStylePatch(components.editor.styles.minimalist, patch),
1403
- path,
1404
- );
423
+ ): ZentuiConfig {
424
+ return saveComponentsMutation((components) => {
425
+ components.editor.styles.minimalist = overlayKnown(
426
+ components.editor.styles.minimalist,
427
+ patch,
428
+ ) as MinimalistEditorStyleConfig;
429
+ }, path);
1405
430
  }
1406
-
1407
431
  export function saveUserMessagesComponentPatch(
1408
432
  patch: Partial<Pick<UserMessagesComponentConfig, "enabled" | "style">>,
1409
433
  path = configPath,
1410
- ): PolishedTuiConfig {
434
+ ): ZentuiConfig {
1411
435
  return saveComponentsMutation(
1412
436
  (components) => {
1413
- const component = components.userMessages;
1414
- if (patch.enabled !== undefined) component.enabled = patch.enabled;
1415
- if (patch.style !== undefined) component.style = patch.style;
437
+ components.userMessages = overlayKnown(
438
+ components.userMessages,
439
+ patch,
440
+ ) as UserMessagesComponentConfig;
1416
441
  },
1417
442
  path,
1418
443
  patch.style !== undefined ? "userMessages" : undefined,
1419
444
  );
1420
445
  }
1421
-
1422
446
  export function saveWorkingLineComponentPatch(
1423
447
  patch: WorkingLineComponentPatch,
1424
448
  path = configPath,
1425
- ): PolishedTuiConfig {
449
+ ): ZentuiConfig {
1426
450
  return saveComponentsMutation((components) => {
1427
- const component = components.workingLine;
1428
- if (patch.enabled !== undefined) component.enabled = patch.enabled;
1429
- if (patch.turnSummary !== undefined)
1430
- component.turnSummary = patch.turnSummary;
1431
- if (patch.spinner !== undefined) component.spinner = patch.spinner;
1432
- if (patch.spinnerIntervalMs !== undefined)
1433
- component.spinnerIntervalMs = patch.spinnerIntervalMs;
1434
- if (patch.animateSpinnerColor !== undefined)
1435
- component.animateSpinnerColor = patch.animateSpinnerColor;
1436
- if (patch.textIntervalMs !== undefined)
1437
- component.textIntervalMs = patch.textIntervalMs;
1438
- if (patch.textAnimation !== undefined)
1439
- component.textAnimation = patch.textAnimation;
1440
- if (patch.messages?.custom !== undefined)
1441
- component.messages.custom = patch.messages.custom;
1442
- if (patch.messages?.values !== undefined) {
1443
- component.messages.values = normalizeWorkingLineMessages([
1444
- ...patch.messages.values,
1445
- ]);
1446
- }
1447
- if (patch.segments?.tool !== undefined)
1448
- component.segments.tool = patch.segments.tool;
1449
- if (patch.segments?.elapsed !== undefined)
1450
- component.segments.elapsed = patch.segments.elapsed;
1451
- if (patch.segments?.thought !== undefined)
1452
- component.segments.thought = patch.segments.thought;
1453
- if (patch.segments?.tokens !== undefined)
1454
- component.segments.tokens = patch.segments.tokens;
451
+ components.workingLine = overlayKnown(
452
+ components.workingLine,
453
+ patch,
454
+ ) as WorkingLineComponentConfig;
1455
455
  }, path);
1456
456
  }
1457
-
1458
457
  export function saveSelectorBordersComponentPatch(
1459
458
  patch: Partial<SelectorBordersComponentConfig>,
1460
459
  path = configPath,
1461
- ): PolishedTuiConfig {
460
+ ): ZentuiConfig {
1462
461
  return saveComponentsMutation(
1463
462
  (components) => {
1464
- const component = components.selectorBorders;
1465
- if (patch.enabled !== undefined) component.enabled = patch.enabled;
1466
- if (patch.style !== undefined) component.style = patch.style;
463
+ components.selectorBorders = overlayKnown(
464
+ components.selectorBorders,
465
+ patch,
466
+ ) as SelectorBordersComponentConfig;
1467
467
  },
1468
468
  path,
1469
469
  patch.style !== undefined ? "selectorBorders" : undefined,
1470
470
  );
1471
471
  }
1472
-
1473
472
  export function saveFooterComponentPatch(
1474
473
  patch: Partial<Pick<FooterComponentConfig, "style" | "modelLabel">>,
1475
474
  path = configPath,
1476
- ): PolishedTuiConfig {
475
+ ): ZentuiConfig {
1477
476
  return saveComponentsMutation(
1478
477
  (components) => {
1479
- const component = components.footer;
1480
- if (patch.style !== undefined) component.style = patch.style;
1481
- if (patch.modelLabel !== undefined)
1482
- component.modelLabel = patch.modelLabel;
478
+ components.footer = overlayKnown(
479
+ components.footer,
480
+ patch,
481
+ ) as FooterComponentConfig;
1483
482
  },
1484
483
  path,
1485
484
  patch.style !== undefined ? "footer" : undefined,
1486
485
  );
1487
486
  }
1488
-
1489
- function applyStarshipStylePatch(
1490
- style: StarshipFooterStyleConfig,
1491
- patch: Partial<StarshipFooterStyleConfig>,
1492
- ): void {
1493
- if (patch.format !== undefined) style.format = patch.format;
1494
- if (patch.responsive !== undefined) style.responsive = patch.responsive;
1495
- if (patch.compactFormat !== undefined)
1496
- style.compactFormat = patch.compactFormat;
1497
- if (patch.compactMaxLines !== undefined)
1498
- style.compactMaxLines = patch.compactMaxLines;
1499
- if (patch.separator !== undefined) style.separator = patch.separator;
1500
- if (patch.contextStyle !== undefined) style.contextStyle = patch.contextStyle;
1501
- if (patch.contextThresholds !== undefined) {
1502
- style.contextThresholds = {
1503
- ...style.contextThresholds,
1504
- ...patch.contextThresholds,
1505
- };
1506
- }
1507
- if (patch.pathDisplay !== undefined) {
1508
- style.pathDisplay = { ...style.pathDisplay, ...patch.pathDisplay };
1509
- }
1510
- if (patch.segments !== undefined)
1511
- style.segments = { ...style.segments, ...patch.segments };
1512
- if (patch.gitBranch !== undefined)
1513
- style.gitBranch = { ...style.gitBranch, ...patch.gitBranch };
1514
- if (patch.gitCommit !== undefined)
1515
- style.gitCommit = { ...style.gitCommit, ...patch.gitCommit };
1516
- if (patch.gitMetrics !== undefined)
1517
- style.gitMetrics = { ...style.gitMetrics, ...patch.gitMetrics };
1518
- if (patch.extensionStatuses !== undefined) {
1519
- style.extensionStatuses = {
1520
- ...style.extensionStatuses,
1521
- ...patch.extensionStatuses,
1522
- placements: {
1523
- ...style.extensionStatuses.placements,
1524
- ...patch.extensionStatuses.placements,
1525
- },
1526
- colorModes: {
1527
- ...style.extensionStatuses.colorModes,
1528
- ...patch.extensionStatuses.colorModes,
1529
- },
1530
- };
1531
- }
1532
- }
487
+ export type StarshipFooterStylePatch = Partial<
488
+ Omit<
489
+ StarshipFooterStyleConfig,
490
+ | "contextThresholds"
491
+ | "pathDisplay"
492
+ | "segments"
493
+ | "gitBranch"
494
+ | "gitCommit"
495
+ | "gitMetrics"
496
+ >
497
+ > & {
498
+ contextThresholds?: Partial<ContextThresholds>;
499
+ pathDisplay?: Partial<PathDisplayConfig>;
500
+ segments?: Partial<FooterSegmentsConfig>;
501
+ gitBranch?: Partial<GitBranchConfig>;
502
+ gitCommit?: Partial<GitCommitConfig>;
503
+ gitMetrics?: Partial<GitMetricsConfig>;
504
+ };
1533
505
 
1534
506
  export function saveStarshipFooterStylePatch(
1535
- patch: Partial<StarshipFooterStyleConfig>,
507
+ patch: StarshipFooterStylePatch,
1536
508
  path = configPath,
1537
- ): PolishedTuiConfig {
1538
- return saveComponentsMutation(
1539
- (components) =>
1540
- applyStarshipStylePatch(components.footer.styles.starship, patch),
1541
- path,
1542
- );
509
+ ): ZentuiConfig {
510
+ return saveComponentsMutation((components) => {
511
+ components.footer.styles.starship = overlayKnown(
512
+ components.footer.styles.starship,
513
+ patch,
514
+ ) as StarshipFooterStyleConfig;
515
+ }, path);
1543
516
  }
1544
-
1545
517
  export function saveUiFeaturesPatch(
1546
518
  patch: Partial<UiFeaturesConfig>,
1547
519
  path = configPath,
1548
- ): PolishedTuiConfig {
1549
- const valid = validUiFeatureEntries(patch);
520
+ ): ZentuiConfig {
1550
521
  return saveComponentsMutation(
1551
522
  (components) => {
1552
- if (valid.editor !== undefined) {
1553
- components.editor.style = valid.editor ? "on" : "off";
1554
- components.userMessages.enabled = valid.editor;
1555
- components.selectorBorders.enabled = valid.editor;
523
+ if (typeof patch.editor === "boolean") {
524
+ components.editor.style = patch.editor ? "on" : "off";
525
+ components.userMessages.enabled = patch.editor;
526
+ components.selectorBorders.enabled = patch.editor;
1556
527
  }
1557
- if (valid.statusLine !== undefined) {
1558
- components.footer.style = valid.statusLine ? "starship" : "native";
528
+ if (typeof patch.statusLine === "boolean") {
529
+ components.footer.style = patch.statusLine ? "starship" : "native";
1559
530
  }
1560
- if (valid.viewportIndicators !== undefined) {
1561
- components.editor.viewportIndicators = valid.viewportIndicators;
531
+ if (typeof patch.viewportIndicators === "boolean") {
532
+ components.editor.viewportIndicators = patch.viewportIndicators;
1562
533
  }
1563
534
  },
1564
535
  path,
1565
- valid.statusLine !== undefined ? "footer" : undefined,
536
+ typeof patch.statusLine === "boolean" ? "footer" : undefined,
1566
537
  );
1567
538
  }
1568
-
1569
539
  export function saveFooterSegmentsPatch(
1570
540
  patch: Partial<FooterSegmentsConfig>,
1571
541
  path = configPath,
1572
- ): PolishedTuiConfig {
1573
- return saveStarshipFooterStylePatch(
1574
- { segments: validFooterSegmentEntries(patch) as FooterSegmentsConfig },
1575
- path,
1576
- );
542
+ ): ZentuiConfig {
543
+ const segments = Object.fromEntries(
544
+ Object.entries(patch).filter(
545
+ ([key, value]) =>
546
+ Object.hasOwn(defaultFooterSegments, key) && typeof value === "boolean",
547
+ ),
548
+ ) as FooterSegmentsConfig;
549
+ return saveStarshipFooterStylePatch({ segments }, path);
1577
550
  }
1578
-
1579
551
  export function saveFooterFormatPatch(
1580
552
  value: string,
1581
553
  path = configPath,
1582
- ): PolishedTuiConfig {
554
+ ): ZentuiConfig {
1583
555
  return saveStarshipFooterStylePatch(
1584
556
  { format: typeof value === "string" ? value : "" },
1585
557
  path,
1586
558
  );
1587
559
  }
1588
-
1589
560
  export function saveResponsiveFooterPatch(
1590
- patch: Partial<
1591
- Pick<
1592
- PolishedTuiConfig,
1593
- "responsiveFooter" | "compactFooterFormat" | "compactFooterMaxLines"
1594
- >
1595
- >,
561
+ patch: {
562
+ responsiveFooter?: boolean;
563
+ compactFooterFormat?: string;
564
+ compactFooterMaxLines?: CompactFooterMaxLines;
565
+ },
1596
566
  path = configPath,
1597
- ): PolishedTuiConfig {
567
+ ): ZentuiConfig {
1598
568
  const canonical: Partial<StarshipFooterStyleConfig> = {};
1599
- if (typeof patch.responsiveFooter === "boolean")
569
+ if (typeof patch.responsiveFooter === "boolean") {
1600
570
  canonical.responsive = patch.responsiveFooter;
1601
- if (typeof patch.compactFooterFormat === "string")
571
+ }
572
+ if (typeof patch.compactFooterFormat === "string") {
1602
573
  canonical.compactFormat = patch.compactFooterFormat;
574
+ }
1603
575
  if (patch.compactFooterMaxLines !== undefined) {
1604
576
  canonical.compactMaxLines = parseCompactFooterMaxLines(
1605
577
  patch.compactFooterMaxLines,
@@ -1607,164 +579,126 @@ export function saveResponsiveFooterPatch(
1607
579
  }
1608
580
  return saveStarshipFooterStylePatch(canonical, path);
1609
581
  }
1610
-
1611
582
  export function saveIconsModePatch(
1612
583
  mode: IconMode,
1613
584
  path = configPath,
1614
- ): PolishedTuiConfig {
585
+ ): ZentuiConfig {
1615
586
  return mutateConfig(path, (record) => {
1616
- const existing = isRecord(record.icons) ? { ...record.icons } : {};
1617
- record.icons = { ...existing, mode: normalizeIconMode(mode) };
587
+ record.icons = {
588
+ ...recordValue(record.icons),
589
+ mode: normalizeIconMode(mode),
590
+ };
1618
591
  });
1619
592
  }
1620
-
1621
593
  export function saveContextStylePatch(
1622
594
  style: ContextStyle,
1623
595
  path = configPath,
1624
- ): PolishedTuiConfig {
596
+ ): ZentuiConfig {
1625
597
  return saveStarshipFooterStylePatch(
1626
598
  { contextStyle: parseContextStyle(style) },
1627
599
  path,
1628
600
  );
1629
601
  }
1630
-
1631
602
  export function saveSeparatorPatch(
1632
603
  separator: SeparatorStyle,
1633
604
  path = configPath,
1634
- ): PolishedTuiConfig {
605
+ ): ZentuiConfig {
1635
606
  return saveStarshipFooterStylePatch(
1636
607
  { separator: parseSeparatorStyle(separator) },
1637
608
  path,
1638
609
  );
1639
610
  }
1640
-
1641
611
  export function saveContextThresholdsPatch(
1642
- thresholds: Partial<ContextThresholds>,
612
+ patch: Partial<ContextThresholds>,
1643
613
  path = configPath,
1644
- ): PolishedTuiConfig {
1645
- if (
1646
- typeof thresholds.warning === "bigint" ||
1647
- typeof thresholds.error === "bigint"
1648
- ) {
614
+ ): ZentuiConfig {
615
+ if (typeof patch.warning === "bigint" || typeof patch.error === "bigint") {
1649
616
  throw new TypeError("Context thresholds must be JSON-serializable numbers");
1650
617
  }
1651
- return saveComponentsMutation((components) => {
1652
- components.footer.styles.starship.contextThresholds = {
1653
- ...components.footer.styles.starship.contextThresholds,
1654
- ...thresholds,
1655
- };
1656
- }, path);
618
+ return saveStarshipFooterStylePatch({ contextThresholds: patch }, path);
1657
619
  }
1658
-
1659
620
  export function savePathDisplayPatch(
1660
621
  patch: Partial<PathDisplayConfig>,
1661
622
  path = configPath,
1662
- ): PolishedTuiConfig {
1663
- return saveComponentsMutation((components) => {
1664
- components.footer.styles.starship.pathDisplay = {
1665
- ...components.footer.styles.starship.pathDisplay,
1666
- ...patch,
1667
- };
1668
- }, path);
623
+ ): ZentuiConfig {
624
+ return saveStarshipFooterStylePatch({ pathDisplay: patch }, path);
1669
625
  }
1670
-
1671
626
  export function saveGitBranchPatch(
1672
627
  patch: Partial<GitBranchConfig>,
1673
628
  path = configPath,
1674
- ): PolishedTuiConfig {
1675
- return saveComponentsMutation((components) => {
1676
- components.footer.styles.starship.gitBranch = {
1677
- ...components.footer.styles.starship.gitBranch,
1678
- ...patch,
1679
- };
1680
- }, path);
629
+ ): ZentuiConfig {
630
+ return saveStarshipFooterStylePatch({ gitBranch: patch }, path);
1681
631
  }
1682
-
1683
632
  export function saveEditorModelLabel(
1684
633
  value: ModelLabelSource,
1685
634
  path = configPath,
1686
- ): PolishedTuiConfig {
635
+ ): ZentuiConfig {
1687
636
  return saveComponentsMutation((components) => {
1688
- const normalized = parseEditorModelLabel(value);
1689
- components.editor.modelLabel = normalized;
1690
- components.footer.modelLabel = normalized;
637
+ components.editor.modelLabel = parseEditorModelLabel(value);
638
+ components.footer.modelLabel = parseEditorModelLabel(value);
1691
639
  }, path);
1692
640
  }
1693
-
1694
641
  export function saveEditorStyle(
1695
642
  value: EditorStyle,
1696
643
  path = configPath,
1697
- ): PolishedTuiConfig {
644
+ ): ZentuiConfig {
1698
645
  return saveEditorComponentPatch({ style: parseEditorStyle(value) }, path);
1699
646
  }
1700
-
1701
647
  export function saveMinimalistPatch(
1702
648
  patch: Partial<MinimalistConfig>,
1703
649
  path = configPath,
1704
- ): PolishedTuiConfig {
650
+ ): ZentuiConfig {
1705
651
  return saveMinimalistEditorStylePatch(patch, path);
1706
652
  }
1707
-
1708
653
  export function saveEditorBorderColorMode(
1709
654
  value: EditorBorderColorMode,
1710
655
  path = configPath,
1711
- ): PolishedTuiConfig {
656
+ ): ZentuiConfig {
1712
657
  return saveEditorComponentPatch(
1713
658
  { borderColorMode: parseEditorBorderColorMode(value) },
1714
659
  path,
1715
660
  );
1716
661
  }
1717
-
1718
662
  export function saveGitCommitPatch(
1719
663
  patch: Partial<Pick<GitCommitConfig, "onlyDetached" | "showTag">>,
1720
664
  path = configPath,
1721
- ): PolishedTuiConfig {
1722
- const valid: Partial<GitCommitConfig> = {};
1723
- if (typeof patch.onlyDetached === "boolean")
1724
- valid.onlyDetached = patch.onlyDetached;
1725
- if (typeof patch.showTag === "boolean") valid.showTag = patch.showTag;
1726
- return saveComponentsMutation((components) => {
1727
- components.footer.styles.starship.gitCommit = {
1728
- ...components.footer.styles.starship.gitCommit,
1729
- ...valid,
1730
- };
1731
- }, path);
665
+ ): ZentuiConfig {
666
+ const valid = Object.fromEntries(
667
+ Object.entries(patch).filter(
668
+ ([key, value]) =>
669
+ (key === "onlyDetached" || key === "showTag") &&
670
+ typeof value === "boolean",
671
+ ),
672
+ );
673
+ return saveStarshipFooterStylePatch({ gitCommit: valid }, path);
1732
674
  }
1733
-
1734
675
  export function saveGitMetricsPatch(
1735
676
  patch: Partial<GitMetricsConfig>,
1736
677
  path = configPath,
1737
- ): PolishedTuiConfig {
1738
- const valid: Partial<GitMetricsConfig> = {};
1739
- if (typeof patch.onlyNonzero === "boolean")
1740
- valid.onlyNonzero = patch.onlyNonzero;
1741
- if (typeof patch.ignoreSubmodules === "boolean")
1742
- valid.ignoreSubmodules = patch.ignoreSubmodules;
1743
- return saveComponentsMutation((components) => {
1744
- components.footer.styles.starship.gitMetrics = {
1745
- ...components.footer.styles.starship.gitMetrics,
1746
- ...valid,
1747
- };
1748
- }, path);
678
+ ): ZentuiConfig {
679
+ const valid = Object.fromEntries(
680
+ Object.entries(patch).filter(
681
+ ([key, value]) =>
682
+ (key === "onlyNonzero" || key === "ignoreSubmodules") &&
683
+ typeof value === "boolean",
684
+ ),
685
+ );
686
+ return saveStarshipFooterStylePatch({ gitMetrics: valid }, path);
1749
687
  }
1750
-
1751
688
  export function saveExtensionStatusDefaultPlacement(
1752
689
  placement: ExtensionStatusPlacement,
1753
690
  path = configPath,
1754
- ): PolishedTuiConfig {
691
+ ): ZentuiConfig {
1755
692
  return saveComponentsMutation((components) => {
1756
693
  components.footer.styles.starship.extensionStatuses.defaultPlacement =
1757
- isExtensionStatusPlacement(placement)
1758
- ? placement
1759
- : defaultConfig.extensionStatuses.defaultPlacement;
694
+ isExtensionStatusPlacement(placement) ? placement : "right";
1760
695
  }, path);
1761
696
  }
1762
-
1763
697
  export function saveExtensionStatusPlacement(
1764
698
  key: string,
1765
699
  placement: ExtensionStatusPlacement,
1766
700
  path = configPath,
1767
- ): PolishedTuiConfig {
701
+ ): ZentuiConfig {
1768
702
  return saveComponentsMutation((components) => {
1769
703
  Object.defineProperty(
1770
704
  components.footer.styles.starship.extensionStatuses.placements,
@@ -1778,12 +712,11 @@ export function saveExtensionStatusPlacement(
1778
712
  );
1779
713
  }, path);
1780
714
  }
1781
-
1782
715
  export function saveExtensionStatusColorMode(
1783
716
  key: string,
1784
717
  colorMode: ExtensionStatusColorMode,
1785
718
  path = configPath,
1786
- ): PolishedTuiConfig {
719
+ ): ZentuiConfig {
1787
720
  return saveComponentsMutation((components) => {
1788
721
  Object.defineProperty(
1789
722
  components.footer.styles.starship.extensionStatuses.colorModes,