@linxin666/dsh-pet 0.4.2 → 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/README.i18n.yaml +2 -2
- package/README.md +16 -28
- package/README.zh.md +16 -28
- package/lib/client.js +74 -65
- package/lib/client.js.map +1 -1
- package/lib/index.js +85 -12
- package/lib/types/client/PetSettingsCard.d.ts +16 -12
- package/lib/types/client/PetSettingsCard.d.ts.map +1 -1
- package/lib/types/client/PetSettingsCard.js +77 -49
- package/lib/types/client/locales.d.ts +0 -2
- package/lib/types/client/locales.d.ts.map +1 -1
- package/lib/types/client/locales.js +0 -2
- package/lib/types/mount-once.d.ts +38 -7
- package/lib/types/mount-once.d.ts.map +1 -1
- package/lib/types/mount-once.js +96 -12
- package/package.json +1 -1
- package/src/client/PetSettingsCard.test.tsx +115 -4
- package/src/client/PetSettingsCard.tsx +76 -66
- package/src/client/locales.ts +0 -2
- package/src/client/pet-css.test.ts +8 -0
- package/src/client/settings-section.module.css +0 -23
- package/src/mount-once.ts +107 -13
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
|
|
6011
|
-
*
|
|
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.
|
|
6016
|
-
*
|
|
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
|
-
|
|
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
|
|
6027
|
-
*
|
|
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
|
-
|
|
6082
|
+
const mount = (...args) => {
|
|
6034
6083
|
const mounted = mountedSet();
|
|
6035
|
-
|
|
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
|
-
|
|
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
|
|
89
|
+
private loadPetState;
|
|
98
90
|
private fallback;
|
|
99
|
-
|
|
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":"AAAA;;;;;;;;;GASG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AACtC,OAAO,KAAK,EAAE,UAAU,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,kCAAkC,CAAA;AAC7F,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,4CAA4C,CAAA;AAC5E,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,+BAA+B,CAAA;AAIlE,OAAO,EAAoD,KAAK,WAAW,EAAE,KAAK,SAAS,EAAE,KAAK,UAAU,IAAI,cAAc,EAAE,MAAM,oBAAoB,CAAA;AAG1J,+EAA+E;AAC/E,MAAM,WAAW,WAAW;IAC1B,oCAAoC;IACpC,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,qBAAqB;IACrB,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,4DAA4D;IAC5D,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,yDAAyD;IACzD,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,wDAAwD;IACxD,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,4EAA4E;IAC5E,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,0CAA0C;IAC1C,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,6DAA6D;IAC7D,iBAAiB,CAAC,EAAE,OAAO,CAAA;CAC5B;AAED,0CAA0C;AAC1C,MAAM,WAAW,oBAAqB,SAAQ,SAAS;IACrD,+FAA+F;IAC/F,oBAAoB,EAAE,OAAO,CAAA;IAC7B,4BAA4B;IAC5B,OAAO,EAAE,cAAc,CAAA;IACvB,qBAAqB;IACrB,OAAO,EAAE,cAAc,CAAA;IACvB,iBAAiB;IACjB,IAAI,EAAE,cAAc,CAAA;IACpB,mBAAmB;IACnB,KAAK,EAAE,cAAc,CAAA;IACrB,oBAAoB;IACpB,MAAM,EAAE,cAAc,CAAA;IACtB,oCAAoC;IACpC,WAAW,EAAE,cAAc,CAAA;IAC3B,oBAAoB;IACpB,KAAK,EAAE,cAAc,CAAA;IACrB,uCAAuC;IACvC,iBAAiB,EAAE,cAAc,CAAA;IACjC,wEAAwE;IACxE,UAAU,EAAE,SAAS;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,EAAE,CAAA;
|
|
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
|
-
/**
|
|
13
|
-
async function
|
|
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
|
-
|
|
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.
|
|
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
|
|
96
|
+
async loadPetState() {
|
|
116
97
|
try {
|
|
117
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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.
|
|
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'); } }),
|
|
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
|
|
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
|
|
8
|
-
*
|
|
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.
|
|
13
|
-
*
|
|
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
|
|
19
|
-
*
|
|
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
|
|
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"}
|