@linxin666/dsh-pet 0.2.2 → 0.2.4

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 (125) hide show
  1. package/README.i18n.yaml +2 -2
  2. package/README.md +129 -3
  3. package/README.zh.md +129 -3
  4. package/assets/decorations/whale/decoration.json +20 -0
  5. package/assets/decorations/whale/whale-frames.png +0 -0
  6. package/assets/whale/pet.json +169 -20
  7. package/assets/whale-refined/pet.json +17 -2
  8. package/contracts/pet-manifest-v2.schema.json +281 -0
  9. package/contracts/status-decoration-v1.schema.json +144 -0
  10. package/contracts/voice-pack-v1.schema.json +234 -0
  11. package/lib/client.js +581 -38
  12. package/lib/client.js.map +1 -1
  13. package/lib/index.js +1684 -59
  14. package/lib/live2d-vendor.js +995 -0
  15. package/lib/live2d-vendor.js.map +1 -0
  16. package/lib/types/chatter.d.ts +61 -3
  17. package/lib/types/chatter.d.ts.map +1 -1
  18. package/lib/types/chatter.js +71 -12
  19. package/lib/types/client/PetDockEntry.d.ts.map +1 -1
  20. package/lib/types/client/PetDockEntry.js +2 -1
  21. package/lib/types/client/PetSettingsCard.d.ts +14 -0
  22. package/lib/types/client/PetSettingsCard.d.ts.map +1 -1
  23. package/lib/types/client/PetSettingsCard.js +24 -1
  24. package/lib/types/client/PetSprite.d.ts +7 -1
  25. package/lib/types/client/PetSprite.d.ts.map +1 -1
  26. package/lib/types/client/PetSprite.js +120 -12
  27. package/lib/types/client/index.d.ts.map +1 -1
  28. package/lib/types/client/index.js +5 -0
  29. package/lib/types/client/locales.d.ts +14 -0
  30. package/lib/types/client/locales.d.ts.map +1 -1
  31. package/lib/types/client/locales.js +14 -0
  32. package/lib/types/client/phase-stream.d.ts +22 -0
  33. package/lib/types/client/phase-stream.d.ts.map +1 -0
  34. package/lib/types/client/phase-stream.js +28 -0
  35. package/lib/types/client/renderers/PetRendererSwitch.d.ts +25 -0
  36. package/lib/types/client/renderers/PetRendererSwitch.d.ts.map +1 -0
  37. package/lib/types/client/renderers/PetRendererSwitch.js +24 -0
  38. package/lib/types/client/renderers/live2d/Live2dVisualMount.d.ts +22 -0
  39. package/lib/types/client/renderers/live2d/Live2dVisualMount.d.ts.map +1 -0
  40. package/lib/types/client/renderers/live2d/Live2dVisualMount.js +75 -0
  41. package/lib/types/client/renderers/live2d/runtime.d.ts +89 -0
  42. package/lib/types/client/renderers/live2d/runtime.d.ts.map +1 -0
  43. package/lib/types/client/renderers/live2d/runtime.js +63 -0
  44. package/lib/types/client/renderers/live2d/vendor-entry.d.ts +15 -0
  45. package/lib/types/client/renderers/live2d/vendor-entry.d.ts.map +1 -0
  46. package/lib/types/client/renderers/live2d/vendor-entry.js +14 -0
  47. package/lib/types/client/renderers/live2d.d.ts +52 -0
  48. package/lib/types/client/renderers/live2d.d.ts.map +1 -0
  49. package/lib/types/client/renderers/live2d.js +192 -0
  50. package/lib/types/client/renderers/registry.d.ts +32 -0
  51. package/lib/types/client/renderers/registry.d.ts.map +1 -0
  52. package/lib/types/client/renderers/registry.js +49 -0
  53. package/lib/types/client/settings-form.d.ts.map +1 -1
  54. package/lib/types/client/settings-form.js +9 -6
  55. package/lib/types/contracts/renderer.d.ts +50 -0
  56. package/lib/types/contracts/renderer.d.ts.map +1 -0
  57. package/lib/types/contracts/renderer.js +15 -0
  58. package/lib/types/contracts/status-decoration.d.ts +85 -0
  59. package/lib/types/contracts/status-decoration.d.ts.map +1 -0
  60. package/lib/types/contracts/status-decoration.js +21 -0
  61. package/lib/types/decoration.d.ts +39 -0
  62. package/lib/types/decoration.d.ts.map +1 -0
  63. package/lib/types/decoration.js +210 -0
  64. package/lib/types/event-projection.d.ts +8 -3
  65. package/lib/types/event-projection.d.ts.map +1 -1
  66. package/lib/types/event-projection.js +9 -4
  67. package/lib/types/index.d.ts +2 -0
  68. package/lib/types/index.d.ts.map +1 -1
  69. package/lib/types/index.js +2 -0
  70. package/lib/types/manifest-v2.d.ts +129 -0
  71. package/lib/types/manifest-v2.d.ts.map +1 -0
  72. package/lib/types/manifest-v2.js +387 -0
  73. package/lib/types/model3.d.ts +26 -0
  74. package/lib/types/model3.d.ts.map +1 -0
  75. package/lib/types/model3.js +96 -0
  76. package/lib/types/registry.d.ts +112 -5
  77. package/lib/types/registry.d.ts.map +1 -1
  78. package/lib/types/registry.js +395 -38
  79. package/lib/types/routes.d.ts +39 -2
  80. package/lib/types/routes.d.ts.map +1 -1
  81. package/lib/types/routes.js +312 -13
  82. package/lib/types/service.d.ts +38 -1
  83. package/lib/types/service.d.ts.map +1 -1
  84. package/lib/types/service.js +46 -3
  85. package/lib/types/voice-pack.d.ts +98 -0
  86. package/lib/types/voice-pack.d.ts.map +1 -0
  87. package/lib/types/voice-pack.js +384 -0
  88. package/package.json +13 -10
  89. package/src/chatter.test.ts +89 -2
  90. package/src/chatter.ts +120 -14
  91. package/src/client/PetDockEntry.tsx +19 -11
  92. package/src/client/PetSettingsCard.tsx +57 -0
  93. package/src/client/PetSprite.test.tsx +265 -12
  94. package/src/client/PetSprite.tsx +165 -33
  95. package/src/client/index.ts +6 -0
  96. package/src/client/locales.ts +14 -0
  97. package/src/client/phase-stream.test.ts +76 -0
  98. package/src/client/phase-stream.ts +39 -0
  99. package/src/client/renderers/PetRendererSwitch.test.tsx +66 -0
  100. package/src/client/renderers/PetRendererSwitch.tsx +48 -0
  101. package/src/client/renderers/live2d/Live2dVisualMount.tsx +98 -0
  102. package/src/client/renderers/live2d/runtime.test.ts +61 -0
  103. package/src/client/renderers/live2d/runtime.ts +119 -0
  104. package/src/client/renderers/live2d/vendor-entry.ts +14 -0
  105. package/src/client/renderers/live2d.test.ts +263 -0
  106. package/src/client/renderers/live2d.ts +231 -0
  107. package/src/client/renderers/registry.ts +58 -0
  108. package/src/client/settings-form.ts +8 -6
  109. package/src/client/settings-section.module.css +23 -0
  110. package/src/contracts/renderer.ts +54 -0
  111. package/src/contracts/status-decoration.ts +78 -0
  112. package/src/decoration.test.ts +178 -0
  113. package/src/decoration.ts +220 -0
  114. package/src/event-projection.ts +10 -5
  115. package/src/index.ts +2 -0
  116. package/src/manifest-v2.test.ts +251 -0
  117. package/src/manifest-v2.ts +414 -0
  118. package/src/model3.test.ts +79 -0
  119. package/src/model3.ts +91 -0
  120. package/src/registry.test.ts +438 -2
  121. package/src/registry.ts +470 -38
  122. package/src/routes.ts +328 -14
  123. package/src/service.ts +65 -3
  124. package/src/voice-pack.test.ts +216 -0
  125. package/src/voice-pack.ts +413 -0
package/src/chatter.ts CHANGED
@@ -17,6 +17,11 @@
17
17
  * plugin has always shown, so existing installs keep their wording until the
18
18
  * scene cycles. No emoji anywhere (repository rule); ~ is the whale-girl's
19
19
  * signature.
20
+ *
21
+ * Since pet-center M4 (issue #677) every pool is overridable through a
22
+ * {@link VoicePoolsProvider}: the built-in pools are the fallback layer, and
23
+ * voice packs (per-pet voice.json / the global .voice.json) layer their
24
+ * pools on top at draw time.
20
25
  * @module @linxin666/dsh-pet/chatter
21
26
  */
22
27
 
@@ -193,6 +198,18 @@ export type ToolCategory =
193
198
  | 'ask'
194
199
  | 'generic'
195
200
 
201
+ /** Every status scene key, in declaration order (voice-pack key allow-list). */
202
+ export const STATUS_SCENES: readonly StatusScene[] = [
203
+ 'prepare', 'waiting', 'thinking', 'review', 'toolResult', 'done',
204
+ 'failed', 'toolFailed', 'maxTokens', 'interrupted', 'blocked',
205
+ ]
206
+
207
+ /** Every tool-family key, in declaration order (voice-pack key allow-list). */
208
+ export const TOOL_CATEGORIES: readonly ToolCategory[] = [
209
+ 'read', 'write', 'edit', 'shell', 'grep', 'find', 'ls', 'webSearch',
210
+ 'webFetch', 'mcp', 'memory', 'subagent', 'todo', 'browser', 'git', 'ask', 'generic',
211
+ ]
212
+
196
213
  /** Map a raw tool name onto its copy family (working-activity style regexes). */
197
214
  export function toolCategory(toolName: string): ToolCategory {
198
215
  const name = toolName.toLowerCase()
@@ -433,12 +450,19 @@ export function toolArgHint(toolName: string, argumentsJson: string): string | u
433
450
  * stretch keeps changing its wording.
434
451
  */
435
452
  export class StatusVoice {
453
+ private readonly pools: VoicePoolsProvider
454
+ private readonly rotateMs: number
436
455
  private readonly counters = new Map<string, number>()
437
456
  private lastScene = ''
438
457
  private lastLine = ''
439
458
  private lastLineAt = Number.NEGATIVE_INFINITY
440
459
 
441
- constructor(private readonly rotateMs: number = STATUS_ROTATE_MS) {}
460
+ constructor(pools: VoicePoolsProvider = () => BUILTIN_VOICE_PACK, rotateMs: number = STATUS_ROTATE_MS) {
461
+ // Plain property assignment, not parameter properties: this module is
462
+ // imported by scripts/ under node's strip-only mode (pet-center M4).
463
+ this.pools = pools
464
+ this.rotateMs = rotateMs
465
+ }
442
466
 
443
467
  /** Draw the next line of one pool, advancing its round-robin cursor. */
444
468
  private draw(poolKey: string, pool: readonly string[]): string {
@@ -456,24 +480,38 @@ export class StatusVoice {
456
480
  return this.lastLine
457
481
  }
458
482
 
483
+ /**
484
+ * A scene's effective pool: the voice-pack override when it carries lines,
485
+ * else the built-in pool. Empty overrides fall back rather than blank the
486
+ * bubble — a scene line always renders.
487
+ */
488
+ private scenePool(scene: StatusScene): readonly string[] {
489
+ const override = this.pools().status?.[scene]
490
+ return override !== undefined && override.length > 0 ? override : STATUS_POOLS[scene]
491
+ }
492
+
459
493
  /** Status line for a phase scene. */
460
494
  scene(scene: StatusScene, nowMs: number): string {
461
- return this.voice('scene:' + scene, 'pool:' + scene, STATUS_POOLS[scene], nowMs)
495
+ return this.voice('scene:' + scene, 'pool:' + scene, this.scenePool(scene), nowMs)
462
496
  }
463
497
 
464
498
  /** Status line for a tool call, with the real-argument hint when known. */
465
499
  tool(toolName: string, displayName: string, hint: string | undefined, nowMs: number): string {
466
500
  const category = toolCategory(toolName)
467
- const line = this.voice('tool:' + category, 'tool:' + category, TOOL_POOLS[category], nowMs)
501
+ const override = this.pools().tools?.[category]
502
+ const pool = override !== undefined && override.length > 0 ? override : TOOL_POOLS[category]
503
+ const line = this.voice('tool:' + category, 'tool:' + category, pool, nowMs)
468
504
  return line
469
- .replace('{tool}', displayName)
470
- .replace('{hint}', hint ?? displayName)
505
+ .replaceAll('{tool}', displayName)
506
+ .replaceAll('{hint}', hint ?? displayName)
471
507
  }
472
508
 
473
509
  /** Status line while sibling tools still run (always reflects the count). */
474
510
  toolRemaining(count: number, nowMs: number): string {
475
- return this.voice('toolRemaining', 'toolRemaining', TOOL_REMAINING_POOL, nowMs)
476
- .replace('{n}', String(count))
511
+ const override = this.pools().toolRemaining
512
+ const pool = override !== undefined && override.length > 0 ? override : TOOL_REMAINING_POOL
513
+ return this.voice('toolRemaining', 'toolRemaining', pool, nowMs)
514
+ .replaceAll('{n}', String(count))
477
515
  }
478
516
  }
479
517
 
@@ -734,24 +772,89 @@ export const WHISPER_RULES: readonly WhisperRule[] = [
734
772
  },
735
773
  ]
736
774
 
775
+ /**
776
+ * Voice-pack overrides (pet-center M4, issue #677): the content a voice
777
+ * pack can replace, one pool at a time. Every field is optional — missing
778
+ * keys inherit the built-in pools. Resolution happens at draw time through
779
+ * a provider function, so swapping pets (or editing the global file) re-
780
+ * voices live engines without rebuilding them.
781
+ *
782
+ * Override semantics:
783
+ * - status/tools/toolRemaining: a non-empty override replaces the built-in
784
+ * pool for that key; an empty override falls back to the built-in pool
785
+ * (a scene line always renders, so it can never be blanked).
786
+ * - whispers.generic / whispers.rules: the override REPLACES the built-in
787
+ * section; an empty array mutes that channel (ambient or keyword).
788
+ */
789
+ export interface VoicePackOverrides {
790
+ /** Status copy pools by scene; each key replaces that scene's pool. */
791
+ status?: Partial<Record<StatusScene, readonly string[]>>
792
+ /** Tool copy pools by family; each key replaces that family's pool. */
793
+ tools?: Partial<Record<ToolCategory, readonly string[]>>
794
+ /** The parallel-tools count line pool ({n} interpolates the count). */
795
+ toolRemaining?: readonly string[]
796
+ /** Murmur pools; each section replaces the built-in one as a whole. */
797
+ whispers?: {
798
+ /** Ambient inner-whisper pool (empty mutes ambient whispers). */
799
+ generic?: readonly string[]
800
+ /** Ordered keyword rules (empty disables keyword-triggered whispers). */
801
+ rules?: readonly WhisperRule[]
802
+ }
803
+ }
804
+
805
+ /** Read the current effective voice-pack overrides (draw-time resolution). */
806
+ export type VoicePoolsProvider = () => VoicePackOverrides
807
+
808
+ /** The built-in voice pack: the plugin's default copy, unchanged since v1. */
809
+ export const BUILTIN_VOICE_PACK: VoicePackOverrides = {
810
+ status: STATUS_POOLS,
811
+ tools: TOOL_POOLS,
812
+ toolRemaining: TOOL_REMAINING_POOL,
813
+ whispers: { generic: WHISPER_GENERIC_POOL, rules: WHISPER_RULES },
814
+ }
815
+
737
816
  /**
738
817
  * The murmur engine (碎碎念): watches the model's own output and lets the pet
739
818
  * whisper its inner voice. Two ways to earn a whisper:
740
819
  * - a keyword rule matches the fresh chunk text (themed whisper);
741
820
  * - enough output volume flowed by without one (ambient whisper).
742
821
  * A cooldown keeps whispers occasional; all picks are round-robin so tests
743
- * reproduce exact lines.
822
+ * reproduce exact lines. The voice-pack provider (pet-center M4) swaps the
823
+ * pools at draw time, so a pet switch re-voices live engines in place.
744
824
  */
745
825
  export class WhisperEngine {
826
+ private readonly pools: VoicePoolsProvider
827
+ private readonly cooldownMs: number
828
+ private readonly charBudget: number
746
829
  private readonly counters = new Map<number, number>()
747
830
  private genericCursor = 0
748
831
  private lastWhisperAt = Number.NEGATIVE_INFINITY
749
832
  private charsSinceWhisper = 0
750
833
 
751
834
  constructor(
752
- private readonly cooldownMs: number = WHISPER_COOLDOWN_MS,
753
- private readonly charBudget: number = WHISPER_CHAR_BUDGET,
754
- ) {}
835
+ pools: VoicePoolsProvider = () => BUILTIN_VOICE_PACK,
836
+ cooldownMs: number = WHISPER_COOLDOWN_MS,
837
+ charBudget: number = WHISPER_CHAR_BUDGET,
838
+ ) {
839
+ this.pools = pools
840
+ this.cooldownMs = cooldownMs
841
+ this.charBudget = charBudget
842
+ }
843
+
844
+ /**
845
+ * Effective keyword rules: an override replaces the built-in rules as a
846
+ * whole; an explicit empty array disables keyword-triggered whispers.
847
+ */
848
+ private rules(): readonly WhisperRule[] {
849
+ const override = this.pools().whispers?.rules
850
+ return override === undefined ? WHISPER_RULES : override
851
+ }
852
+
853
+ /** Effective ambient pool (an explicit empty array mutes ambient whispers). */
854
+ private generic(): readonly string[] {
855
+ const override = this.pools().whispers?.generic
856
+ return override === undefined ? WHISPER_GENERIC_POOL : override
857
+ }
755
858
 
756
859
  /**
757
860
  * Feed one model-output chunk (reasoning or text). Returns the whisper to
@@ -765,8 +868,9 @@ export class WhisperEngine {
765
868
  return undefined
766
869
  }
767
870
  const haystack = text.toLowerCase()
768
- for (let ruleIndex = 0; ruleIndex < WHISPER_RULES.length; ruleIndex += 1) {
769
- const rule = WHISPER_RULES[ruleIndex]!
871
+ const rules = this.rules()
872
+ for (let ruleIndex = 0; ruleIndex < rules.length; ruleIndex += 1) {
873
+ const rule = rules[ruleIndex]!
770
874
  if (!rule.keywords.some(keyword => haystack.includes(keyword))) continue
771
875
  const index = (this.counters.get(ruleIndex) ?? 0) % rule.pool.length
772
876
  this.counters.set(ruleIndex, index + 1)
@@ -774,7 +878,9 @@ export class WhisperEngine {
774
878
  }
775
879
  this.charsSinceWhisper += text.length
776
880
  if (this.charsSinceWhisper < this.charBudget) return undefined
777
- const line = WHISPER_GENERIC_POOL[this.genericCursor % WHISPER_GENERIC_POOL.length]!
881
+ const generic = this.generic()
882
+ if (generic.length === 0) return undefined
883
+ const line = generic[this.genericCursor % generic.length]!
778
884
  this.genericCursor += 1
779
885
  return this.speak(line, nowMs)
780
886
  }
@@ -16,6 +16,7 @@ import type { PropsLocale } from '@deepseek-ai/dsh-client-ui-slots'
16
16
  import type { PetDisplayConfig } from '../persist.ts'
17
17
  import type { PetStoreInstance } from './pet-store.ts'
18
18
  import { PetSprite } from './PetSprite.tsx'
19
+ import { PetRendererSwitch } from './renderers/PetRendererSwitch.tsx'
19
20
  import { NS } from './locales.ts'
20
21
  import styles from './pet.module.css'
21
22
 
@@ -75,20 +76,27 @@ export function PetDockEntry(props: PetDockEntryProps): ReactElement {
75
76
  {snapshot === null || definition === null
76
77
  ? null
77
78
  : (
78
- <PetSprite
79
- snapshot={snapshot}
79
+ <PetRendererSwitch
80
80
  definition={definition}
81
- display={snapshot.display}
82
- feedback={feedback}
81
+ phase={snapshot?.phase ?? 'idle'}
83
82
  onPet={props.pet}
84
- onFeed={props.feed}
85
- onHide={props.hide}
86
- onDragEnd={props.dragEnd}
87
- onRename={props.rename}
88
- onOpenSession={props.openSession}
89
- onFeedbackDone={props.feedbackDone}
90
83
  t={props.t}
91
- />
84
+ >
85
+ <PetSprite
86
+ snapshot={snapshot}
87
+ definition={definition}
88
+ display={snapshot.display}
89
+ feedback={feedback}
90
+ onPet={props.pet}
91
+ onFeed={props.feed}
92
+ onHide={props.hide}
93
+ onDragEnd={props.dragEnd}
94
+ onRename={props.rename}
95
+ onOpenSession={props.openSession}
96
+ onFeedbackDone={props.feedbackDone}
97
+ t={props.t}
98
+ />
99
+ </PetRendererSwitch>
92
100
  )}
93
101
  </span>
94
102
  )
@@ -30,6 +30,8 @@ export interface PetSettings {
30
30
  bottom?: number
31
31
  /** Selected pet id (a registry entry). */
32
32
  petId?: string
33
+ /** Status-decoration master switch (pet-center M5, #567). */
34
+ decorationEnabled?: boolean
33
35
  }
34
36
 
35
37
  /** What the pet settings card renders. */
@@ -46,8 +48,12 @@ export interface PetSettingsCardState extends CardShell {
46
48
  bottom: CardFieldState
47
49
  /** Selected pet. */
48
50
  petId: CardFieldState
51
+ /** Status-decoration master switch. */
52
+ decorationEnabled: CardFieldState
49
53
  /** Pet choices (registry ids + display names), loaded from the host. */
50
54
  petChoices: readonly { value: string; label: string }[]
55
+ /** Registry diagnostics (v1 migration hints, invalid entries), host-served. */
56
+ petDiagnostics: readonly PetDiagnosticView[]
51
57
  }
52
58
 
53
59
  /** The registration-side face the card's slot entry injects. */
@@ -64,6 +70,12 @@ interface PetChoice {
64
70
  displayName: string
65
71
  }
66
72
 
73
+ /** One registry diagnostic as served by '/api/pet/diagnostics' (#623). */
74
+ export interface PetDiagnosticView {
75
+ level: 'error' | 'warning'
76
+ message: string
77
+ }
78
+
67
79
  /** Fetch the registry list (the same data the sprite renders from). */
68
80
  async function fetchPetChoices(): Promise<PetChoice[]> {
69
81
  const response = await fetch('/api/pet/pets')
@@ -71,6 +83,14 @@ async function fetchPetChoices(): Promise<PetChoice[]> {
71
83
  return (await response.json()) as PetChoice[]
72
84
  }
73
85
 
86
+ /** Fetch the registry diagnostics (v1 migration hints, invalid entries). */
87
+ async function fetchPetDiagnostics(): Promise<PetDiagnosticView[]> {
88
+ const response = await fetch('/api/pet/diagnostics')
89
+ if (!response.ok) throw new Error('pet diagnostics failed: ' + response.status)
90
+ const body = (await response.json()) as { diagnostics?: PetDiagnosticView[] }
91
+ return body.diagnostics ?? []
92
+ }
93
+
74
94
  /** Bridges the 'pet' scope onto the card's staged form. */
75
95
  export class PetSettingsCardController {
76
96
  private readonly form: CardForm<PetSettings>
@@ -80,6 +100,7 @@ export class PetSettingsCardController {
80
100
  // without rebuilding the form.
81
101
  private readonly petChoices: string[] = []
82
102
  private readonly petLabels = new Map<string, string>()
103
+ private diagnostics: PetDiagnosticView[] = []
83
104
  private loaded = false
84
105
  private attempts = 0
85
106
 
@@ -87,6 +108,7 @@ export class PetSettingsCardController {
87
108
  constructor(scope: SettingsScope<PetSettings>) {
88
109
  this.form = new CardForm(scope, [
89
110
  booleanField('enabled'),
111
+ booleanField('decorationEnabled'),
90
112
  booleanField('visible'),
91
113
  numberField('size'),
92
114
  numberField('right'),
@@ -95,6 +117,17 @@ export class PetSettingsCardController {
95
117
  ])
96
118
  this.store = this.form.bind(() => this.projection())
97
119
  void this.loadPets()
120
+ void this.loadDiagnostics()
121
+ }
122
+
123
+ /** Fetch registry diagnostics once (soft-fail: an empty list on error). */
124
+ private async loadDiagnostics(): Promise<void> {
125
+ try {
126
+ this.diagnostics = await fetchPetDiagnostics()
127
+ this.store.set(this.projection())
128
+ } catch {
129
+ this.diagnostics = []
130
+ }
98
131
  }
99
132
 
100
133
  /** Resolve the registry choices once (retried a few times on failure). */
@@ -118,12 +151,14 @@ export class PetSettingsCardController {
118
151
  return {
119
152
  ...this.form.shell(),
120
153
  enabled: this.form.field('enabled'),
154
+ decorationEnabled: this.form.field('decorationEnabled'),
121
155
  visible: this.form.field('visible'),
122
156
  size: this.form.field('size'),
123
157
  right: this.form.field('right'),
124
158
  bottom: this.form.field('bottom'),
125
159
  petId: this.form.field('petId'),
126
160
  petChoices: this.petChoices.map(id => ({ value: id, label: this.petLabels.get(id) ?? id })),
161
+ petDiagnostics: this.diagnostics,
127
162
  }
128
163
  }
129
164
 
@@ -186,6 +221,18 @@ export function PetSettingsCard(props: PetSettingsCardProps) {
186
221
  onEdit={(text) => { props.edit('enabled', text) }}
187
222
  onReset={() => { props.resetField('enabled') }}
188
223
  />
224
+ <BooleanField
225
+ id="settings-pet-decoration"
226
+ label={t('settings.decoration')}
227
+ hint={t('settings.decorationHint')}
228
+ inheritLabel={t('settings.inherit')}
229
+ onLabel={t('settings.on')}
230
+ offLabel={t('settings.off')}
231
+ {...fieldProps}
232
+ {...state.decorationEnabled}
233
+ onEdit={(text) => { props.edit('decorationEnabled', text) }}
234
+ onReset={() => { props.resetField('decorationEnabled') }}
235
+ />
189
236
  <ChoiceField
190
237
  id="settings-pet-pet"
191
238
  label={t('settings.pet')}
@@ -197,6 +244,16 @@ export function PetSettingsCard(props: PetSettingsCardProps) {
197
244
  onEdit={(text) => { props.edit('petId', text) }}
198
245
  onReset={() => { props.resetField('petId') }}
199
246
  />
247
+ {state.petDiagnostics.length === 0 ? null : (
248
+ <li className={sectionCss.diagnostics} data-dsh-part="diagnostics">
249
+ <span className={sectionCss.diagnosticsTitle}>{t('settings.diagnosticsTitle')}</span>
250
+ <ul>
251
+ {state.petDiagnostics.map((diagnostic, index) => (
252
+ <li key={index} data-level={diagnostic.level}>{diagnostic.message}</li>
253
+ ))}
254
+ </ul>
255
+ </li>
256
+ )}
200
257
  <BooleanField
201
258
  id="settings-pet-visible"
202
259
  label={t('settings.visible')}