@lmzhen/dsh-evolution-settings-ui 0.15.2 → 0.16.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 +18 -1
- package/lib/client.js +402 -3
- package/lib/types/client/PendingCard.d.ts +29 -0
- package/lib/types/client/api.d.ts +61 -0
- package/lib/types/client/approval-routes.d.ts +17 -0
- package/lib/types/client/index.d.ts +7 -0
- package/lib/types/client/messages.d.ts +23 -0
- package/lib/types/client/pending-state.d.ts +73 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -24,6 +24,23 @@ Writes go through the client settings scope (`set`/`unset`), which carries the r
|
|
|
24
24
|
|
|
25
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 / valueLabels. 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`.
|
|
26
26
|
|
|
27
|
+
## The pending-write card
|
|
28
|
+
|
|
29
|
+
One card in the same section is not a parameter namespace: **「待批写入」** lists the staged self-evolution
|
|
30
|
+
writes and carries the two decisions plus a preview. It reads the approval plugin's loopback routes
|
|
31
|
+
(`src/client/api.ts` + `approval-routes.ts`; the path literals are compared against the host table by a spec),
|
|
32
|
+
wears the same card shell and button vocabulary as the parameter cards, and invents no state of its own.
|
|
33
|
+
|
|
34
|
+
Its states are deliberately distinct: **reading** ("正在读取待批项…"), **empty** ("没有待批的写入") and
|
|
35
|
+
**failed** (the reason plus a retry). Merging the first two is what once made a slow answer read as lost data
|
|
36
|
+
(E7), so the state machine lives in `pending-state.ts` and the three are asserted without a browser.
|
|
37
|
+
|
|
38
|
+
A decision re-reads the window instead of editing the list locally (the host owns the state) and shows the
|
|
39
|
+
host's own sentence when it refuses. **Preview** opens one row at a time: the facts line (`n 行新增` /
|
|
40
|
+
`n 行删除` / truncated) and the source block with `-`/`+` lines, the same shape the history panel's diff
|
|
41
|
+
uses; an unavailable preview prints the host's reason. A late answer for a row the reader already left is
|
|
42
|
+
dropped rather than painted under another row's heading.
|
|
43
|
+
|
|
27
44
|
## The `hooks` compartment never reaches the component
|
|
28
45
|
|
|
29
46
|
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.
|
|
@@ -48,4 +65,4 @@ The browser half ships as the client module system's lazy CJS factory artifact (
|
|
|
48
65
|
- 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.
|
|
49
66
|
- 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.
|
|
50
67
|
- 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
|
|
68
|
+
- The parameter card has no RENDERED spec: the family specs are Node-level and the family carries no React runtime, so it is covered by `tsc`, the bundle build, the gate steps and a live pass on the installed artifact (0.7.0's save defect was found exactly there). The verdict is pinned instead by `tests/settle.spec.ts` (the pure `landedWrites` decision, including the already-overridden refusal case); rendering the card with the props the renderer actually builds — no `hooks`, the bound seat stubbed — remains the follow-up that would catch the wiring in CI.
|
package/lib/client.js
CHANGED
|
@@ -5,6 +5,76 @@ window.__ModuleLoader__.load({
|
|
|
5
5
|
var exports = module.exports;
|
|
6
6
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
7
7
|
let react = require("react");
|
|
8
|
+
//#region src/client/approval-routes.ts
|
|
9
|
+
/**
|
|
10
|
+
* The approval paths this browser half calls.
|
|
11
|
+
*
|
|
12
|
+
* A browser half cannot import the host package's table at runtime (the family's client bundles treat
|
|
13
|
+
* every `@deepseek-ai/*` specifier as external), so the literals are declared here and a spec compares
|
|
14
|
+
* them against `APPROVAL_ROUTES` in `evolution-approval` — a rename on one side would otherwise surface
|
|
15
|
+
* as a 404 in the GUI instead of in CI.
|
|
16
|
+
* @module @lmzhen/dsh-evolution-settings-ui/client/approval-routes
|
|
17
|
+
*/
|
|
18
|
+
/** One path per call the pending card makes. */
|
|
19
|
+
const APPROVAL_CLIENT_ROUTES = {
|
|
20
|
+
pending: "/api/dsh-evolution/approval/pending",
|
|
21
|
+
approve: "/api/dsh-evolution/approval/approve",
|
|
22
|
+
reject: "/api/dsh-evolution/approval/reject",
|
|
23
|
+
preview: "/api/dsh-evolution/approval/preview"
|
|
24
|
+
};
|
|
25
|
+
//#endregion
|
|
26
|
+
//#region src/client/api.ts
|
|
27
|
+
/**
|
|
28
|
+
* The approval routes this bundle calls, and the ONE place a request is made.
|
|
29
|
+
*
|
|
30
|
+
* Structural types on purpose: the browser half cannot import the host package at runtime, so the row
|
|
31
|
+
* shape is declared by what the route hands over. Every answer — including a refusal — is parsed the
|
|
32
|
+
* same way, because a refusal is a business answer the card shows as a sentence, not a transport error.
|
|
33
|
+
* @module @lmzhen/dsh-evolution-settings-ui/client/api
|
|
34
|
+
*/
|
|
35
|
+
/** Read one JSON answer, mapping every failure onto the same refusal shape. */
|
|
36
|
+
async function read(doFetch, path, init) {
|
|
37
|
+
try {
|
|
38
|
+
const response = await doFetch(path, init);
|
|
39
|
+
if (!response.ok) return {
|
|
40
|
+
ok: false,
|
|
41
|
+
message: "HTTP " + String(response.status)
|
|
42
|
+
};
|
|
43
|
+
const body = await response.json();
|
|
44
|
+
if (body.ok === true) return {
|
|
45
|
+
ok: true,
|
|
46
|
+
data: body.data
|
|
47
|
+
};
|
|
48
|
+
return {
|
|
49
|
+
ok: false,
|
|
50
|
+
message: typeof body.message === "string" && body.message !== "" ? body.message : "the host refused the request"
|
|
51
|
+
};
|
|
52
|
+
} catch (error) {
|
|
53
|
+
return {
|
|
54
|
+
ok: false,
|
|
55
|
+
message: error instanceof Error ? error.message : String(error)
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Build the approval face over one fetch implementation.
|
|
61
|
+
* @param doFetch - the fetch to use (the platform's global in the browser, a fake in a spec).
|
|
62
|
+
* @returns the three calls the pending card makes.
|
|
63
|
+
*/
|
|
64
|
+
function createApprovalApi(doFetch) {
|
|
65
|
+
const post = (path, id) => read(doFetch, path, {
|
|
66
|
+
method: "POST",
|
|
67
|
+
headers: { "content-type": "application/json" },
|
|
68
|
+
body: JSON.stringify({ id })
|
|
69
|
+
});
|
|
70
|
+
return {
|
|
71
|
+
pending: async () => await read(doFetch, APPROVAL_CLIENT_ROUTES.pending),
|
|
72
|
+
approve: async (id) => await post(APPROVAL_CLIENT_ROUTES.approve, id),
|
|
73
|
+
reject: async (id) => await post(APPROVAL_CLIENT_ROUTES.reject, id),
|
|
74
|
+
preview: async (id) => await read(doFetch, APPROVAL_CLIENT_ROUTES.preview + "?id=" + encodeURIComponent(id))
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
//#endregion
|
|
8
78
|
//#region src/client/generated-params.ts
|
|
9
79
|
const CLIENT_PARAM_SECTIONS = [
|
|
10
80
|
{
|
|
@@ -422,6 +492,267 @@ window.__ModuleLoader__.load({
|
|
|
422
492
|
}
|
|
423
493
|
];
|
|
424
494
|
//#endregion
|
|
495
|
+
//#region src/client/pending-state.ts
|
|
496
|
+
/** The state a card mounts in: still loading, nothing in flight, nothing said yet. */
|
|
497
|
+
const INITIAL_PENDING_STATE = {
|
|
498
|
+
view: { kind: "loading" },
|
|
499
|
+
busy: [],
|
|
500
|
+
notice: null,
|
|
501
|
+
open: null,
|
|
502
|
+
preview: null
|
|
503
|
+
};
|
|
504
|
+
/** Open one row's preview: the row is now the open one and its answer is on the way. */
|
|
505
|
+
function expanded(state, id) {
|
|
506
|
+
return {
|
|
507
|
+
...state,
|
|
508
|
+
open: id,
|
|
509
|
+
preview: { kind: "loading" }
|
|
510
|
+
};
|
|
511
|
+
}
|
|
512
|
+
/** Close whatever preview is open. */
|
|
513
|
+
function collapsed(state) {
|
|
514
|
+
return {
|
|
515
|
+
...state,
|
|
516
|
+
open: null,
|
|
517
|
+
preview: null
|
|
518
|
+
};
|
|
519
|
+
}
|
|
520
|
+
/**
|
|
521
|
+
* A preview answer arrived. An answer for a row the reader has already left is DROPPED: it would
|
|
522
|
+
* otherwise paint one row's diff under another row's heading.
|
|
523
|
+
*/
|
|
524
|
+
function previewArrived(state, id, view) {
|
|
525
|
+
return state.open === id ? {
|
|
526
|
+
...state,
|
|
527
|
+
preview: view
|
|
528
|
+
} : state;
|
|
529
|
+
}
|
|
530
|
+
/** The first load, or a refresh after a decision: back to loading, keeping no stale rows. */
|
|
531
|
+
function loading(state) {
|
|
532
|
+
return {
|
|
533
|
+
...state,
|
|
534
|
+
view: { kind: "loading" }
|
|
535
|
+
};
|
|
536
|
+
}
|
|
537
|
+
/**
|
|
538
|
+
* A list arrived. An empty list is its OWN state — the reader is told the window is clear rather than
|
|
539
|
+
* being shown the same line a slow answer shows.
|
|
540
|
+
*/
|
|
541
|
+
function loaded(state, rows) {
|
|
542
|
+
return {
|
|
543
|
+
...state,
|
|
544
|
+
view: rows.length === 0 ? { kind: "empty" } : {
|
|
545
|
+
kind: "ready",
|
|
546
|
+
rows
|
|
547
|
+
}
|
|
548
|
+
};
|
|
549
|
+
}
|
|
550
|
+
/** The list could not be read: the reason stays on screen next to a retry control. */
|
|
551
|
+
function failed(state, message) {
|
|
552
|
+
return {
|
|
553
|
+
...state,
|
|
554
|
+
view: {
|
|
555
|
+
kind: "failed",
|
|
556
|
+
message
|
|
557
|
+
}
|
|
558
|
+
};
|
|
559
|
+
}
|
|
560
|
+
/** Mark one record as having an action in flight (its buttons are disabled until it ends). */
|
|
561
|
+
function beginAction(state, id) {
|
|
562
|
+
return state.busy.includes(id) ? state : {
|
|
563
|
+
...state,
|
|
564
|
+
busy: [...state.busy, id]
|
|
565
|
+
};
|
|
566
|
+
}
|
|
567
|
+
/** The action ended, whatever it answered. */
|
|
568
|
+
function endAction(state, id) {
|
|
569
|
+
return {
|
|
570
|
+
...state,
|
|
571
|
+
busy: state.busy.filter((candidate) => candidate !== id)
|
|
572
|
+
};
|
|
573
|
+
}
|
|
574
|
+
/** One refusal or failure line under the list; `null` clears it (a later success does). */
|
|
575
|
+
function noticed(state, message) {
|
|
576
|
+
return {
|
|
577
|
+
...state,
|
|
578
|
+
notice: message
|
|
579
|
+
};
|
|
580
|
+
}
|
|
581
|
+
//#endregion
|
|
582
|
+
//#region src/client/PendingCard.ts
|
|
583
|
+
/**
|
|
584
|
+
* The "writes awaiting approval" card: the staged write window, with the two decisions on each row.
|
|
585
|
+
*
|
|
586
|
+
* It is a card in the SAME section and wears the SAME shell as the parameter cards (header with title,
|
|
587
|
+
* count and chevron; body of field blocks; a button row per block), so nothing here invents a second
|
|
588
|
+
* visual language. Two things differ on purpose: the data comes from the host's approval routes rather
|
|
589
|
+
* than the settings transport, and its three states are distinct — "reading", "nothing staged" and "the
|
|
590
|
+
* read failed" each say so in their own words (E7: merging the first two is what made an empty panel
|
|
591
|
+
* look like lost data).
|
|
592
|
+
*
|
|
593
|
+
* Every decision re-reads the window instead of predicting the outcome: the host owns the state, and a
|
|
594
|
+
* refusal is shown with the host's own sentence.
|
|
595
|
+
* @module @lmzhen/dsh-evolution-settings-ui/client/PendingCard
|
|
596
|
+
*/
|
|
597
|
+
/** Which word one age bucket gets; an unknown bucket falls back to the count form. */
|
|
598
|
+
const AGE_KEYS = Object.freeze({
|
|
599
|
+
now: "approvalAgeNow",
|
|
600
|
+
minutes: "approvalAgeMinutes",
|
|
601
|
+
hours: "approvalAgeHours",
|
|
602
|
+
days: "approvalAgeDays",
|
|
603
|
+
months: "approvalAgeMonths"
|
|
604
|
+
});
|
|
605
|
+
/** Which word one staged kind gets. */
|
|
606
|
+
const KIND_KEYS = Object.freeze({
|
|
607
|
+
memory: "approvalKindMemory",
|
|
608
|
+
skill: "approvalKindSkill",
|
|
609
|
+
capability: "approvalKindCapability"
|
|
610
|
+
});
|
|
611
|
+
/**
|
|
612
|
+
* Render the pending window.
|
|
613
|
+
* @param props - the copy seat and the host calls.
|
|
614
|
+
* @returns the card.
|
|
615
|
+
*/
|
|
616
|
+
function PendingCard(props) {
|
|
617
|
+
const { t, api } = props;
|
|
618
|
+
const [state, setState] = (0, react.useState)(INITIAL_PENDING_STATE);
|
|
619
|
+
const [open, setOpen] = (0, react.useState)(true);
|
|
620
|
+
const latest = (0, react.useRef)(state);
|
|
621
|
+
const apply = (next) => {
|
|
622
|
+
const value = next(latest.current);
|
|
623
|
+
latest.current = value;
|
|
624
|
+
setState(value);
|
|
625
|
+
};
|
|
626
|
+
const load = async () => {
|
|
627
|
+
apply(loading);
|
|
628
|
+
const answer = await api.pending();
|
|
629
|
+
apply((current) => answer.ok ? loaded(current, answer.data) : failed(current, answer.message));
|
|
630
|
+
};
|
|
631
|
+
(0, react.useEffect)(() => {
|
|
632
|
+
load();
|
|
633
|
+
}, []);
|
|
634
|
+
const decide = (id, action) => {
|
|
635
|
+
apply((current) => beginAction(current, id));
|
|
636
|
+
(async () => {
|
|
637
|
+
const answer = action === "approve" ? await api.approve(id) : await api.reject(id);
|
|
638
|
+
if (!answer.ok) {
|
|
639
|
+
apply((current) => noticed(endAction(current, id), answer.message));
|
|
640
|
+
return;
|
|
641
|
+
}
|
|
642
|
+
apply((current) => noticed(endAction(current, id), null));
|
|
643
|
+
await load();
|
|
644
|
+
})();
|
|
645
|
+
};
|
|
646
|
+
const age = (row) => {
|
|
647
|
+
const key = AGE_KEYS[row.age.unit];
|
|
648
|
+
if (key === void 0) return String(row.age.n);
|
|
649
|
+
return t(key).replace("{n}", String(row.age.n));
|
|
650
|
+
};
|
|
651
|
+
/** Ask the host what this record would store; the answer lands only if the row is still open. */
|
|
652
|
+
const loadPreview = async (id) => {
|
|
653
|
+
const answer = await api.preview(id);
|
|
654
|
+
const view = answer.ok ? answer.data.available ? {
|
|
655
|
+
kind: "ready",
|
|
656
|
+
answer: answer.data
|
|
657
|
+
} : {
|
|
658
|
+
kind: "unavailable",
|
|
659
|
+
reason: answer.data.reason
|
|
660
|
+
} : {
|
|
661
|
+
kind: "failed",
|
|
662
|
+
message: answer.message
|
|
663
|
+
};
|
|
664
|
+
apply((current) => previewArrived(current, id, view));
|
|
665
|
+
};
|
|
666
|
+
const togglePreview = (id) => {
|
|
667
|
+
if (state.open === id) {
|
|
668
|
+
apply(collapsed);
|
|
669
|
+
return;
|
|
670
|
+
}
|
|
671
|
+
apply((current) => expanded(current, id));
|
|
672
|
+
loadPreview(id);
|
|
673
|
+
};
|
|
674
|
+
/** The facts line, then the source view: the same shape the history panel's diff uses. */
|
|
675
|
+
const preview = (view) => {
|
|
676
|
+
if (view.kind === "loading") return (0, react.createElement)("p", { className: "evolution-param-note" }, t("approvalPreviewReading"));
|
|
677
|
+
if (view.kind === "failed") return (0, react.createElement)("p", { className: "evolution-param-error" }, view.message);
|
|
678
|
+
if (view.kind === "unavailable") return (0, react.createElement)("p", { className: "evolution-param-error" }, t("approvalPreviewNone") + view.reason);
|
|
679
|
+
const facts = [
|
|
680
|
+
(0, react.createElement)("span", {
|
|
681
|
+
className: "evolution-param-fact",
|
|
682
|
+
key: "added"
|
|
683
|
+
}, t("approvalDiffAdded").replace("{n}", String(view.answer.linesAdded))),
|
|
684
|
+
(0, react.createElement)("span", {
|
|
685
|
+
className: "evolution-param-fact",
|
|
686
|
+
key: "removed"
|
|
687
|
+
}, t("approvalDiffRemoved").replace("{n}", String(view.answer.linesRemoved))),
|
|
688
|
+
...view.answer.truncated ? [(0, react.createElement)("span", {
|
|
689
|
+
className: "evolution-param-fact",
|
|
690
|
+
key: "cut"
|
|
691
|
+
}, t("approvalDiffCut"))] : []
|
|
692
|
+
];
|
|
693
|
+
const lines = [];
|
|
694
|
+
for (const hunk of view.answer.hunks) {
|
|
695
|
+
for (const [index, line] of hunk.oldText.split("\n").entries()) {
|
|
696
|
+
if (hunk.oldText === "" && line === "") continue;
|
|
697
|
+
lines.push((0, react.createElement)("div", {
|
|
698
|
+
key: "old-" + String(index),
|
|
699
|
+
className: "evolution-param-line-del"
|
|
700
|
+
}, "- " + line));
|
|
701
|
+
}
|
|
702
|
+
for (const [index, line] of hunk.newText.split("\n").entries()) {
|
|
703
|
+
if (hunk.newText === "" && line === "") continue;
|
|
704
|
+
lines.push((0, react.createElement)("div", {
|
|
705
|
+
key: "new-" + String(index),
|
|
706
|
+
className: "evolution-param-line-add"
|
|
707
|
+
}, "+ " + line));
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
return (0, react.createElement)("div", null, (0, react.createElement)("p", { className: "evolution-param-facts" }, ...facts), (0, react.createElement)("pre", { className: "evolution-param-source" }, ...lines.length === 0 ? ["±"] : lines));
|
|
711
|
+
};
|
|
712
|
+
const row = (entry) => {
|
|
713
|
+
const busy = state.busy.includes(entry.id);
|
|
714
|
+
const button = (label, action) => (0, react.createElement)("button", {
|
|
715
|
+
className: "evolution-param-button",
|
|
716
|
+
type: "button",
|
|
717
|
+
disabled: busy,
|
|
718
|
+
onClick: () => {
|
|
719
|
+
decide(entry.id, action);
|
|
720
|
+
}
|
|
721
|
+
}, busy ? t("approvalLoading") : t(label));
|
|
722
|
+
return (0, react.createElement)("div", {
|
|
723
|
+
className: "evolution-param-field",
|
|
724
|
+
key: entry.id
|
|
725
|
+
}, (0, react.createElement)("div", { className: "evolution-param-label" }, (0, react.createElement)("span", { className: "evolution-param-source" }, t(KIND_KEYS[entry.kind])), (0, react.createElement)("span", { className: "evolution-param-value" }, entry.summary)), (0, react.createElement)("p", { className: "evolution-param-hint" }, age(entry)), (0, react.createElement)("div", { className: "evolution-param-actions" }, button("approvalApprove", "approve"), button("approvalReject", "reject"), (0, react.createElement)("button", {
|
|
726
|
+
className: "evolution-param-button",
|
|
727
|
+
type: "button",
|
|
728
|
+
onClick: () => {
|
|
729
|
+
togglePreview(entry.id);
|
|
730
|
+
}
|
|
731
|
+
}, state.open === entry.id ? t("approvalPreviewClose") : t("approvalPreview"))), state.open === entry.id && state.preview !== null ? preview(state.preview) : null);
|
|
732
|
+
};
|
|
733
|
+
const body = () => {
|
|
734
|
+
if (state.view.kind === "loading") return (0, react.createElement)("p", { className: "evolution-param-note" }, t("approvalLoading"));
|
|
735
|
+
if (state.view.kind === "empty") return (0, react.createElement)("p", { className: "evolution-param-note" }, t("approvalEmpty"));
|
|
736
|
+
if (state.view.kind === "failed") return (0, react.createElement)("div", { className: "evolution-param-actions" }, (0, react.createElement)("p", { className: "evolution-param-error" }, t("approvalFailed") + ": " + state.view.message), (0, react.createElement)("button", {
|
|
737
|
+
className: "evolution-param-button",
|
|
738
|
+
type: "button",
|
|
739
|
+
onClick: () => {
|
|
740
|
+
load();
|
|
741
|
+
}
|
|
742
|
+
}, t("approvalRetry")));
|
|
743
|
+
return (0, react.createElement)("div", null, ...state.view.rows.map(row), state.notice === null ? null : (0, react.createElement)("p", { className: "evolution-param-error" }, state.notice));
|
|
744
|
+
};
|
|
745
|
+
const count = state.view.kind === "ready" ? state.view.rows.length : 0;
|
|
746
|
+
return (0, react.createElement)("section", { className: "evolution-param-card" }, (0, react.createElement)("button", {
|
|
747
|
+
className: "evolution-param-head",
|
|
748
|
+
type: "button",
|
|
749
|
+
onClick: () => {
|
|
750
|
+
setOpen(!open);
|
|
751
|
+
},
|
|
752
|
+
"aria-expanded": open
|
|
753
|
+
}, (0, react.createElement)("span", { className: "evolution-param-card-title" }, t("approvalTitle")), count === 0 ? null : (0, react.createElement)("span", { className: "evolution-param-count" }, t("approvalCount").replace("{n}", String(count))), (0, react.createElement)("span", { className: "evolution-param-chevron" }, open ? "▲" : "▼")), open ? (0, react.createElement)("div", { className: "evolution-param-body" }, body()) : null);
|
|
754
|
+
}
|
|
755
|
+
//#endregion
|
|
425
756
|
//#region src/client/messages.ts
|
|
426
757
|
/**
|
|
427
758
|
* Copy for the evolution settings section and its parameter cards.
|
|
@@ -455,7 +786,30 @@ window.__ModuleLoader__.load({
|
|
|
455
786
|
cardMemory: "长期记忆",
|
|
456
787
|
cardToolMemory: "记忆写入",
|
|
457
788
|
cardCurator: "技能整理",
|
|
458
|
-
cardSkills: "技能写入规则"
|
|
789
|
+
cardSkills: "技能写入规则",
|
|
790
|
+
approvalTitle: "待批写入",
|
|
791
|
+
approvalLoading: "正在读取待批项…",
|
|
792
|
+
approvalEmpty: "没有待批的写入",
|
|
793
|
+
approvalFailed: "待批项读取失败",
|
|
794
|
+
approvalRetry: "重试",
|
|
795
|
+
approvalApprove: "批准",
|
|
796
|
+
approvalReject: "拒绝",
|
|
797
|
+
approvalCount: "{n} 项待批",
|
|
798
|
+
approvalKindMemory: "记忆",
|
|
799
|
+
approvalKindSkill: "技能",
|
|
800
|
+
approvalKindCapability: "能力",
|
|
801
|
+
approvalAgeNow: "刚刚",
|
|
802
|
+
approvalAgeMinutes: "{n} 分钟前",
|
|
803
|
+
approvalAgeHours: "{n} 小时前",
|
|
804
|
+
approvalAgeDays: "{n} 天前",
|
|
805
|
+
approvalAgeMonths: "{n} 个月前",
|
|
806
|
+
approvalPreview: "预览",
|
|
807
|
+
approvalPreviewClose: "收起",
|
|
808
|
+
approvalPreviewReading: "正在读取预览…",
|
|
809
|
+
approvalPreviewNone: "看不了预览:",
|
|
810
|
+
approvalDiffAdded: "{n} 行新增",
|
|
811
|
+
approvalDiffRemoved: "{n} 行删除",
|
|
812
|
+
approvalDiffCut: "改动区域已截断"
|
|
459
813
|
};
|
|
460
814
|
/** English copy, key for key with {@link zh}. */
|
|
461
815
|
const en = {
|
|
@@ -477,7 +831,30 @@ window.__ModuleLoader__.load({
|
|
|
477
831
|
cardMemory: "Long-term memory",
|
|
478
832
|
cardToolMemory: "Writing memory",
|
|
479
833
|
cardCurator: "Skill tidy-up",
|
|
480
|
-
cardSkills: "Skill write rules"
|
|
834
|
+
cardSkills: "Skill write rules",
|
|
835
|
+
approvalTitle: "Writes awaiting approval",
|
|
836
|
+
approvalLoading: "reading the pending window…",
|
|
837
|
+
approvalEmpty: "nothing is waiting for a decision",
|
|
838
|
+
approvalFailed: "the pending window could not be read",
|
|
839
|
+
approvalRetry: "Retry",
|
|
840
|
+
approvalApprove: "Approve",
|
|
841
|
+
approvalReject: "Reject",
|
|
842
|
+
approvalCount: "{n} waiting",
|
|
843
|
+
approvalKindMemory: "memory",
|
|
844
|
+
approvalKindSkill: "skill",
|
|
845
|
+
approvalKindCapability: "capability",
|
|
846
|
+
approvalAgeNow: "just now",
|
|
847
|
+
approvalAgeMinutes: "{n} min ago",
|
|
848
|
+
approvalAgeHours: "{n} h ago",
|
|
849
|
+
approvalAgeDays: "{n} d ago",
|
|
850
|
+
approvalAgeMonths: "{n} mo ago",
|
|
851
|
+
approvalPreview: "Preview",
|
|
852
|
+
approvalPreviewClose: "Hide",
|
|
853
|
+
approvalPreviewReading: "reading the preview…",
|
|
854
|
+
approvalPreviewNone: "no preview: ",
|
|
855
|
+
approvalDiffAdded: "{n} lines added",
|
|
856
|
+
approvalDiffRemoved: "{n} lines removed",
|
|
857
|
+
approvalDiffCut: "the changed region is truncated"
|
|
481
858
|
};
|
|
482
859
|
/**
|
|
483
860
|
* Card titles per settings namespace. A namespace without an entry falls back to
|
|
@@ -825,7 +1202,12 @@ window.__ModuleLoader__.load({
|
|
|
825
1202
|
".evolution-param-check:disabled{cursor:default;opacity:.5}",
|
|
826
1203
|
".evolution-param-footer{display:flex;justify-content:flex-end;gap:var(--evo-space-8);padding:var(--evo-space-12) 0 0;border-top:var(--evo-hairline-width) solid var(--dsw-alias-border-l2)}",
|
|
827
1204
|
".evolution-param-note{margin:0;color:var(--dsw-alias-label-tertiary);font-size:var(--evo-type-xs);line-height:var(--evo-leading-base)}",
|
|
828
|
-
".evolution-param-error{margin:0;color:var(--dsw-alias-state-error-primary);font-size:var(--evo-type-xs);line-height:var(--evo-leading-base)}"
|
|
1205
|
+
".evolution-param-error{margin:0;color:var(--dsw-alias-state-error-primary);font-size:var(--evo-type-xs);line-height:var(--evo-leading-base)}",
|
|
1206
|
+
".evolution-param-facts{display:flex;flex-wrap:wrap;align-items:baseline;margin:0;color:var(--dsw-alias-label-tertiary);font-size:var(--evo-type-xs);line-height:var(--evo-leading-base)}",
|
|
1207
|
+
".evolution-param-fact+.evolution-param-fact::before{content:\"·\";margin:0 var(--evo-space-6);color:var(--dsw-alias-label-tertiary)}",
|
|
1208
|
+
".evolution-param-source{margin:0;padding:var(--evo-space-12) var(--evo-space-16);max-height:var(--evo-cap-block);overflow:auto;border:var(--evo-hairline-width) solid var(--dsw-alias-border-l1);border-radius:var(--evo-radius-surface);background:var(--dsw-alias-markdown-code-block);color:var(--dsw-alias-label-primary);font:var(--dsw-font-markdown-code-block-small);white-space:pre-wrap}",
|
|
1209
|
+
".evolution-param-line-add{color:var(--evo-tone-success)}",
|
|
1210
|
+
".evolution-param-line-del{color:var(--evo-tone-danger)}"
|
|
829
1211
|
].join("\n");
|
|
830
1212
|
/**
|
|
831
1213
|
* Inject the stylesheet once per document.
|
|
@@ -853,6 +1235,13 @@ window.__ModuleLoader__.load({
|
|
|
853
1235
|
const SECTION_ID = "evolution";
|
|
854
1236
|
const SECTION_ORDER = 25;
|
|
855
1237
|
/**
|
|
1238
|
+
* The pending-window card's slot key.
|
|
1239
|
+
*
|
|
1240
|
+
* The section pairs cards by key and the parameter cards use their settings namespace; this card has no
|
|
1241
|
+
* namespace (its data comes from the host's approval routes), so it carries a key of its own.
|
|
1242
|
+
*/
|
|
1243
|
+
const PENDING_CARD_KEY = "approval-pending";
|
|
1244
|
+
/**
|
|
856
1245
|
* Register the locale namespace, the section and its cards.
|
|
857
1246
|
* @param ctx - client cordis context.
|
|
858
1247
|
*/
|
|
@@ -913,9 +1302,19 @@ window.__ModuleLoader__.load({
|
|
|
913
1302
|
inject: face
|
|
914
1303
|
}, ParamCard);
|
|
915
1304
|
}
|
|
1305
|
+
const api = createApprovalApi((input, init) => fetch(input, init));
|
|
1306
|
+
yield seam.slots.register({
|
|
1307
|
+
name: CARD_SLOT,
|
|
1308
|
+
key: PENDING_CARD_KEY,
|
|
1309
|
+
inject: () => ({
|
|
1310
|
+
t,
|
|
1311
|
+
api
|
|
1312
|
+
})
|
|
1313
|
+
}, PendingCard);
|
|
916
1314
|
});
|
|
917
1315
|
}
|
|
918
1316
|
//#endregion
|
|
1317
|
+
exports.PENDING_CARD_KEY = PENDING_CARD_KEY;
|
|
919
1318
|
exports.SECTION_ID = SECTION_ID;
|
|
920
1319
|
exports.SECTION_ORDER = SECTION_ORDER;
|
|
921
1320
|
exports.apply = apply;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The "writes awaiting approval" card: the staged write window, with the two decisions on each row.
|
|
3
|
+
*
|
|
4
|
+
* It is a card in the SAME section and wears the SAME shell as the parameter cards (header with title,
|
|
5
|
+
* count and chevron; body of field blocks; a button row per block), so nothing here invents a second
|
|
6
|
+
* visual language. Two things differ on purpose: the data comes from the host's approval routes rather
|
|
7
|
+
* than the settings transport, and its three states are distinct — "reading", "nothing staged" and "the
|
|
8
|
+
* read failed" each say so in their own words (E7: merging the first two is what made an empty panel
|
|
9
|
+
* look like lost data).
|
|
10
|
+
*
|
|
11
|
+
* Every decision re-reads the window instead of predicting the outcome: the host owns the state, and a
|
|
12
|
+
* refusal is shown with the host's own sentence.
|
|
13
|
+
* @module @lmzhen/dsh-evolution-settings-ui/client/PendingCard
|
|
14
|
+
*/
|
|
15
|
+
import { type ReactNode } from 'react';
|
|
16
|
+
import type { ApprovalApi } from './api.ts';
|
|
17
|
+
import type { MessageKey } from './messages.ts';
|
|
18
|
+
/** Injected face: the copy seat and the host calls. No hook compartment — this card keeps its own state. */
|
|
19
|
+
export interface PendingCardFace {
|
|
20
|
+
t: (key: MessageKey) => string;
|
|
21
|
+
api: ApprovalApi;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Render the pending window.
|
|
25
|
+
* @param props - the copy seat and the host calls.
|
|
26
|
+
* @returns the card.
|
|
27
|
+
*/
|
|
28
|
+
export declare function PendingCard(props: PendingCardFace): ReactNode;
|
|
29
|
+
//# sourceMappingURL=PendingCard.d.ts.map
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/** One staged write, as the pending route hands it over (never the staged args). */
|
|
2
|
+
export interface PendingRow {
|
|
3
|
+
readonly id: string;
|
|
4
|
+
readonly kind: 'memory' | 'skill' | 'capability';
|
|
5
|
+
readonly summary: string;
|
|
6
|
+
readonly createdAt: string;
|
|
7
|
+
/** How long ago it was staged, in the family's `{unit, n}` closed vocabulary. */
|
|
8
|
+
readonly age: {
|
|
9
|
+
readonly unit: string;
|
|
10
|
+
readonly n: number;
|
|
11
|
+
};
|
|
12
|
+
readonly status: 'pending' | 'executing' | 'approved' | 'rejected';
|
|
13
|
+
readonly claimedBy?: string | undefined;
|
|
14
|
+
}
|
|
15
|
+
/** One windowed hunk of a previewed change: the region the write would touch. */
|
|
16
|
+
export interface PreviewHunk {
|
|
17
|
+
readonly path: string;
|
|
18
|
+
readonly oldText: string;
|
|
19
|
+
readonly newText: string;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* What a staged write WOULD store. `available: false` carries the host's reason — plenty of records have
|
|
23
|
+
* no bytes worth showing, and the reader is owed that sentence rather than an empty diff.
|
|
24
|
+
*/
|
|
25
|
+
export type PreviewAnswer = {
|
|
26
|
+
readonly available: true;
|
|
27
|
+
readonly path: string;
|
|
28
|
+
readonly linesAdded: number;
|
|
29
|
+
readonly linesRemoved: number;
|
|
30
|
+
readonly hunks: readonly PreviewHunk[];
|
|
31
|
+
readonly truncated: boolean;
|
|
32
|
+
} | {
|
|
33
|
+
readonly available: false;
|
|
34
|
+
readonly reason: string;
|
|
35
|
+
};
|
|
36
|
+
/** What one call answered: the data, or the reason it could not be carried out. */
|
|
37
|
+
export type ApprovalAnswer<T> = {
|
|
38
|
+
readonly ok: true;
|
|
39
|
+
readonly data: T;
|
|
40
|
+
} | {
|
|
41
|
+
readonly ok: false;
|
|
42
|
+
readonly message: string;
|
|
43
|
+
};
|
|
44
|
+
/** How this bundle reaches the host. Injectable so a spec drives it without a server. */
|
|
45
|
+
export interface ApprovalApi {
|
|
46
|
+
/** The staged writes waiting for a decision. */
|
|
47
|
+
pending: () => Promise<ApprovalAnswer<readonly PendingRow[]>>;
|
|
48
|
+
/** Ask the host to replay one staged write. */
|
|
49
|
+
approve: (id: string) => Promise<ApprovalAnswer<unknown>>;
|
|
50
|
+
/** Close one staged write without replaying it. */
|
|
51
|
+
reject: (id: string) => Promise<ApprovalAnswer<unknown>>;
|
|
52
|
+
/** What that staged write would store, resolved by the host without running it. */
|
|
53
|
+
preview: (id: string) => Promise<ApprovalAnswer<PreviewAnswer>>;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Build the approval face over one fetch implementation.
|
|
57
|
+
* @param doFetch - the fetch to use (the platform's global in the browser, a fake in a spec).
|
|
58
|
+
* @returns the three calls the pending card makes.
|
|
59
|
+
*/
|
|
60
|
+
export declare function createApprovalApi(doFetch: typeof fetch): ApprovalApi;
|
|
61
|
+
//# sourceMappingURL=api.d.ts.map
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The approval paths this browser half calls.
|
|
3
|
+
*
|
|
4
|
+
* A browser half cannot import the host package's table at runtime (the family's client bundles treat
|
|
5
|
+
* every `@deepseek-ai/*` specifier as external), so the literals are declared here and a spec compares
|
|
6
|
+
* them against `APPROVAL_ROUTES` in `evolution-approval` — a rename on one side would otherwise surface
|
|
7
|
+
* as a 404 in the GUI instead of in CI.
|
|
8
|
+
* @module @lmzhen/dsh-evolution-settings-ui/client/approval-routes
|
|
9
|
+
*/
|
|
10
|
+
/** One path per call the pending card makes. */
|
|
11
|
+
export declare const APPROVAL_CLIENT_ROUTES: {
|
|
12
|
+
readonly pending: "/api/dsh-evolution/approval/pending";
|
|
13
|
+
readonly approve: "/api/dsh-evolution/approval/approve";
|
|
14
|
+
readonly reject: "/api/dsh-evolution/approval/reject";
|
|
15
|
+
readonly preview: "/api/dsh-evolution/approval/preview";
|
|
16
|
+
};
|
|
17
|
+
//# sourceMappingURL=approval-routes.d.ts.map
|
|
@@ -21,6 +21,13 @@ export declare const inject: string[];
|
|
|
21
21
|
/** Section id and nav order (the platform's own sections sit at 0/10/15/20). */
|
|
22
22
|
export declare const SECTION_ID = "evolution";
|
|
23
23
|
export declare const SECTION_ORDER = 25;
|
|
24
|
+
/**
|
|
25
|
+
* The pending-window card's slot key.
|
|
26
|
+
*
|
|
27
|
+
* The section pairs cards by key and the parameter cards use their settings namespace; this card has no
|
|
28
|
+
* namespace (its data comes from the host's approval routes), so it carries a key of its own.
|
|
29
|
+
*/
|
|
30
|
+
export declare const PENDING_CARD_KEY = "approval-pending";
|
|
24
31
|
/**
|
|
25
32
|
* Register the locale namespace, the section and its cards.
|
|
26
33
|
* @param ctx - client cordis context.
|
|
@@ -31,6 +31,29 @@ export declare const zh: {
|
|
|
31
31
|
readonly cardToolMemory: "记忆写入";
|
|
32
32
|
readonly cardCurator: "技能整理";
|
|
33
33
|
readonly cardSkills: "技能写入规则";
|
|
34
|
+
readonly approvalTitle: "待批写入";
|
|
35
|
+
readonly approvalLoading: "正在读取待批项…";
|
|
36
|
+
readonly approvalEmpty: "没有待批的写入";
|
|
37
|
+
readonly approvalFailed: "待批项读取失败";
|
|
38
|
+
readonly approvalRetry: "重试";
|
|
39
|
+
readonly approvalApprove: "批准";
|
|
40
|
+
readonly approvalReject: "拒绝";
|
|
41
|
+
readonly approvalCount: "{n} 项待批";
|
|
42
|
+
readonly approvalKindMemory: "记忆";
|
|
43
|
+
readonly approvalKindSkill: "技能";
|
|
44
|
+
readonly approvalKindCapability: "能力";
|
|
45
|
+
readonly approvalAgeNow: "刚刚";
|
|
46
|
+
readonly approvalAgeMinutes: "{n} 分钟前";
|
|
47
|
+
readonly approvalAgeHours: "{n} 小时前";
|
|
48
|
+
readonly approvalAgeDays: "{n} 天前";
|
|
49
|
+
readonly approvalAgeMonths: "{n} 个月前";
|
|
50
|
+
readonly approvalPreview: "预览";
|
|
51
|
+
readonly approvalPreviewClose: "收起";
|
|
52
|
+
readonly approvalPreviewReading: "正在读取预览…";
|
|
53
|
+
readonly approvalPreviewNone: "看不了预览:";
|
|
54
|
+
readonly approvalDiffAdded: "{n} 行新增";
|
|
55
|
+
readonly approvalDiffRemoved: "{n} 行删除";
|
|
56
|
+
readonly approvalDiffCut: "改动区域已截断";
|
|
34
57
|
};
|
|
35
58
|
/** English copy, key for key with {@link zh}. */
|
|
36
59
|
export declare const en: Record<keyof typeof zh, string>;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The pending card's state machine, kept OUT of the component so the three states the review asked for
|
|
3
|
+
* can be asserted without a browser (and so a second rendering of the same data cannot invent a fourth).
|
|
4
|
+
*
|
|
5
|
+
* The one rule worth naming: LOADING and EMPTY are different states with different copy. The skill
|
|
6
|
+
* history panel shipped them merged once and a reader could not tell "nothing is staged" from "the
|
|
7
|
+
* answer has not arrived" (E7).
|
|
8
|
+
* @module @lmzhen/dsh-evolution-settings-ui/client/pending-state
|
|
9
|
+
*/
|
|
10
|
+
import type { PendingRow, PreviewAnswer } from './api.ts';
|
|
11
|
+
/** What one row's preview is showing while it is open. */
|
|
12
|
+
export type PreviewView = {
|
|
13
|
+
readonly kind: 'loading';
|
|
14
|
+
} | {
|
|
15
|
+
readonly kind: 'ready';
|
|
16
|
+
readonly answer: Extract<PreviewAnswer, {
|
|
17
|
+
available: true;
|
|
18
|
+
}>;
|
|
19
|
+
} | {
|
|
20
|
+
readonly kind: 'unavailable';
|
|
21
|
+
readonly reason: string;
|
|
22
|
+
} | {
|
|
23
|
+
readonly kind: 'failed';
|
|
24
|
+
readonly message: string;
|
|
25
|
+
};
|
|
26
|
+
/** What the card is showing right now. */
|
|
27
|
+
export type PendingView = {
|
|
28
|
+
readonly kind: 'loading';
|
|
29
|
+
} | {
|
|
30
|
+
readonly kind: 'ready';
|
|
31
|
+
readonly rows: readonly PendingRow[];
|
|
32
|
+
} | {
|
|
33
|
+
readonly kind: 'empty';
|
|
34
|
+
} | {
|
|
35
|
+
readonly kind: 'failed';
|
|
36
|
+
readonly message: string;
|
|
37
|
+
};
|
|
38
|
+
/** The whole card state: the view, the ids with an action in flight, and the last refusal line. */
|
|
39
|
+
export interface PendingState {
|
|
40
|
+
readonly view: PendingView;
|
|
41
|
+
readonly busy: readonly string[];
|
|
42
|
+
readonly notice: string | null;
|
|
43
|
+
/** The row whose preview is open, or null. One at a time: a preview is a thing being read. */
|
|
44
|
+
readonly open: string | null;
|
|
45
|
+
readonly preview: PreviewView | null;
|
|
46
|
+
}
|
|
47
|
+
/** The state a card mounts in: still loading, nothing in flight, nothing said yet. */
|
|
48
|
+
export declare const INITIAL_PENDING_STATE: PendingState;
|
|
49
|
+
/** Open one row's preview: the row is now the open one and its answer is on the way. */
|
|
50
|
+
export declare function expanded(state: PendingState, id: string): PendingState;
|
|
51
|
+
/** Close whatever preview is open. */
|
|
52
|
+
export declare function collapsed(state: PendingState): PendingState;
|
|
53
|
+
/**
|
|
54
|
+
* A preview answer arrived. An answer for a row the reader has already left is DROPPED: it would
|
|
55
|
+
* otherwise paint one row's diff under another row's heading.
|
|
56
|
+
*/
|
|
57
|
+
export declare function previewArrived(state: PendingState, id: string, view: PreviewView): PendingState;
|
|
58
|
+
/** The first load, or a refresh after a decision: back to loading, keeping no stale rows. */
|
|
59
|
+
export declare function loading(state: PendingState): PendingState;
|
|
60
|
+
/**
|
|
61
|
+
* A list arrived. An empty list is its OWN state — the reader is told the window is clear rather than
|
|
62
|
+
* being shown the same line a slow answer shows.
|
|
63
|
+
*/
|
|
64
|
+
export declare function loaded(state: PendingState, rows: readonly PendingRow[]): PendingState;
|
|
65
|
+
/** The list could not be read: the reason stays on screen next to a retry control. */
|
|
66
|
+
export declare function failed(state: PendingState, message: string): PendingState;
|
|
67
|
+
/** Mark one record as having an action in flight (its buttons are disabled until it ends). */
|
|
68
|
+
export declare function beginAction(state: PendingState, id: string): PendingState;
|
|
69
|
+
/** The action ended, whatever it answered. */
|
|
70
|
+
export declare function endAction(state: PendingState, id: string): PendingState;
|
|
71
|
+
/** One refusal or failure line under the list; `null` clears it (a later success does). */
|
|
72
|
+
export declare function noticed(state: PendingState, message: string | null): PendingState;
|
|
73
|
+
//# sourceMappingURL=pending-state.d.ts.map
|
package/package.json
CHANGED