@lmzhen/dsh-evolution-settings-ui 0.7.0 → 0.8.0
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.md +10 -1
- package/lib/client.js +55 -20
- package/lib/types/client/ParamCard.d.ts +14 -3
- package/lib/types/client/settle.d.ts +25 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -16,12 +16,20 @@ The cards claim the keyed child slot per namespace, so a namespace the Host does
|
|
|
16
16
|
|
|
17
17
|
## What a card does
|
|
18
18
|
|
|
19
|
-
A card renders the namespace's user-writable (E3) fields with the registry's Chinese label, its unit, a 用户/部署 source chip, a control **typed from the registry** (switch / select / number / text) holding the current value, the registry's hint, and a per-field 恢复部署默认 action when a user override exists. The card's foot carries one 放弃修改 / 保存 pair: edits live in a draft, 保存 writes only the dirty fields in order and
|
|
19
|
+
A card renders the namespace's user-writable (E3) fields with the registry's Chinese label, its unit, a 用户/部署 source chip, a control **typed from the registry** (switch / select / number / text) holding the current value, the registry's hint, and a per-field 恢复部署默认 action when a user override exists. The card's foot carries one 放弃修改 / 保存 pair: edits live in a draft, 保存 writes only the dirty fields in order, and a settling effect clears the draft once the Host kept every written key.
|
|
20
|
+
|
|
21
|
+
Success is never assumed from the write call: the client settings scope RESOLVES a refused write (it recovers the snapshot and returns; the remote call never rejects). Both scopes this bundle runs on — the platform's own controller and the bridge variant — finish that recovery read BEFORE the write promise resolves, so the raw user section read in the settling effect is already the verdict: a field the Host refused leaves no user key. The card then reports the refusal under the fields and keeps the draft, so nothing the operator typed is lost and a refusal never looks like a silent revert. The verdict cannot be read from the save callback itself: the raw snapshot is reachable only through the render-time seat (see the next section).
|
|
20
22
|
|
|
21
23
|
Writes go through the client settings scope (`set`/`unset`), which carries the revision it read as the write's fence; a field whose key is present in the raw user section reads as a user override even when its value equals the deployment value.
|
|
22
24
|
|
|
23
25
|
The field list AND the per-field UI metadata are GENERATED from the parameter registry (`packages/scripts/gen-param-client-view.mjs` writes `src/client/generated-params.ts`): id, group, the English summary, and the E3 rows' label / hint / control / unit / values. The browser half therefore cannot name a parameter the Host does not register, and cannot invent a control the registry does not declare. Regenerate after every registry edit; the family gate runs the generator with `--check`.
|
|
24
26
|
|
|
27
|
+
## The `hooks` compartment never reaches the component
|
|
28
|
+
|
|
29
|
+
A card's inject face carries `hooks: { paramSection: source }` — that is the SHELL's seat, not a prop. The renderer binds each entry to a `use<Name>` seat and hands the component the face with `hooks` REMOVED (the slot contract is `PropsHooks<face['hooks']>` plus `Omit<face, 'hooks'>`). Reading `props.hooks` therefore reads a prop that never exists, and the failure only shows up at runtime, inside the save path.
|
|
30
|
+
|
|
31
|
+
The props type is spelled the way the renderer derives it — `Omit<ParamCardFace, 'hooks'> & { useParamSection }` — so re-introducing that mistake fails `tsc` instead of failing the operator's save. 0.7.0 shipped the mistake; 0.7.1 fixed it and derived the type this way.
|
|
32
|
+
|
|
25
33
|
## Styling
|
|
26
34
|
|
|
27
35
|
The design system's CSS is not exported to packages outside the platform repository, so this bundle carries its own stylesheet and injects it once behind a `<style data-plugin-css="…">` tag — the mechanism the platform's own client bundles use. Every rule reads a `--dsw-alias-*` design token, so light and dark follow the theme without a colour of our own, and the field metrics (12px padding, 6px gap, 13px label, 12px hint) copy the platform's settings fields.
|
|
@@ -40,3 +48,4 @@ The browser half ships as the client module system's lazy CJS factory artifact (
|
|
|
40
48
|
- The section renders plain controls styled with the design tokens rather than the platform's `@deepseek-ai/dsh-client-ui-primitives` kit: that module IS available to out-of-repo bundles (the market plugin requires it), but its prop shapes are not published, and guessing them would break the live GUI.
|
|
41
49
|
- An unsaved draft lives in the card's own component state and the settings shell renders only the active section, so switching to another section drops a draft that was not saved yet. Values already written are unaffected; moving the draft into an apply-time store is the 0.7.x follow-up.
|
|
42
50
|
- A deployment that overrides a field through the `evolution-policy` row does not show on the card: the card reports the deployment value the settings scope serves, while the policy snapshot can differ. That divergence stays a doctor / `/evolution params` matter.
|
|
51
|
+
- The browser half has no rendered spec: the family specs are Node-level, so this package's client code is covered by `tsc`, the bundle build and the gate steps, plus a live pass on the installed artifact (0.7.0's save defect was found exactly there). A spec that renders the card with the props the renderer actually builds — no `hooks`, the bound seat stubbed — is the follow-up that catches this verdict path in CI.
|
package/lib/client.js
CHANGED
|
@@ -435,6 +435,29 @@ window.__ModuleLoader__.load({
|
|
|
435
435
|
return language === "zh" ? zh[key] : en[key];
|
|
436
436
|
}
|
|
437
437
|
//#endregion
|
|
438
|
+
//#region src/client/settle.ts
|
|
439
|
+
/**
|
|
440
|
+
* Structural equality for the values a parameter can hold.
|
|
441
|
+
* @param a - one side of the comparison.
|
|
442
|
+
* @param b - the other side.
|
|
443
|
+
* @returns true when the two are the same value.
|
|
444
|
+
*/
|
|
445
|
+
function sameValue(a, b) {
|
|
446
|
+
if (Object.is(a, b)) return true;
|
|
447
|
+
if (typeof a !== typeof b || a === null || b === null) return false;
|
|
448
|
+
return JSON.stringify(a) === JSON.stringify(b);
|
|
449
|
+
}
|
|
450
|
+
/**
|
|
451
|
+
* Whether every staged write is visible in the user layer.
|
|
452
|
+
* @param pending - the writes awaiting a verdict.
|
|
453
|
+
* @param user - the user layer of the post-write snapshot; a non-section reads as empty.
|
|
454
|
+
* @returns true when each staged id now holds the value that was requested.
|
|
455
|
+
*/
|
|
456
|
+
function landedWrites(pending, user) {
|
|
457
|
+
const section = typeof user === "object" && user !== null && !Array.isArray(user) ? user : {};
|
|
458
|
+
return pending.every((entry) => sameValue(section[entry.id], entry.want));
|
|
459
|
+
}
|
|
460
|
+
//#endregion
|
|
438
461
|
//#region src/client/ParamCard.ts
|
|
439
462
|
/**
|
|
440
463
|
* One namespace's parameter card.
|
|
@@ -481,12 +504,6 @@ window.__ModuleLoader__.load({
|
|
|
481
504
|
function isSection(value) {
|
|
482
505
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
483
506
|
}
|
|
484
|
-
/** Let the settings store settle one write before the read-back (it batches updates). */
|
|
485
|
-
async function settle() {
|
|
486
|
-
await new Promise((done) => {
|
|
487
|
-
setTimeout(done, 60);
|
|
488
|
-
});
|
|
489
|
-
}
|
|
490
507
|
/** One field's control, typed from the registry. */
|
|
491
508
|
function FieldControl(props) {
|
|
492
509
|
const { field, text } = props;
|
|
@@ -554,12 +571,14 @@ window.__ModuleLoader__.load({
|
|
|
554
571
|
const [draft, setDraft] = (0, react.useState)({});
|
|
555
572
|
const [busy, setBusy] = (0, react.useState)(false);
|
|
556
573
|
const [error, setError] = (0, react.useState)("");
|
|
574
|
+
const [pending, setPending] = (0, react.useState)(null);
|
|
557
575
|
const snapshot = props.useParamSection((state) => state);
|
|
558
576
|
if (snapshot.status === "loading") return (0, react.createElement)("p", { className: "evolution-param-note" }, t("loading"));
|
|
559
577
|
if (snapshot.status === "unavailable") return (0, react.createElement)("p", { className: "evolution-param-note" }, t("unavailable"));
|
|
560
578
|
const user = isSection(snapshot.user) ? snapshot.user : {};
|
|
561
579
|
const value = isSection(snapshot.value) ? snapshot.value : {};
|
|
562
580
|
const disabled = !snapshot.writable;
|
|
581
|
+
const locked = disabled || busy;
|
|
563
582
|
const titleKey = NAMESPACE_TITLES[namespace];
|
|
564
583
|
const title = titleKey === void 0 ? namespace : t(titleKey);
|
|
565
584
|
const changed = fields.filter((field) => Object.hasOwn(user, field.id)).length;
|
|
@@ -571,27 +590,43 @@ window.__ModuleLoader__.load({
|
|
|
571
590
|
[id]: next
|
|
572
591
|
});
|
|
573
592
|
};
|
|
593
|
+
/**
|
|
594
|
+
* Settle a staged write against the Host's answer.
|
|
595
|
+
*
|
|
596
|
+
* A resolved write promise is not the verdict: both settings scopes this bundle runs
|
|
597
|
+
* on (the shell's own controller and the bridge variant) finish their recovery read
|
|
598
|
+
* BEFORE it resolves, so a value the Host refused is simply still the previous one.
|
|
599
|
+
* Presence in the user layer therefore cannot answer the question — on a field the
|
|
600
|
+
* operator had already overridden, a refused value's key is present as well. The
|
|
601
|
+
* verdict asks whether each staged id now holds the value this card requested.
|
|
602
|
+
* The check lives in an effect, not in the save callback, because the raw snapshot is
|
|
603
|
+
* reachable only through the render-time seat — the face's `hooks` compartment never
|
|
604
|
+
* reaches the component.
|
|
605
|
+
*/
|
|
606
|
+
(0, react.useEffect)(() => {
|
|
607
|
+
if (pending === null) return;
|
|
608
|
+
setPending(null);
|
|
609
|
+
setBusy(false);
|
|
610
|
+
if (landedWrites(pending, snapshot.user)) setDraft({});
|
|
611
|
+
else setError(t("refused"));
|
|
612
|
+
}, [
|
|
613
|
+
pending,
|
|
614
|
+
snapshot.user,
|
|
615
|
+
t
|
|
616
|
+
]);
|
|
574
617
|
const save = () => {
|
|
575
618
|
setBusy(true);
|
|
576
619
|
setError("");
|
|
577
620
|
(async () => {
|
|
578
621
|
try {
|
|
579
622
|
for (const field of dirty) await write(field.id, parseFor(field.control, textOf(field)));
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
landed = isSection(fresh.user) ? fresh.user : {};
|
|
585
|
-
}
|
|
586
|
-
if (dirty.some((field) => !Object.hasOwn(landed, field.id))) {
|
|
587
|
-
setError(t("refused"));
|
|
588
|
-
return;
|
|
589
|
-
}
|
|
590
|
-
setDraft({});
|
|
623
|
+
setPending(dirty.map((field) => ({
|
|
624
|
+
id: field.id,
|
|
625
|
+
want: parseFor(field.control, textOf(field))
|
|
626
|
+
})));
|
|
591
627
|
} catch (caught) {
|
|
592
|
-
setError(caught instanceof Error && caught.message !== "" ? caught.message : t("refused"));
|
|
593
|
-
} finally {
|
|
594
628
|
setBusy(false);
|
|
629
|
+
setError(caught instanceof Error && caught.message !== "" ? caught.message : t("refused"));
|
|
595
630
|
}
|
|
596
631
|
})();
|
|
597
632
|
};
|
|
@@ -607,7 +642,7 @@ window.__ModuleLoader__.load({
|
|
|
607
642
|
field,
|
|
608
643
|
text: textOf(field),
|
|
609
644
|
overridden: Object.hasOwn(user, field.id),
|
|
610
|
-
disabled,
|
|
645
|
+
disabled: locked,
|
|
611
646
|
t,
|
|
612
647
|
onChange: change,
|
|
613
648
|
clear
|
|
@@ -17,7 +17,14 @@ import { type ReactNode } from 'react';
|
|
|
17
17
|
import type { ClientParamField } from './generated-params.ts';
|
|
18
18
|
import { type MessageKey } from './messages.ts';
|
|
19
19
|
import type { ParamSectionSnapshot, ParamSectionSource } from './seam.ts';
|
|
20
|
-
/**
|
|
20
|
+
/**
|
|
21
|
+
* Injected face: plain data and callbacks, plus the hook seat.
|
|
22
|
+
*
|
|
23
|
+
* The `hooks` compartment belongs to the shell, not to the component: the
|
|
24
|
+
* renderer binds its entries to `use<Name>` props and omits `hooks` from what
|
|
25
|
+
* the component receives. A card that reads `props.hooks` therefore reads a prop
|
|
26
|
+
* that never exists.
|
|
27
|
+
*/
|
|
21
28
|
export interface ParamCardFace {
|
|
22
29
|
namespace: string;
|
|
23
30
|
fields: readonly ClientParamField[];
|
|
@@ -28,8 +35,12 @@ export interface ParamCardFace {
|
|
|
28
35
|
paramSection: ParamSectionSource;
|
|
29
36
|
};
|
|
30
37
|
}
|
|
31
|
-
/**
|
|
32
|
-
|
|
38
|
+
/**
|
|
39
|
+
* Props the renderer binds for one card: the inject face minus its hook compartment,
|
|
40
|
+
* plus the seats bound from it. Spelled the way the shell derives it, so reaching for
|
|
41
|
+
* `props.hooks` fails the type check instead of failing at runtime.
|
|
42
|
+
*/
|
|
43
|
+
export type ParamCardProps = Omit<ParamCardFace, 'hooks'> & {
|
|
33
44
|
/** Bound from \`hooks.paramSection\` by the renderer. */
|
|
34
45
|
readonly useParamSection: <T>(selector: (state: ParamSectionSnapshot) => T) => T;
|
|
35
46
|
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The save verdict: did the Host take the values this card asked for?
|
|
3
|
+
*
|
|
4
|
+
* A write promise that resolves is not a verdict. Both settings scopes this bundle runs
|
|
5
|
+
* on finish a recovery read before it resolves, so a value the Host refused is simply
|
|
6
|
+
* still the previous one. Presence in the user layer cannot tell refusal from acceptance
|
|
7
|
+
* on a field the operator had already overridden — the refused value's key is present
|
|
8
|
+
* too — so the verdict compares what each staged id holds now against what was requested.
|
|
9
|
+
* @module @lmzhen/dsh-evolution-settings-ui
|
|
10
|
+
*/
|
|
11
|
+
/** One staged write awaiting its verdict. */
|
|
12
|
+
export interface PendingWrite {
|
|
13
|
+
/** Parameter id the write named. */
|
|
14
|
+
readonly id: string;
|
|
15
|
+
/** The value handed to the scope's `set`. */
|
|
16
|
+
readonly want: unknown;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Whether every staged write is visible in the user layer.
|
|
20
|
+
* @param pending - the writes awaiting a verdict.
|
|
21
|
+
* @param user - the user layer of the post-write snapshot; a non-section reads as empty.
|
|
22
|
+
* @returns true when each staged id now holds the value that was requested.
|
|
23
|
+
*/
|
|
24
|
+
export declare function landedWrites(pending: readonly PendingWrite[], user: unknown): boolean;
|
|
25
|
+
//# sourceMappingURL=settle.d.ts.map
|
package/package.json
CHANGED