@linxin666/dsh-pet 0.3.25 → 0.4.3

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.
package/lib/index.js CHANGED
@@ -6007,38 +6007,111 @@ function makePetRoutes(deps) {
6007
6007
  * the loader accepts a standalone install of the same package side by side;
6008
6008
  * without this guard the second instance would still re-register the same
6009
6009
  * webserver routes, tools, settings namespaces, and system-prompt sections
6010
- * and fail the boot. mountOnce makes the second host apply a no-op for the
6011
- * lifetime of the first instance (the browser half is already deduped by
6012
- * package name in the client module host).
6010
+ * and fail the boot. mountOnce makes a mount of an already-mounted package a
6011
+ * no-op for as long as the first instance lives (the browser half is already
6012
+ * deduped by package name in the client module host).
6013
+ *
6014
+ * A no-op is only safe while the holder is ALIVE. The holder can be disposed
6015
+ * long after the refused mount ran its course: the Host reloads a profile by
6016
+ * creating the new loader entries before the old ones are torn down (a
6017
+ * plugin-manager enable/disable/install write, a settings-driven row reload,
6018
+ * HMR), so the new aggregate shell entry mounts its family plugin while the
6019
+ * previous entry still owns the name. Dropping that refused mount lost the
6020
+ * plugin for good - the previous entry then disposed its own mount, releasing
6021
+ * the name with nobody left to take it, and the family row stayed listed as
6022
+ * active while its host routes 404ed (the task-board panel showed
6023
+ * "board.hostError.notMounted", no degraded record appeared, and only a Host
6024
+ * restart recovered it).
6025
+ *
6026
+ * The refused mount is therefore QUEUED, not dropped, and replayed the moment
6027
+ * the holder releases the name - if the waiting fiber is still alive then. The
6028
+ * single-instance guarantee is unchanged: exactly one mount is live per
6029
+ * package name, and the replay re-enters the guard so a later mount still
6030
+ * dedupes against it.
6013
6031
  *
6014
6032
  * The registry rides a global symbol so two module instances of the same
6015
- * package (npm copy vs repository link) still share one verdict. cordis
6016
- * `ctx.effect` runs its callback immediately and treats the callback's
6033
+ * package (npm copy vs repository link) still share one verdict. That symbol
6034
+ * is a CROSS-REPOSITORY contract, not this file's private state: the four
6035
+ * satellite packages (dsh-skins / dsh-pet / dsh-presets /
6036
+ * dsh-community-plugins) are separate repositories carrying their own copy of
6037
+ * this guard, rebuilt on their own schedule, so the value under `MOUNTED`
6038
+ * must keep the shape every published copy reads (a `Set` of package names
6039
+ * with `has`/`add`/`delete`). The wait queues this guard added therefore live
6040
+ * under their own additive key, and the registry reads back a `Set` even when
6041
+ * some other build left a different value there. Changing `MOUNTED`'s shape
6042
+ * in place broke that contract once: a satellite's legacy copy created a
6043
+ * `Set`, the family's new copy read it as a `Map`, and every family row
6044
+ * mounted after it failed with "claims.get is not a function".
6045
+ *
6046
+ * cordis `ctx.effect` runs its callback immediately and treats the callback's
6017
6047
  * return value as the fiber disposer, so the unmarker is returned, not run.
6018
6048
  */
6049
+ /** Published cross-repository contract: package names currently mounted. */
6019
6050
  const MOUNTED = Symbol.for("dsh-web.mounted-plugins");
6051
+ /** Additive key this guard owns: refused mounts waiting for the name. */
6052
+ const WAITERS = Symbol.for("dsh-web.mounted-plugins.waiters");
6053
+ /**
6054
+ * The shared name registry, always a `Set` whatever another build stored here:
6055
+ * a foreign value (an interim shape, a hand-written global) must not take every
6056
+ * family plugin down with it.
6057
+ * @returns the process-wide set of mounted package names.
6058
+ */
6020
6059
  function mountedSet() {
6021
6060
  const registry = globalThis;
6022
- return registry[MOUNTED] ??= /* @__PURE__ */ new Set();
6061
+ const existing = registry[MOUNTED];
6062
+ if (existing instanceof Set) return existing;
6063
+ const created = /* @__PURE__ */ new Set();
6064
+ registry[MOUNTED] = created;
6065
+ return created;
6066
+ }
6067
+ /** Queue per package name for mounts refused while a holder was alive. */
6068
+ function mountWaiters() {
6069
+ const registry = globalThis;
6070
+ return registry[WAITERS] ??= /* @__PURE__ */ new Map();
6023
6071
  }
6024
6072
  /**
6025
6073
  * Wrap a cordis plugin apply so the package runs at most once per process.
6026
- * The first mount registers normally and unmarks when its fiber disposes;
6027
- * any later mount of the same package name is a no-op.
6074
+ * The first mount registers normally and releases the name when its fiber
6075
+ * disposes; a mount refused while that name is held waits for the release and
6076
+ * then runs, unless its own fiber disposes first.
6028
6077
  * @param packageName - npm package identity shared by every install source.
6029
6078
  * @param fn - the original plugin apply.
6030
6079
  * @returns an apply of the same shape.
6031
6080
  */
6032
6081
  function mountOnce(packageName, fn) {
6033
- return ((...args) => {
6082
+ const mount = (...args) => {
6034
6083
  const mounted = mountedSet();
6035
- if (mounted.has(packageName)) return;
6084
+ const ctx = args[0];
6085
+ if (mounted.has(packageName)) {
6086
+ const waiters = mountWaiters();
6087
+ const queue = waiters.get(packageName) ?? [];
6088
+ let alive = true;
6089
+ const pending = { run: () => {
6090
+ if (alive) mount(...args);
6091
+ } };
6092
+ ctx?.effect?.(() => () => {
6093
+ alive = false;
6094
+ const index = queue.indexOf(pending);
6095
+ if (index >= 0) queue.splice(index, 1);
6096
+ });
6097
+ queue.push(pending);
6098
+ waiters.set(packageName, queue);
6099
+ return;
6100
+ }
6036
6101
  mounted.add(packageName);
6037
- args[0]?.effect?.(() => () => {
6102
+ ctx?.effect?.(() => () => {
6038
6103
  mounted.delete(packageName);
6104
+ const waiters = mountWaiters();
6105
+ const queue = waiters.get(packageName);
6106
+ if (queue === void 0) return;
6107
+ waiters.delete(packageName);
6108
+ for (const waiter of queue.splice(0)) queueMicrotask(() => {
6109
+ waiter.run();
6110
+ });
6039
6111
  });
6040
6112
  return fn(...args);
6041
- });
6113
+ };
6114
+ return mount;
6042
6115
  }
6043
6116
  //#endregion
6044
6117
  //#region src/index.ts
@@ -57,8 +57,6 @@ export interface PetSettingsCardState extends CardShell {
57
57
  value: string;
58
58
  label: string;
59
59
  }[];
60
- /** Registry diagnostics (v1 migration hints, invalid entries), host-served. */
61
- petDiagnostics: readonly PetDiagnosticView[];
62
60
  }
63
61
  /** The registration-side face the card's slot entry injects. */
64
62
  export interface PetSettingsCardFace extends CardActions {
@@ -67,20 +65,16 @@ export interface PetSettingsCardFace extends CardActions {
67
65
  petSettingsCard: SnapshotStore<PetSettingsCardState>;
68
66
  };
69
67
  }
70
- /** One registry diagnostic as served by '/api/pet/diagnostics' (#623). */
71
- export interface PetDiagnosticView {
72
- level: 'error' | 'warning';
73
- message: string;
74
- }
75
68
  /** Bridges the 'pet' scope onto the card's staged form. */
76
69
  export declare class PetSettingsCardController {
77
70
  private readonly form;
78
71
  private readonly store;
79
72
  private readonly petChoices;
80
73
  private readonly petLabels;
81
- private diagnostics;
82
74
  private selectedPetId;
75
+ private selectedVisible;
83
76
  private stagedPetId;
77
+ private stagedVisible;
84
78
  private savingPet;
85
79
  private petSaveFailed;
86
80
  private loaded;
@@ -90,13 +84,23 @@ export declare class PetSettingsCardController {
90
84
  private pendingTimer;
91
85
  /** @param scope - the bound configuration form for the 'pet' entry. */
92
86
  constructor(scope: ConfigForm<PetSettings>);
93
- /** Fetch registry diagnostics once (soft-fail: an empty list on error). */
94
- private loadDiagnostics;
95
87
  /** Resolve the registry choices once (retried a few times on failure). */
96
88
  private loadPets;
97
- private loadSelectedPet;
89
+ private loadPetState;
98
90
  private fallback;
99
- private savePetSelection;
91
+ /**
92
+ * The visibility the fallback switch renders: the staged draft when the user
93
+ * moved it, the persisted value otherwise. There is no user layer to override
94
+ * here, so the field is never marked overridden.
95
+ */
96
+ private fallbackVisible;
97
+ /**
98
+ * Persist the staged selection and visibility through the pet API — the only
99
+ * writer available when the aggregate shell serves this card, since there is
100
+ * no Host settings form to mutate — then confirm the read-back before the
101
+ * drafts are cleared.
102
+ */
103
+ private saveFallback;
100
104
  private projection;
101
105
  /**
102
106
  * Build the face the card's slot registration injects.
@@ -1 +1 @@
1
- {"version":3,"file":"PetSettingsCard.d.ts","sourceRoot":"","sources":["../../../src/client/PetSettingsCard.tsx"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"PetSettingsCard.d.ts","sourceRoot":"","sources":["../../../src/client/PetSettingsCard.tsx"],"names":[],"mappings":"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"}
@@ -9,23 +9,16 @@ async function fetchPetChoices() {
9
9
  throw new Error('pet pets failed: ' + response.status);
10
10
  return (await response.json());
11
11
  }
12
- /** Fetch the registry diagnostics (v1 migration hints, invalid entries). */
13
- async function fetchPetDiagnostics() {
14
- const response = await fetch('/api/pet/diagnostics');
15
- if (!response.ok)
16
- throw new Error('pet diagnostics failed: ' + response.status);
17
- const body = (await response.json());
18
- return body.diagnostics ?? [];
19
- }
20
- /** Read the selection from the same persisted state the pet renders. */
21
- async function fetchSelectedPetId() {
12
+ /** Read the selection and visibility from the same persisted state the pet renders. */
13
+ async function fetchPetState() {
22
14
  const response = await fetch('/api/pet/state');
23
15
  if (!response.ok)
24
16
  throw new Error('pet state failed: ' + response.status);
25
17
  const body = (await response.json());
26
18
  if (typeof body.pet?.id !== 'string')
27
19
  throw new Error('pet state has no selected pet');
28
- return body.pet.id;
20
+ // A Host without the display block predates the visibility switch; the pet shows.
21
+ return { petId: body.pet.id, visible: body.display?.visible !== false };
29
22
  }
30
23
  /** Bridges the 'pet' scope onto the card's staged form. */
31
24
  export class PetSettingsCardController {
@@ -36,9 +29,10 @@ export class PetSettingsCardController {
36
29
  // without rebuilding the form.
37
30
  petChoices = [];
38
31
  petLabels = new Map();
39
- diagnostics = [];
40
32
  selectedPetId;
33
+ selectedVisible;
41
34
  stagedPetId;
35
+ stagedVisible;
42
36
  savingPet = false;
43
37
  petSaveFailed = false;
44
38
  loaded = false;
@@ -68,22 +62,9 @@ export class PetSettingsCardController {
68
62
  if (this.disposed)
69
63
  return;
70
64
  void this.loadPets();
71
- void this.loadDiagnostics();
72
- void this.loadSelectedPet();
65
+ void this.loadPetState();
73
66
  }, 0);
74
67
  }
75
- /** Fetch registry diagnostics once (soft-fail: an empty list on error). */
76
- async loadDiagnostics() {
77
- try {
78
- this.diagnostics = await fetchPetDiagnostics();
79
- if (this.disposed)
80
- return;
81
- this.store.set(this.projection());
82
- }
83
- catch {
84
- this.diagnostics = [];
85
- }
86
- }
87
68
  /** Resolve the registry choices once (retried a few times on failure). */
88
69
  async loadPets() {
89
70
  if (this.loaded || this.disposed)
@@ -112,12 +93,13 @@ export class PetSettingsCardController {
112
93
  }
113
94
  }
114
95
  }
115
- async loadSelectedPet() {
96
+ async loadPetState() {
116
97
  try {
117
- const petId = await fetchSelectedPetId();
98
+ const state = await fetchPetState();
118
99
  if (this.disposed)
119
100
  return;
120
- this.selectedPetId = petId;
101
+ this.selectedPetId = state.petId;
102
+ this.selectedVisible = state.visible;
121
103
  this.store.set(this.projection());
122
104
  }
123
105
  catch {
@@ -129,26 +111,65 @@ export class PetSettingsCardController {
129
111
  const shell = this.form.shell();
130
112
  return shell.available && !shell.exposed && this.selectedPetId !== undefined;
131
113
  }
132
- async savePetSelection() {
114
+ /**
115
+ * The visibility the fallback switch renders: the staged draft when the user
116
+ * moved it, the persisted value otherwise. There is no user layer to override
117
+ * here, so the field is never marked overridden.
118
+ */
119
+ fallbackVisible() {
120
+ const value = this.stagedVisible ?? this.selectedVisible;
121
+ return { text: value === undefined ? '' : String(value), overridden: false, invalid: false };
122
+ }
123
+ /**
124
+ * Persist the staged selection and visibility through the pet API — the only
125
+ * writer available when the aggregate shell serves this card, since there is
126
+ * no Host settings form to mutate — then confirm the read-back before the
127
+ * drafts are cleared.
128
+ */
129
+ async saveFallback() {
133
130
  const petId = this.stagedPetId;
134
- if (petId === undefined || this.savingPet || !this.petChoices.includes(petId))
131
+ const visible = this.stagedVisible;
132
+ if (this.savingPet)
133
+ return;
134
+ if (petId === undefined && visible === undefined)
135
+ return;
136
+ if (petId !== undefined && !this.petChoices.includes(petId))
135
137
  return;
136
138
  this.savingPet = true;
137
139
  this.petSaveFailed = false;
138
140
  this.store.set(this.projection());
139
141
  try {
140
- const response = await fetch('/api/pet/set-pet', {
141
- method: 'POST',
142
- headers: { 'content-type': 'application/json' },
143
- body: JSON.stringify({ petId }),
144
- });
145
- const result = (await response.json());
146
- if (!response.ok || result.ok !== true || result.petId !== petId || await fetchSelectedPetId() !== petId) {
147
- throw new Error('pet selection was not persisted');
142
+ if (petId !== undefined) {
143
+ const response = await fetch('/api/pet/set-pet', {
144
+ method: 'POST',
145
+ headers: { 'content-type': 'application/json' },
146
+ body: JSON.stringify({ petId }),
147
+ });
148
+ const result = (await response.json());
149
+ if (!response.ok || result.ok !== true || result.petId !== petId) {
150
+ throw new Error('pet selection was not persisted');
151
+ }
152
+ }
153
+ if (visible !== undefined) {
154
+ const response = await fetch('/api/pet/set-visible', {
155
+ method: 'POST',
156
+ headers: { 'content-type': 'application/json' },
157
+ body: JSON.stringify({ visible }),
158
+ });
159
+ if (!response.ok)
160
+ throw new Error('pet visibility was not persisted');
148
161
  }
149
- this.selectedPetId = petId;
162
+ const persisted = await fetchPetState();
163
+ if (petId !== undefined && persisted.petId !== petId)
164
+ throw new Error('pet selection was not persisted');
165
+ if (visible !== undefined && persisted.visible !== visible)
166
+ throw new Error('pet visibility was not persisted');
167
+ this.selectedPetId = persisted.petId;
168
+ this.selectedVisible = persisted.visible;
150
169
  if (this.stagedPetId === petId)
151
170
  this.stagedPetId = undefined;
171
+ if (this.stagedVisible === visible)
172
+ this.stagedVisible = undefined;
152
173
  }
153
174
  catch {
154
175
  this.petSaveFailed = true;
@@ -168,7 +189,8 @@ export class PetSettingsCardController {
168
189
  ...(fallback ? {
169
190
  exposed: true,
170
191
  writable: true,
171
- dirty: this.stagedPetId !== undefined && this.stagedPetId !== this.selectedPetId,
192
+ dirty: this.stagedPetId !== undefined && this.stagedPetId !== this.selectedPetId
193
+ || this.stagedVisible !== undefined && this.stagedVisible !== this.selectedVisible,
172
194
  invalid: this.stagedPetId !== undefined && !this.petChoices.includes(this.stagedPetId),
173
195
  saving: this.savingPet,
174
196
  failed: this.petSaveFailed,
@@ -177,7 +199,7 @@ export class PetSettingsCardController {
177
199
  petSelectionFallback: fallback,
178
200
  enabled: this.form.field('enabled'),
179
201
  decorationEnabled: this.form.field('decorationEnabled'),
180
- visible: this.form.field('visible'),
202
+ visible: fallback ? this.fallbackVisible() : this.form.field('visible'),
181
203
  size: this.form.field('size'),
182
204
  right: this.form.field('right'),
183
205
  bottom: this.form.field('bottom'),
@@ -188,7 +210,6 @@ export class PetSettingsCardController {
188
210
  ? { ...configuredPet, text: this.selectedPetId }
189
211
  : configuredPet,
190
212
  petChoices: this.petChoices.map(id => ({ value: id, label: this.petLabels.get(id) ?? id })),
191
- petDiagnostics: this.diagnostics,
192
213
  };
193
214
  }
194
215
  /**
@@ -202,29 +223,36 @@ export class PetSettingsCardController {
202
223
  edit: (field, value) => {
203
224
  if (!this.fallback())
204
225
  return actions.edit(field, value);
205
- if (field !== 'petId')
226
+ if (field === 'petId')
227
+ this.stagedPetId = value === '' ? undefined : value;
228
+ else if (field === 'visible')
229
+ this.stagedVisible = value === '' ? undefined : value === 'true';
230
+ else
206
231
  return;
207
- this.stagedPetId = value === '' ? undefined : value;
208
232
  this.petSaveFailed = false;
209
233
  this.store.set(this.projection());
210
234
  },
211
235
  resetField: (field) => {
212
236
  if (!this.fallback())
213
237
  return actions.resetField(field);
214
- if (field !== 'petId')
238
+ if (field === 'petId')
239
+ this.stagedPetId = undefined;
240
+ else if (field === 'visible')
241
+ this.stagedVisible = undefined;
242
+ else
215
243
  return;
216
- this.stagedPetId = undefined;
217
244
  this.petSaveFailed = false;
218
245
  this.store.set(this.projection());
219
246
  },
220
247
  save: () => { if (this.fallback())
221
- void this.savePetSelection();
248
+ void this.saveFallback();
222
249
  else
223
250
  actions.save(); },
224
251
  discard: () => {
225
252
  if (!this.fallback())
226
253
  return actions.discard();
227
254
  this.stagedPetId = undefined;
255
+ this.stagedVisible = undefined;
228
256
  this.petSaveFailed = false;
229
257
  this.store.set(this.projection());
230
258
  },
@@ -260,7 +288,7 @@ export function PetSettingsCard(props) {
260
288
  invalidLabel: t('settings.invalidNumber'),
261
289
  disabled,
262
290
  };
263
- return (_jsxs(PluginSettingsCard, { t: t, titleKey: "settings.title", descriptionKey: "settings.description", descriptionNode: state.petSelectionFallback ? t('settings.petHint') : undefined, state: state, onSave: props.save, onDiscard: props.discard, alwaysOpen: true, children: [state.petSelectionFallback ? null : _jsx(BooleanField, { id: "settings-pet-enabled", label: t('settings.enabled'), hint: t('settings.enabledHint'), inheritLabel: t('settings.inherit'), onLabel: t('settings.on'), offLabel: t('settings.off'), ...fieldProps, ...state.enabled, onEdit: (text) => { props.edit('enabled', text); }, onReset: () => { props.resetField('enabled'); } }), state.petSelectionFallback ? null : _jsx(BooleanField, { id: "settings-pet-decoration", label: t('settings.decoration'), hint: t('settings.decorationHint'), inheritLabel: t('settings.inherit'), onLabel: t('settings.on'), offLabel: t('settings.off'), ...fieldProps, ...state.decorationEnabled, onEdit: (text) => { props.edit('decorationEnabled', text); }, onReset: () => { props.resetField('decorationEnabled'); } }), _jsx(ChoiceField, { id: "settings-pet-pet", label: t('settings.pet'), hint: t('settings.petHint'), inheritLabel: t('settings.inherit'), ...fieldProps, ...state.petId, choices: state.petChoices, onEdit: (text) => { props.edit('petId', text); }, onReset: () => { props.resetField('petId'); } }), state.petDiagnostics.length === 0 ? null : (_jsxs("li", { className: sectionCss.diagnostics, "data-dsh-part": "diagnostics", children: [_jsx("span", { className: sectionCss.diagnosticsTitle, children: t('settings.diagnosticsTitle') }), _jsx("ul", { children: state.petDiagnostics.map((diagnostic, index) => (_jsx("li", { "data-level": diagnostic.level, children: diagnostic.message }, index))) })] })), state.petSelectionFallback ? null : _jsx(BooleanField, { id: "settings-pet-visible", label: t('settings.visible'), hint: t('settings.visibleHint'), inheritLabel: t('settings.inherit'), onLabel: t('settings.on'), offLabel: t('settings.off'), ...fieldProps, ...state.visible, onEdit: (text) => { props.edit('visible', text); }, onReset: () => { props.resetField('visible'); } }), state.petSelectionFallback ? null : _jsx(ValueField, { id: "settings-pet-size", label: t('settings.size'), hint: t('settings.sizeHint'), numeric: true, ...fieldProps, ...state.size, onEdit: (text) => { props.edit('size', text); }, onReset: () => { props.resetField('size'); } }), state.petSelectionFallback ? null : _jsx(ValueField, { id: "settings-pet-right", label: t('settings.right'), hint: t('settings.rightHint'), numeric: true, ...fieldProps, ...state.right, onEdit: (text) => { props.edit('right', text); }, onReset: () => { props.resetField('right'); } }), state.petSelectionFallback ? null : _jsx(ValueField, { id: "settings-pet-bottom", label: t('settings.bottom'), hint: t('settings.bottomHint'), numeric: true, ...fieldProps, ...state.bottom, onEdit: (text) => { props.edit('bottom', text); }, onReset: () => { props.resetField('bottom'); } }), state.petSelectionFallback ? null : _jsx(ValueField, { id: "settings-pet-bubble-scale", label: t('settings.bubbleScale'), hint: t('settings.bubbleScaleHint'), numeric: true, ...fieldProps, ...state.bubbleScale, onEdit: (text) => { props.edit('bubbleScale', text); }, onReset: () => { props.resetField('bubbleScale'); } })] }));
291
+ return (_jsxs(PluginSettingsCard, { t: t, titleKey: "settings.title", descriptionKey: "settings.description", descriptionNode: state.petSelectionFallback ? t('settings.petHint') : undefined, state: state, onSave: props.save, onDiscard: props.discard, alwaysOpen: true, children: [state.petSelectionFallback ? null : _jsx(BooleanField, { id: "settings-pet-enabled", label: t('settings.enabled'), hint: t('settings.enabledHint'), inheritLabel: t('settings.inherit'), onLabel: t('settings.on'), offLabel: t('settings.off'), ...fieldProps, ...state.enabled, onEdit: (text) => { props.edit('enabled', text); }, onReset: () => { props.resetField('enabled'); } }), state.petSelectionFallback ? null : _jsx(BooleanField, { id: "settings-pet-decoration", label: t('settings.decoration'), hint: t('settings.decorationHint'), inheritLabel: t('settings.inherit'), onLabel: t('settings.on'), offLabel: t('settings.off'), ...fieldProps, ...state.decorationEnabled, onEdit: (text) => { props.edit('decorationEnabled', text); }, onReset: () => { props.resetField('decorationEnabled'); } }), _jsx(ChoiceField, { id: "settings-pet-pet", label: t('settings.pet'), hint: t('settings.petHint'), inheritLabel: t('settings.inherit'), ...fieldProps, ...state.petId, choices: state.petChoices, onEdit: (text) => { props.edit('petId', text); }, onReset: () => { props.resetField('petId'); } }), _jsx(BooleanField, { id: "settings-pet-visible", label: t('settings.visible'), hint: t('settings.visibleHint'), inheritLabel: t('settings.inherit'), onLabel: t('settings.on'), offLabel: t('settings.off'), ...fieldProps, ...state.visible, onEdit: (text) => { props.edit('visible', text); }, onReset: () => { props.resetField('visible'); } }), state.petSelectionFallback ? null : _jsx(ValueField, { id: "settings-pet-size", label: t('settings.size'), hint: t('settings.sizeHint'), numeric: true, ...fieldProps, ...state.size, onEdit: (text) => { props.edit('size', text); }, onReset: () => { props.resetField('size'); } }), state.petSelectionFallback ? null : _jsx(ValueField, { id: "settings-pet-right", label: t('settings.right'), hint: t('settings.rightHint'), numeric: true, ...fieldProps, ...state.right, onEdit: (text) => { props.edit('right', text); }, onReset: () => { props.resetField('right'); } }), state.petSelectionFallback ? null : _jsx(ValueField, { id: "settings-pet-bottom", label: t('settings.bottom'), hint: t('settings.bottomHint'), numeric: true, ...fieldProps, ...state.bottom, onEdit: (text) => { props.edit('bottom', text); }, onReset: () => { props.resetField('bottom'); } }), state.petSelectionFallback ? null : _jsx(ValueField, { id: "settings-pet-bubble-scale", label: t('settings.bubbleScale'), hint: t('settings.bubbleScaleHint'), numeric: true, ...fieldProps, ...state.bubbleScale, onEdit: (text) => { props.edit('bubbleScale', text); }, onReset: () => { props.resetField('bubbleScale'); } })] }));
264
292
  }
265
293
  /** Render the pet settings card as a first-level settings page. */
266
294
  export function PetSettingsSection(props) {
@@ -53,7 +53,6 @@ export declare const zh: {
53
53
  readonly 'pet.moreSessions': "展开其余 {n} 个会话的气泡";
54
54
  readonly 'pet.collapseSessions': "收起会话气泡";
55
55
  readonly 'settings.title': "宠物";
56
- readonly 'settings.diagnosticsTitle': "宠物目录诊断";
57
56
  readonly 'settings.description': "选择宠物并调整它的显示布局。";
58
57
  readonly 'settings.pet': "宠物";
59
58
  readonly 'settings.petHint': "选择显示哪只宠物;每只宠物独立命名,可在宠物悬浮面板改名。";
@@ -136,7 +135,6 @@ export declare const en: {
136
135
  readonly 'pet.moreSessions': "Expand {n} more session bubbles";
137
136
  readonly 'pet.collapseSessions': "Collapse session bubbles";
138
137
  readonly 'settings.title': "Pet";
139
- readonly 'settings.diagnosticsTitle': "Pet directory diagnostics";
140
138
  readonly 'settings.description': "Pick a pet and tune its display layout.";
141
139
  readonly 'settings.pet': "Pet";
142
140
  readonly 'settings.petHint': "Choose which pet shows. Names are stored per pet; rename from the pet hover panel.";
@@ -1 +1 @@
1
- {"version":3,"file":"locales.d.ts","sourceRoot":"","sources":["../../../src/client/locales.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,mDAAmD;AACnD,eAAO,MAAM,EAAE,QAAQ,CAAA;AAEvB,oBAAoB;AACpB,eAAO,MAAM,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAmFL,CAAA;AAEV,oBAAoB;AACpB,eAAO,MAAM,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAmFL,CAAA;AAEV,oCAAoC;AACpC,MAAM,MAAM,MAAM,GAAG,MAAM,OAAO,EAAE,CAAA;AAEpC,qDAAqD;AACrD,MAAM,MAAM,eAAe,GAAG,MAAM,CAAA;AAEpC;;;;;GAKG;AACH,wBAAgB,UAAU,IAAI,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAGnD;AAED;;;;;;;GAOG;AACH,wBAAgB,CAAC,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAQvE;AAED,OAAO,QAAQ,kCAAkC,CAAC;IAChD,UAAU,kBAAkB;QAC1B,uBAAuB;QACvB,GAAG,EAAE,MAAM,CAAA;KACZ;CACF"}
1
+ {"version":3,"file":"locales.d.ts","sourceRoot":"","sources":["../../../src/client/locales.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,mDAAmD;AACnD,eAAO,MAAM,EAAE,QAAQ,CAAA;AAEvB,oBAAoB;AACpB,eAAO,MAAM,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAkFL,CAAA;AAEV,oBAAoB;AACpB,eAAO,MAAM,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAkFL,CAAA;AAEV,oCAAoC;AACpC,MAAM,MAAM,MAAM,GAAG,MAAM,OAAO,EAAE,CAAA;AAEpC,qDAAqD;AACrD,MAAM,MAAM,eAAe,GAAG,MAAM,CAAA;AAEpC;;;;;GAKG;AACH,wBAAgB,UAAU,IAAI,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAGnD;AAED;;;;;;;GAOG;AACH,wBAAgB,CAAC,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAQvE;AAED,OAAO,QAAQ,kCAAkC,CAAC;IAChD,UAAU,kBAAkB;QAC1B,uBAAuB;QACvB,GAAG,EAAE,MAAM,CAAA;KACZ;CACF"}
@@ -55,7 +55,6 @@ export const zh = {
55
55
  'pet.collapseSessions': '收起会话气泡',
56
56
  // 一级设置页(settings.section 席位)。
57
57
  'settings.title': '宠物',
58
- 'settings.diagnosticsTitle': '宠物目录诊断',
59
58
  'settings.description': '选择宠物并调整它的显示布局。',
60
59
  'settings.pet': '宠物',
61
60
  'settings.petHint': '选择显示哪只宠物;每只宠物独立命名,可在宠物悬浮面板改名。',
@@ -140,7 +139,6 @@ export const en = {
140
139
  'pet.collapseSessions': 'Collapse session bubbles',
141
140
  // First-level settings section (the `settings.section` seat).
142
141
  'settings.title': 'Pet',
143
- 'settings.diagnosticsTitle': 'Pet directory diagnostics',
144
142
  'settings.description': 'Pick a pet and tune its display layout.',
145
143
  'settings.pet': 'Pet',
146
144
  'settings.petHint': 'Choose which pet shows. Names are stored per pet; rename from the pet hover panel.',
@@ -4,19 +4,50 @@
4
4
  * the loader accepts a standalone install of the same package side by side;
5
5
  * without this guard the second instance would still re-register the same
6
6
  * webserver routes, tools, settings namespaces, and system-prompt sections
7
- * and fail the boot. mountOnce makes the second host apply a no-op for the
8
- * lifetime of the first instance (the browser half is already deduped by
9
- * package name in the client module host).
7
+ * and fail the boot. mountOnce makes a mount of an already-mounted package a
8
+ * no-op for as long as the first instance lives (the browser half is already
9
+ * deduped by package name in the client module host).
10
+ *
11
+ * A no-op is only safe while the holder is ALIVE. The holder can be disposed
12
+ * long after the refused mount ran its course: the Host reloads a profile by
13
+ * creating the new loader entries before the old ones are torn down (a
14
+ * plugin-manager enable/disable/install write, a settings-driven row reload,
15
+ * HMR), so the new aggregate shell entry mounts its family plugin while the
16
+ * previous entry still owns the name. Dropping that refused mount lost the
17
+ * plugin for good - the previous entry then disposed its own mount, releasing
18
+ * the name with nobody left to take it, and the family row stayed listed as
19
+ * active while its host routes 404ed (the task-board panel showed
20
+ * "board.hostError.notMounted", no degraded record appeared, and only a Host
21
+ * restart recovered it).
22
+ *
23
+ * The refused mount is therefore QUEUED, not dropped, and replayed the moment
24
+ * the holder releases the name - if the waiting fiber is still alive then. The
25
+ * single-instance guarantee is unchanged: exactly one mount is live per
26
+ * package name, and the replay re-enters the guard so a later mount still
27
+ * dedupes against it.
10
28
  *
11
29
  * The registry rides a global symbol so two module instances of the same
12
- * package (npm copy vs repository link) still share one verdict. cordis
13
- * `ctx.effect` runs its callback immediately and treats the callback's
30
+ * package (npm copy vs repository link) still share one verdict. That symbol
31
+ * is a CROSS-REPOSITORY contract, not this file's private state: the four
32
+ * satellite packages (dsh-skins / dsh-pet / dsh-presets /
33
+ * dsh-community-plugins) are separate repositories carrying their own copy of
34
+ * this guard, rebuilt on their own schedule, so the value under `MOUNTED`
35
+ * must keep the shape every published copy reads (a `Set` of package names
36
+ * with `has`/`add`/`delete`). The wait queues this guard added therefore live
37
+ * under their own additive key, and the registry reads back a `Set` even when
38
+ * some other build left a different value there. Changing `MOUNTED`'s shape
39
+ * in place broke that contract once: a satellite's legacy copy created a
40
+ * `Set`, the family's new copy read it as a `Map`, and every family row
41
+ * mounted after it failed with "claims.get is not a function".
42
+ *
43
+ * cordis `ctx.effect` runs its callback immediately and treats the callback's
14
44
  * return value as the fiber disposer, so the unmarker is returned, not run.
15
45
  */
16
46
  /**
17
47
  * Wrap a cordis plugin apply so the package runs at most once per process.
18
- * The first mount registers normally and unmarks when its fiber disposes;
19
- * any later mount of the same package name is a no-op.
48
+ * The first mount registers normally and releases the name when its fiber
49
+ * disposes; a mount refused while that name is held waits for the release and
50
+ * then runs, unless its own fiber disposes first.
20
51
  * @param packageName - npm package identity shared by every install source.
21
52
  * @param fn - the original plugin apply.
22
53
  * @returns an apply of the same shape.
@@ -1 +1 @@
1
- {"version":3,"file":"mount-once.d.ts","sourceRoot":"","sources":["../../src/mount-once.ts"],"names":[],"mappings":"AACA;;;;;;;;;;;;;;GAcG;AAaH;;;;;;;GAOG;AACH,wBAAgB,SAAS,CAAC,CAAC,SAAS,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,OAAO,EAAE,WAAW,EAAE,MAAM,EAAE,EAAE,EAAE,CAAC,GAAG,CAAC,CAW9F"}
1
+ {"version":3,"file":"mount-once.d.ts","sourceRoot":"","sources":["../../src/mount-once.ts"],"names":[],"mappings":"AACA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4CG;AA6CH;;;;;;;;GAQG;AACH,wBAAgB,SAAS,CAAC,CAAC,SAAS,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,OAAO,EAAE,WAAW,EAAE,MAAM,EAAE,EAAE,EAAE,CAAC,GAAG,CAAC,CA0C9F"}