@lmzhen/dsh-evolution-ui 0.32.6 → 0.33.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/lib/client.js CHANGED
@@ -815,326 +815,6 @@ window.__ModuleLoader__.load({
815
815
  }
816
816
  ];
817
817
  //#endregion
818
- //#region ../evolution-client-kit/src/generated/states.generated.ts
819
- /** A neutral/ warn/ fail block; a fail block is role=alert so assistive tech announces it. */
820
- function Block(props) {
821
- const tone = props.tone ?? "neutral";
822
- return (0, react.createElement)("div", {
823
- "data-tone": tone,
824
- ...tone === "fail" ? { role: "alert" } : {}
825
- }, props.title === void 0 ? null : (0, react.createElement)("div", { "data-tone-title": tone }, props.title), props.children);
826
- }
827
- /** One classified failure as a reader-facing sentence. */
828
- function failureText(failure, t) {
829
- switch (failure.kind) {
830
- case "unauthenticated": return t("failureUnauthorized");
831
- case "forbidden": return t("failureForbidden");
832
- case "unmounted": return t("failureUnmounted");
833
- case "server": return failure.status === 0 ? t("failureServer") : t("failureServer") + " (HTTP " + String(failure.status) + ")";
834
- case "refused": return failure.message;
835
- }
836
- }
837
- /** The failed-state block: sentence + (when the surface exists but is unreachable) the command fallback. */
838
- function FailureBlock(props) {
839
- const hint = props.fallback === void 0 ? null : (0, react.createElement)("div", { "data-tone-hint": "true" }, props.t("fallbackHint", { command: props.fallback.command }));
840
- return Block({
841
- tone: "fail",
842
- title: failureText(props.failure, props.t),
843
- children: hint
844
- });
845
- }
846
- //#endregion
847
- //#region src/panels/settings/DiagnosticsCard.ts
848
- /**
849
- * The diagnostics card (W7 S7): the settings face of the SAME DiagnosticsView the CLI and the
850
- * `/api/dsh-evolution/diagnostics` route answer.
851
- *
852
- * Read-only by construction: it fetches once on mount and only refetches on an explicit Refresh; a
853
- * failed read renders the shared FailureBlock with the command-face fallback. The client owns no
854
- * key list and no status vocabulary: it renders the host view's frozen runtime rows and leads with
855
- * a plain-language summary. The view's documentation facts are a CLI/JSON concern and are
856
- * deliberately NOT rendered here (0.27.2 real-machine feedback: a settings page is not a spec
857
- * sheet; the runtime window is the whole reading).
858
- * @module @lmzhen/dsh-evolution-ui/panels/settings/client/DiagnosticsCard
859
- */
860
- /** Which word one status gets (the frozen W1 status set). */
861
- const STATUS_KEYS = Object.freeze({
862
- ok: "diagStatusOk",
863
- warn: "diagStatusWarn",
864
- fail: "diagStatusFail",
865
- unknown: "diagStatusUnknown"
866
- });
867
- /** The summary word for a badge status (the same frozen set; ok has no count). */
868
- const SUMMARY_KEYS = Object.freeze({
869
- ok: "diagSummaryOk",
870
- warn: "diagSummaryWarn",
871
- fail: "diagSummaryFail",
872
- unknown: "diagSummaryUnknown"
873
- });
874
- /** The one status the card leads with: fail > warn > unknown > ok. */
875
- function summaryOf(rows) {
876
- const count = (status) => rows.filter((row) => row.status === status).length;
877
- for (const status of [
878
- "fail",
879
- "warn",
880
- "unknown"
881
- ]) {
882
- const n = count(status);
883
- if (n > 0) return {
884
- status,
885
- count: n
886
- };
887
- }
888
- return {
889
- status: "ok",
890
- count: 0
891
- };
892
- }
893
- /**
894
- * Render the card.
895
- * @param props - the copy seat, the host call and the command fallback.
896
- * @returns the card.
897
- */
898
- function DiagnosticsCard(props) {
899
- const { t, api } = props;
900
- const translate = (key, values) => t(key, values);
901
- const [state, setState] = (0, react.useState)({ phase: "loading" });
902
- const [refreshing, setRefreshing] = (0, react.useState)(false);
903
- const [open, setOpen] = (0, react.useState)(false);
904
- const ticket = (0, react.useRef)(0);
905
- const read = (keepView) => {
906
- const current = ticket.current + 1;
907
- ticket.current = current;
908
- if (keepView) setRefreshing(true);
909
- else setState({ phase: "loading" });
910
- api.view().then((answer) => {
911
- if (ticket.current !== current) return;
912
- setState(answer.ok ? {
913
- phase: "ready",
914
- view: answer.data
915
- } : {
916
- phase: "failed",
917
- failure: answer.failure
918
- });
919
- }).finally(() => {
920
- if (ticket.current === current) setRefreshing(false);
921
- });
922
- };
923
- (0, react.useEffect)(() => {
924
- read(false);
925
- }, []);
926
- const refresh = () => {
927
- read(true);
928
- };
929
- const summary = state.phase === "ready" ? summaryOf(state.view.runtime) : void 0;
930
- const header = (0, react.createElement)("button", {
931
- type: "button",
932
- className: "evolution-param-head",
933
- "aria-expanded": open ? "true" : "false",
934
- onClick: () => {
935
- setOpen(!open);
936
- }
937
- }, (0, react.createElement)("span", { className: "evolution-param-card-title" }, t("diagTitle")), summary === void 0 ? null : (0, react.createElement)("span", {
938
- className: "evo-diag-badge",
939
- "data-status": summary.status
940
- }, t(SUMMARY_KEYS[summary.status], summary.count === 0 ? void 0 : { n: summary.count })), (0, react.createElement)("span", { className: "evolution-param-chevron" }, open ? "▲" : "▼"));
941
- const runtime = (view) => view.runtime.length === 0 ? null : (0, react.createElement)("div", {}, (0, react.createElement)("h3", { className: "evolution-param-group" }, t("diagRuntimeTitle")), (0, react.createElement)("div", { className: "evo-diag-list" }, ...view.runtime.map((row) => (0, react.createElement)("div", {
942
- key: "runtime-" + row.key,
943
- className: "evo-diag-item",
944
- "data-kind": "runtime",
945
- "data-status": row.status
946
- }, (0, react.createElement)("div", { className: "evo-diag-row-head" }, (0, react.createElement)("span", { className: "evo-diag-dot" }), (0, react.createElement)("span", { className: "evo-diag-label" }, row.label), (0, react.createElement)("span", { className: "evo-diag-status" }, t(STATUS_KEYS[row.status]))), row.detail === void 0 ? null : (0, react.createElement)("span", { className: "evo-diag-detail" }, row.detail), row.remedy === void 0 ? null : (0, react.createElement)("span", { className: "evo-diag-remedy" }, t("diagRemedy") + row.remedy)))));
947
- const body = () => {
948
- if (state.phase === "loading") return (0, react.createElement)("p", { className: "evolution-param-loading" }, t("stateLoading"));
949
- if (state.phase === "failed") return FailureBlock({
950
- failure: state.failure,
951
- t: translate,
952
- fallback: props.fallback
953
- });
954
- const view = state.view;
955
- return (0, react.createElement)("div", {}, (0, react.createElement)("p", { className: "evolution-param-note" }, t("diagSubtitle")), (0, react.createElement)("div", { className: "evolution-param-actions" }, (0, react.createElement)("button", {
956
- type: "button",
957
- className: "evolution-param-button",
958
- onClick: refresh,
959
- disabled: refreshing,
960
- "data-action": "diag-refresh"
961
- }, t(refreshing ? "diagRefreshing" : "diagRefresh"))), runtime(view), view.runtime.length === 0 ? (0, react.createElement)("p", { className: "evolution-param-note" }, t("diagNoRows")) : null);
962
- };
963
- return (0, react.createElement)("section", {
964
- className: "evolution-param-card",
965
- "data-card": "diagnostics"
966
- }, header, open ? (0, react.createElement)("div", { className: "evolution-param-body" }, body()) : null);
967
- }
968
- //#endregion
969
- //#region ../evolution-client-kit/src/generated/routes.generated.ts
970
- /**
971
- * Every family host path, generated from evolution-core's HOST_ROUTES (the ONE declaration).
972
- * A browser half cannot import the host package at runtime (client bundles keep @deepseek-ai/*
973
- * external), so the table is mirrored here mechanically; a rename in core regenerates all three
974
- * client copies, and the generator's --check makes a stale copy red.
975
- */
976
- const ROUTES = {
977
- approval: {
978
- pending: "/api/dsh-evolution/approval/pending",
979
- preview: "/api/dsh-evolution/approval/preview",
980
- approve: "/api/dsh-evolution/approval/approve",
981
- reject: "/api/dsh-evolution/approval/reject",
982
- release: "/api/dsh-evolution/approval/release"
983
- },
984
- "skill-history": {
985
- skills: "/api/dsh-evolution/skill-history/skills",
986
- versions: "/api/dsh-evolution/skill-history/versions",
987
- diff: "/api/dsh-evolution/skill-history/versions/diff",
988
- body: "/api/dsh-evolution/skill-history/versions/body",
989
- undo: "/api/dsh-evolution/skill-history/undo",
990
- health: "/api/dsh-evolution/skill-history/health"
991
- },
992
- "skill-health": { rows: "/api/dsh-evolution/skill-health/rows" },
993
- merge: {
994
- view: "/api/dsh-evolution/merge/view",
995
- rows: "/api/dsh-evolution/merge/rows",
996
- run: "/api/dsh-evolution/merge/run",
997
- runs: "/api/dsh-evolution/merge/runs",
998
- body: "/api/dsh-evolution/merge/body",
999
- history: "/api/dsh-evolution/merge/history"
1000
- },
1001
- diagnostics: { view: "/api/dsh-evolution/diagnostics" },
1002
- settings: {
1003
- describe: "/api/dsh-evolution/settings/describe",
1004
- mutate: "/api/dsh-evolution/settings/mutate"
1005
- }
1006
- };
1007
- //#endregion
1008
- //#region ../evolution-client-kit/src/generated/route-client.generated.ts
1009
- /** Classify a non-2xx status: 401/403/404 keep their own kind, everything else is a server answer. */
1010
- function classify(status) {
1011
- if (status === 401) return {
1012
- kind: "unauthenticated",
1013
- status: 401
1014
- };
1015
- if (status === 403) return {
1016
- kind: "forbidden",
1017
- status: 403
1018
- };
1019
- if (status === 404) return {
1020
- kind: "unmounted",
1021
- status: 404
1022
- };
1023
- return {
1024
- kind: "server",
1025
- status
1026
- };
1027
- }
1028
- /**
1029
- * One request, no retries and no body parsing for the platform's admission answers.
1030
- * @param doFetch - the fetch in use (the browser's, or a spec's stub).
1031
- * @param path - the exact route path from the generated ROUTES table.
1032
- * @param init - standard RequestInit.
1033
- * @returns the data, or the classified failure (network/parse failures read as 'server').
1034
- */
1035
- async function requestJson(doFetch, path, init) {
1036
- let response;
1037
- try {
1038
- response = await doFetch(path, init);
1039
- } catch (error) {
1040
- return {
1041
- ok: false,
1042
- failure: {
1043
- kind: "server",
1044
- status: 0,
1045
- detail: error instanceof Error ? error.message : String(error)
1046
- }
1047
- };
1048
- }
1049
- if (!response.ok) return {
1050
- ok: false,
1051
- failure: classify(response.status)
1052
- };
1053
- let body;
1054
- try {
1055
- body = await response.json();
1056
- } catch {
1057
- return {
1058
- ok: false,
1059
- failure: {
1060
- kind: "server",
1061
- status: response.status
1062
- }
1063
- };
1064
- }
1065
- if (typeof body === "object" && body !== null) {
1066
- const record = body;
1067
- if (record.ok === true) return {
1068
- ok: true,
1069
- data: record.data
1070
- };
1071
- const nested = record.error;
1072
- const errorRecord = typeof nested === "object" && nested !== null ? nested : void 0;
1073
- const code = typeof record.code === "string" && record.code !== "" ? record.code : typeof errorRecord?.code === "string" ? errorRecord.code : void 0;
1074
- const message = typeof record.message === "string" && record.message !== "" ? record.message : typeof errorRecord?.message === "string" ? errorRecord.message : void 0;
1075
- if (record.ok === false && code !== void 0 && message !== void 0 && message !== "") return {
1076
- ok: false,
1077
- failure: {
1078
- kind: "refused",
1079
- code,
1080
- message
1081
- }
1082
- };
1083
- }
1084
- return {
1085
- ok: false,
1086
- failure: {
1087
- kind: "server",
1088
- status: response.status
1089
- }
1090
- };
1091
- }
1092
- /** The one-sentence rendering legacy callers attach to a classified failure. */
1093
- function failureMessage(failure) {
1094
- switch (failure.kind) {
1095
- case "refused": return failure.message;
1096
- case "server": return failure.status === 0 ? failure.detail === void 0 || failure.detail === "" ? "网络错误:无法连接宿主" : failure.detail : "HTTP " + String(failure.status);
1097
- case "unauthenticated": return "HTTP 401";
1098
- case "forbidden": return "HTTP 403";
1099
- case "unmounted": return "HTTP 404";
1100
- }
1101
- }
1102
- //#endregion
1103
- //#region src/panels/settings/diagnostics-state.ts
1104
- /**
1105
- * The diagnostics card's data face (W7 S7): ONE request to `HOST_ROUTES.diagnostics.view` through the
1106
- * GENERATED route client, and the four-state answer the card renders.
1107
- *
1108
- * The Host route is the SAME `buildDoctorView` the CLI prints (W6/W8), so this client owns no facts
1109
- * and no computation — it only renders.
1110
- * @module @lmzhen/dsh-evolution-ui/panels/settings/client/diagnostics-state
1111
- */
1112
- /**
1113
- * Build the diagnostics face over one fetch implementation.
1114
- * @param doFetch - the browser fetch, or a spec stub.
1115
- */
1116
- function createDiagnosticsApi(doFetch = fetch) {
1117
- return { view: async () => await requestJson(doFetch, ROUTES.diagnostics.view, { headers: { accept: "application/json" } }) };
1118
- }
1119
- //#endregion
1120
- //#region ../evolution-client-kit/src/generated/fallbacks.generated.ts
1121
- /**
1122
- * The command-face fallback for every Host surface, generated from evolution-commands'
1123
- * COMMAND_FALLBACKS (the ONE home). When the platform's connection carrier is absent (E-325) the
1124
- * panels render this hint instead of a bare HTTP failure — same use case, command face.
1125
- */
1126
- const FALLBACKS = {
1127
- approval: { command: "/evolution pending … /evolution approve <id> … /evolution reject <id> … /evolution release <id>" },
1128
- "skill-history": { command: "/evolution skill undo <name> [--to v<N>](查看:/evolution skill history <name>)" },
1129
- "skill-health": { command: "/evolution skills refresh(健康行:/evolution doctor)" },
1130
- merge: { command: "/evolution merge status / pending / approve <id> / report" },
1131
- diagnostics: {
1132
- command: "/evolution doctor",
1133
- platformCli: "dsh --json /evolution doctor"
1134
- },
1135
- settings: { command: "/evolution policy set <id> <value>(查看:/evolution policy)" }
1136
- };
1137
- //#endregion
1138
818
  //#region src/panels/settings/messages.ts
1139
819
  /**
1140
820
  * Copy for the evolution settings section and its parameter cards.
@@ -1172,10 +852,13 @@ window.__ModuleLoader__.load({
1172
852
  invalidSelect: "不在允许的取值里",
1173
853
  invalidAtLeast: "不能小于同组的 {n}",
1174
854
  approvalConfirmNamed: "即将批准:{n}",
855
+ approvalRawSummary: "原始摘要",
1175
856
  approvalConfirmYes: "确认批准",
1176
857
  approvalConfirmNo: "取消",
1177
858
  resetTo: "复位为 {value}",
1178
859
  saved: "已保存。",
860
+ saveReadbackFailed: "写入已被接受,但回读失败;点刷新重读后再确认。",
861
+ saveConflict: "这个值没有被接受:设置已被其他页面修改。请重读后再保存。",
1179
862
  resetDone: "已恢复默认值。",
1180
863
  refused: "这个值没有被接受(界面上保持原样)。想看原因:在对话里让它说明。",
1181
864
  resetRefused: "恢复默认值没有生效(这一行还是你的值)。想看原因:在对话里让它说明。",
@@ -1274,10 +957,13 @@ window.__ModuleLoader__.load({
1274
957
  invalidSelect: "Not one of the allowed values",
1275
958
  invalidAtLeast: "must not be below {n}",
1276
959
  approvalConfirmNamed: "About to approve: {n}",
960
+ approvalRawSummary: "Raw summary",
1277
961
  approvalConfirmYes: "Approve",
1278
962
  approvalConfirmNo: "Cancel",
1279
963
  resetTo: "Restores {value}",
1280
964
  saved: "Saved.",
965
+ saveReadbackFailed: "The write was accepted, but reading it back failed; refresh and confirm.",
966
+ saveConflict: "The value was not accepted: settings changed in another page. Re-read before saving again.",
1281
967
  resetDone: "Default restored.",
1282
968
  refused: "The value was not accepted (the page keeps what you see). For the reason, ask in the conversation.",
1283
969
  resetRefused: "The reset did not take (this row still holds your value). For the reason, ask in the conversation.",
@@ -1589,7 +1275,7 @@ window.__ModuleLoader__.load({
1589
1275
  * @returns the card.
1590
1276
  */
1591
1277
  function ParamCardView(props) {
1592
- const { t, fields, mutateOnce, clear, namespace, snapshot } = props;
1278
+ const { t, fields, mutateOnce, saveOnce, clear, namespace, snapshot } = props;
1593
1279
  const lang = props.lang?.() ?? "zh";
1594
1280
  const [open, setOpen] = (0, react.useState)(false);
1595
1281
  const [draft, setDraft] = (0, react.useState)({});
@@ -1616,8 +1302,12 @@ window.__ModuleLoader__.load({
1616
1302
  setPending(null);
1617
1303
  setBusy(false);
1618
1304
  setClearing(null);
1305
+ if (pending.readbackFailed === true) {
1306
+ setError(t("saveReadbackFailed"));
1307
+ return;
1308
+ }
1619
1309
  if (!pending.accepted) {
1620
- setError(t(pending.resets ? "resetRefused" : "refused"));
1310
+ setError(t(pending.resets ? "resetRefused" : pending.kind === "conflict" ? "saveConflict" : "refused"));
1621
1311
  return;
1622
1312
  }
1623
1313
  if (pending.accepted) if (pending.resets) {
@@ -1708,12 +1398,19 @@ window.__ModuleLoader__.load({
1708
1398
  setNote("");
1709
1399
  (async () => {
1710
1400
  try {
1401
+ const ops = dirty.map((field) => ({
1402
+ op: "set",
1403
+ path: [keyOf(field)],
1404
+ value: parseFor(field.control, textOf(field))
1405
+ }));
1406
+ const result = saveOnce === void 0 ? {
1407
+ written: await mutateOnce(ops),
1408
+ readback: "ok"
1409
+ } : await saveOnce(ops);
1711
1410
  setPending({
1712
- accepted: await mutateOnce(dirty.map((field) => ({
1713
- op: "set",
1714
- path: [keyOf(field)],
1715
- value: parseFor(field.control, textOf(field))
1716
- }))),
1411
+ accepted: result.written && result.readback === "ok",
1412
+ readbackFailed: result.written && result.readback === "failed",
1413
+ ...result.kind === void 0 ? {} : { kind: result.kind },
1717
1414
  resets: false
1718
1415
  });
1719
1416
  } catch (caught) {
@@ -1787,17 +1484,6 @@ window.__ModuleLoader__.load({
1787
1484
  * @param props - the injected card face.
1788
1485
  * @returns the card with its live row snapshot.
1789
1486
  */
1790
- const pluginSources = /* @__PURE__ */ new Map();
1791
- /**
1792
- * Register a plugin-surface form source by namespace. The platform's
1793
- * `settings.plugin.item` slot does not bind the card's `hooks` compartment, so
1794
- * the owning settings apply hands the stable source here instead.
1795
- * @param namespace - settings namespace.
1796
- * @param source - the stable external-store source.
1797
- */
1798
- function registerPluginParamSource(namespace, source) {
1799
- pluginSources.set(namespace, source);
1800
- }
1801
1487
  const MISSING_SNAPSHOT = {
1802
1488
  status: "unavailable",
1803
1489
  value: void 0,
@@ -1812,7 +1498,7 @@ window.__ModuleLoader__.load({
1812
1498
  revive: () => {}
1813
1499
  };
1814
1500
  function PluginParamCard(props) {
1815
- const source = props.hooks?.paramSection ?? pluginSources.get(props.namespace) ?? MISSING_SOURCE;
1501
+ const source = props.hooks?.paramSection ?? MISSING_SOURCE;
1816
1502
  const snapshot = (0, react.useSyncExternalStore)(source.subscribe, source.getSnapshot, source.getSnapshot);
1817
1503
  return ParamCardView({
1818
1504
  namespace: props.namespace,
@@ -1820,16 +1506,114 @@ window.__ModuleLoader__.load({
1820
1506
  t: props.t,
1821
1507
  ...props.lang === void 0 ? {} : { lang: props.lang },
1822
1508
  mutateOnce: props.mutateOnce,
1509
+ ...props.saveOnce === void 0 ? {} : { saveOnce: props.saveOnce },
1823
1510
  clear: props.clear,
1824
1511
  snapshot
1825
1512
  });
1826
1513
  }
1827
1514
  //#endregion
1828
- //#region src/panels/settings/seam.ts
1829
- /** Our card slot, hosted by the section this bundle registers. */
1830
- const CARD_SLOT = "evolution.namespace.card";
1831
- /** The diagnostics card's slot key: read-only, no settings namespace (its data comes from HOST_ROUTES.diagnostics). */
1832
- const DIAGNOSTICS_CARD_KEY = "diagnostics";
1515
+ //#region ../evolution-client-kit/src/panel.ts
1516
+ function interpolate(text, values) {
1517
+ if (values === void 0) return text;
1518
+ return text.replace(/\{(\w+)\}/g, (_match, key) => String(values[key] ?? ""));
1519
+ }
1520
+ function injectStyles$1(doc, css, tagId) {
1521
+ if (doc === void 0) return () => {};
1522
+ doc.querySelector("style[data-plugin-css=\"" + tagId + "\"]")?.remove();
1523
+ const tag = doc.createElement("style");
1524
+ tag.setAttribute("data-plugin-css", tagId);
1525
+ tag.textContent = css;
1526
+ doc.head.append(tag);
1527
+ return () => {
1528
+ tag.remove();
1529
+ };
1530
+ }
1531
+ /**
1532
+ * Register one sidebar row and its main body under one lifecycle.
1533
+ * @param ctx - the client context carrying the slots and locale seats.
1534
+ * @param spec - the feature-specific row/body descriptor.
1535
+ */
1536
+ function registerSidebarPanel(ctx, spec) {
1537
+ const seam = ctx;
1538
+ let registered = false;
1539
+ ctx.effect(() => {
1540
+ try {
1541
+ const dispose = seam.locale.register(spec.namespace, spec.dictionaries);
1542
+ registered = true;
1543
+ return dispose;
1544
+ } catch (error) {
1545
+ ctx.logger.warn(spec.namespace + ": the locale seat refused the dictionary (" + String(error) + "); the panel falls back to the bundle copy");
1546
+ return () => {};
1547
+ }
1548
+ }, spec.namespace + ": locale");
1549
+ ctx.effect(() => injectStyles$1(typeof document === "undefined" ? void 0 : document, spec.styles.css, spec.styles.tagId), spec.namespace + ": styles");
1550
+ const fallback = spec.dictionaries[spec.fallback ?? "zh"];
1551
+ const lookup = (key) => {
1552
+ const own = fallback[key] ?? key;
1553
+ if (!registered) return own;
1554
+ try {
1555
+ return seam.locale.bind(spec.namespace)(key);
1556
+ } catch {
1557
+ return own;
1558
+ }
1559
+ };
1560
+ const t = (key, values) => interpolate(lookup(key), values);
1561
+ const label = () => lookup("entry.label");
1562
+ seam.slots.inject("sidebar.panellist", function* () {
1563
+ yield seam.slots.register({
1564
+ name: "sidebar.panellist",
1565
+ id: spec.id,
1566
+ order: spec.order,
1567
+ label
1568
+ }, spec.icon);
1569
+ yield seam.slots.register({
1570
+ name: "main",
1571
+ key: spec.id,
1572
+ inject: () => spec.body(t),
1573
+ ...spec.children === void 0 ? {} : { children: spec.children },
1574
+ ...spec.store === void 0 ? {} : { store: spec.store }
1575
+ }, spec.component);
1576
+ });
1577
+ }
1578
+ //#endregion
1579
+ //#region ../evolution-client-kit/src/generated/routes.generated.ts
1580
+ /**
1581
+ * Every family host path, generated from evolution-core's HOST_ROUTES (the ONE declaration).
1582
+ * A browser half cannot import the host package at runtime (client bundles keep @deepseek-ai/*
1583
+ * external), so the table is mirrored here mechanically; a rename in core regenerates all three
1584
+ * client copies, and the generator's --check makes a stale copy red.
1585
+ */
1586
+ const ROUTES = {
1587
+ approval: {
1588
+ pending: "/api/dsh-evolution/approval/pending",
1589
+ preview: "/api/dsh-evolution/approval/preview",
1590
+ approve: "/api/dsh-evolution/approval/approve",
1591
+ reject: "/api/dsh-evolution/approval/reject",
1592
+ release: "/api/dsh-evolution/approval/release"
1593
+ },
1594
+ "skill-history": {
1595
+ skills: "/api/dsh-evolution/skill-history/skills",
1596
+ versions: "/api/dsh-evolution/skill-history/versions",
1597
+ diff: "/api/dsh-evolution/skill-history/versions/diff",
1598
+ body: "/api/dsh-evolution/skill-history/versions/body",
1599
+ undo: "/api/dsh-evolution/skill-history/undo",
1600
+ health: "/api/dsh-evolution/skill-history/health"
1601
+ },
1602
+ "skill-health": { rows: "/api/dsh-evolution/skill-health/rows" },
1603
+ merge: {
1604
+ view: "/api/dsh-evolution/merge/view",
1605
+ rows: "/api/dsh-evolution/merge/rows",
1606
+ run: "/api/dsh-evolution/merge/run",
1607
+ runs: "/api/dsh-evolution/merge/runs",
1608
+ body: "/api/dsh-evolution/merge/body",
1609
+ history: "/api/dsh-evolution/merge/history"
1610
+ },
1611
+ diagnostics: { view: "/api/dsh-evolution/diagnostics" },
1612
+ settings: {
1613
+ describe: "/api/dsh-evolution/settings/describe",
1614
+ mutate: "/api/dsh-evolution/settings/mutate"
1615
+ }
1616
+ };
1833
1617
  //#endregion
1834
1618
  //#region ../evolution-client-kit/src/settings-seat.ts
1835
1619
  const SETTINGS_DESCRIBE = ROUTES.settings.describe;
@@ -1972,6 +1756,53 @@ window.__ModuleLoader__.load({
1972
1756
  * @module @lmzhen/dsh-evolution-client-kit
1973
1757
  */
1974
1758
  //#endregion
1759
+ //#region ../evolution-client-kit/src/generated/fallbacks.generated.ts
1760
+ /**
1761
+ * The command-face fallback for every Host surface, generated from evolution-commands'
1762
+ * COMMAND_FALLBACKS (the ONE home). When the platform's connection carrier is absent (E-325) the
1763
+ * panels render this hint instead of a bare HTTP failure — same use case, command face.
1764
+ */
1765
+ const FALLBACKS = {
1766
+ approval: { command: "/evolution pending … /evolution approve <id> … /evolution reject <id> … /evolution release <id>" },
1767
+ "skill-history": { command: "/evolution skill undo <name> [--to v<N>](查看:/evolution skill history <name>)" },
1768
+ "skill-health": { command: "/evolution skills refresh(健康行:/evolution doctor)" },
1769
+ merge: { command: "/evolution merge status / pending / approve <id> / report" },
1770
+ diagnostics: {
1771
+ command: "/evolution doctor",
1772
+ platformCli: "dsh --json /evolution doctor"
1773
+ },
1774
+ settings: { command: "/evolution policy set <id> <value>(查看:/evolution policy)" }
1775
+ };
1776
+ //#endregion
1777
+ //#region ../evolution-client-kit/src/generated/states.generated.ts
1778
+ /** A neutral/ warn/ fail block; a fail block is role=alert so assistive tech announces it. */
1779
+ function Block(props) {
1780
+ const tone = props.tone ?? "neutral";
1781
+ return (0, react.createElement)("div", {
1782
+ "data-tone": tone,
1783
+ ...tone === "fail" ? { role: "alert" } : {}
1784
+ }, props.title === void 0 ? null : (0, react.createElement)("div", { "data-tone-title": tone }, props.title), props.children);
1785
+ }
1786
+ /** One classified failure as a reader-facing sentence. */
1787
+ function failureText(failure, t) {
1788
+ switch (failure.kind) {
1789
+ case "unauthenticated": return t("failureUnauthorized");
1790
+ case "forbidden": return t("failureForbidden");
1791
+ case "unmounted": return t("failureUnmounted");
1792
+ case "server": return failure.status === 0 ? t("failureServer") : t("failureServer") + " (HTTP " + String(failure.status) + ")";
1793
+ case "refused": return failure.message;
1794
+ }
1795
+ }
1796
+ /** The failed-state block: sentence + (when the surface exists but is unreachable) the command fallback. */
1797
+ function FailureBlock(props) {
1798
+ const hint = props.fallback === void 0 ? null : (0, react.createElement)("div", { "data-tone-hint": "true" }, props.t("fallbackHint", { command: props.fallback.command }));
1799
+ return Block({
1800
+ tone: "fail",
1801
+ title: failureText(props.failure, props.t),
1802
+ children: hint
1803
+ });
1804
+ }
1805
+ //#endregion
1975
1806
  //#region ../evolution-client-kit/src/tokens.ts
1976
1807
  /**
1977
1808
  * The scale, declared on one element so it never leaks into the host shell.
@@ -1981,29 +1812,8 @@ window.__ModuleLoader__.load({
1981
1812
  function tokenVars(scope) {
1982
1813
  return scope + "{--evo-type-lg:calc(1em + 2px);--evo-type-body:1em;--evo-type-sm:calc(1em - 1px);--evo-type-xs:calc(1em - 2px);--evo-space-2:2px;--evo-space-4:4px;--evo-space-6:6px;--evo-space-8:8px;--evo-space-10:10px;--evo-space-12:12px;--evo-space-14:14px;--evo-space-16:16px;--evo-space-20:20px;--evo-radius-chip:4px;--evo-radius-control:8px;--evo-radius-surface:12px;--evo-radius-pill:999px;--evo-hairline-width:.5px;--evo-hairline-marker:2px;--evo-measure-read:80ch;--evo-leading-base:1.5;--evo-tone-quiet:var(--dsw-alias-label-secondary);--evo-tone-primary:var(--dsw-alias-brand-primary);--evo-tone-danger:var(--dsw-alias-state-error-primary);--evo-tone-success:var(--dsw-alias-state-success-primary);--evo-focus-ring:2px solid var(--dsw-alias-label-primary);--evo-focus-offset:-2px;--evo-cap-aside:280px;--evo-cap-block:260px}";
1983
1814
  }
1984
- //#endregion
1985
- //#region src/panels/settings/SettingsSection.ts
1986
- /**
1987
- * The section shell: title, one-line explanation, then one card per namespace — with the staged-write
1988
- * window's card ahead of them all, because it is the only card here that waits on a decision.
1989
- *
1990
- * This is the shape the platform's own settings sections use — a heading plus the
1991
- * child slots it declared in its registration. The cards themselves are separate
1992
- * registrations keyed by namespace, so a namespace the Host does not serve simply
1993
- * renders nothing and a deployment that drops this row loses the whole section.
1994
- * @module @lmzhen/dsh-evolution-ui/panels/settings/client
1995
- */
1996
- /**
1997
- * Render the section.
1998
- * @param face - the shell's inject face: translator and child-slot renderer.
1999
- * @returns the section element.
2000
- */
2001
- function SettingsSection(face) {
2002
- return (0, react.createElement)("div", { className: "evolution-params" }, (0, react.createElement)("h2", { className: "evolution-params-title" }, face.t("title")), (0, react.createElement)("p", { className: "evolution-params-subtitle" }, face.t("subtitle")), (0, react.createElement)("p", { className: "evolution-params-about" }, "0.32.6 · " + face.t("aboutPurpose")), (0, react.createElement)("div", {
2003
- key: DIAGNOSTICS_CARD_KEY,
2004
- className: "evolution-params-group"
2005
- }, face.renderSlot(CARD_SLOT, {}, { entryKey: DIAGNOSTICS_CARD_KEY })));
2006
- }
1815
+ /** Stylesheet for {@link PAGE_SURFACE_CLASS}; consumers append it to their own CSS. */
1816
+ const PAGE_SURFACE_CSS = ".evolution-page-surface{min-height:100%;background:var(--dsw-alias-bg-base);color:var(--dsw-alias-label-primary)}";
2007
1817
  //#endregion
2008
1818
  //#region src/panels/settings/source.ts
2009
1819
  /** What a card shows when this UI found no settings seat at all. */
@@ -2141,12 +1951,47 @@ window.__ModuleLoader__.load({
2141
1951
  */
2142
1952
  function writeFaceFor(probe, namespace) {
2143
1953
  const form = () => probe()?.get(namespace);
1954
+ const saveOnce = async (ops) => {
1955
+ const seat = form();
1956
+ if (seat === void 0) return {
1957
+ written: false,
1958
+ readback: "failed",
1959
+ kind: "transport"
1960
+ };
1961
+ const before = seat.getSnapshot().revision;
1962
+ let written = false;
1963
+ try {
1964
+ written = await seat.mutate(ops);
1965
+ } catch (failure) {
1966
+ return {
1967
+ written: false,
1968
+ readback: "failed",
1969
+ kind: "transport",
1970
+ failure
1971
+ };
1972
+ }
1973
+ const snapshot = seat.getSnapshot();
1974
+ if (!written) return {
1975
+ written: false,
1976
+ readback: "failed",
1977
+ kind: "conflict"
1978
+ };
1979
+ if (snapshot.status !== "ready") return {
1980
+ written: true,
1981
+ readback: "failed",
1982
+ kind: "transport"
1983
+ };
1984
+ return {
1985
+ written: true,
1986
+ readback: snapshot.revision !== before ? "ok" : "failed"
1987
+ };
1988
+ };
2144
1989
  return {
2145
1990
  mutateOnce: async (ops) => {
2146
- const seat = form();
2147
- if (seat === void 0) return false;
2148
- return await seat.mutate(ops);
1991
+ if (form() === void 0) return false;
1992
+ return (await saveOnce(ops)).written;
2149
1993
  },
1994
+ saveOnce,
2150
1995
  clear: async (field) => {
2151
1996
  const seat = form();
2152
1997
  if (seat === void 0) return false;
@@ -2283,7 +2128,7 @@ window.__ModuleLoader__.load({
2283
2128
  * @param host - the document to inject into; defaults to the global one.
2284
2129
  * @returns the disposal; a no-op when there is no document or the tag was already present.
2285
2130
  */
2286
- function injectStyles$1(host) {
2131
+ function injectStyles(host) {
2287
2132
  const doc = host ?? globalThis.document;
2288
2133
  if (doc === void 0) return () => {};
2289
2134
  doc.querySelector("style[data-plugin-css=\"@lmzhen/dsh-evolution-ui/settings.css\"]")?.remove?.();
@@ -2305,7 +2150,7 @@ window.__ModuleLoader__.load({
2305
2150
  * @param ctx - client cordis context.
2306
2151
  */
2307
2152
  function apply$2(ctx) {
2308
- ctx.effect(() => injectStyles$1(), "evolution-settings-ui: styles");
2153
+ ctx.effect(() => injectStyles(), "evolution-settings-ui: styles");
2309
2154
  const seam = ctx;
2310
2155
  let registered = false;
2311
2156
  ctx.effect(() => {
@@ -2348,54 +2193,132 @@ window.__ModuleLoader__.load({
2348
2193
  ctx.inject?.(["remote"], () => {
2349
2194
  sources.revive();
2350
2195
  });
2351
- seam.slots.inject("settings.section", function* () {
2352
- yield seam.slots.register({
2353
- name: "settings.section",
2354
- id: SECTION_ID,
2355
- order: 25,
2356
- label: () => t("title"),
2357
- locale: NS$1,
2358
- children: { [CARD_SLOT]: {
2359
- kind: "keyed",
2360
- scope: "root"
2361
- } }
2362
- }, SettingsSection);
2363
- });
2364
- for (const [index, section] of CLIENT_PARAM_SECTIONS.entries()) {
2196
+ const cards = CLIENT_PARAM_SECTIONS.map((section) => {
2365
2197
  const source = sources.sourceFor(section.seatKey);
2366
- registerPluginParamSource(section.namespace, source);
2367
- const { mutateOnce, clear } = writeFaceFor(probe, section.namespace);
2368
- const face = () => ({
2198
+ const { mutateOnce, saveOnce, clear } = writeFaceFor(probe, section.namespace);
2199
+ return {
2369
2200
  namespace: section.namespace,
2370
2201
  fields: section.fields,
2371
2202
  t,
2372
2203
  lang: language,
2373
2204
  mutateOnce,
2205
+ saveOnce,
2374
2206
  clear,
2375
2207
  hooks: { paramSection: source }
2376
- });
2377
- seam.slots.inject("settings.plugin.item", function* () {
2378
- yield seam.slots.register({
2379
- name: "settings.plugin.item",
2380
- id: section.namespace,
2381
- order: index * 10,
2382
- locale: NS$1,
2383
- inject: face
2384
- }, PluginParamCard);
2385
- });
2208
+ };
2209
+ });
2210
+ seam.slots.inject("settings.plugins.tab", function* () {
2211
+ yield seam.slots.register({
2212
+ name: "settings.plugins.tab",
2213
+ id: SECTION_ID,
2214
+ order: 25,
2215
+ label: () => t("title"),
2216
+ locale: NS$1,
2217
+ inject: () => ({ cards })
2218
+ }, EvolutionSettingsTab);
2219
+ });
2220
+ }
2221
+ /** One family tab: every parameter card lives inside the platform's Plugins page. */
2222
+ function EvolutionSettingsTab(props) {
2223
+ return (0, react.createElement)("div", { className: "evolution-settings-tab" }, ...props.cards.map((face, index) => (0, react.createElement)(PluginParamCard, {
2224
+ key: face.namespace + ":" + String(index),
2225
+ ...face
2226
+ })));
2227
+ }
2228
+ //#endregion
2229
+ //#region ../evolution-client-kit/src/generated/route-client.generated.ts
2230
+ /** Classify a non-2xx status: 401/403/404 keep their own kind, everything else is a server answer. */
2231
+ function classify(status) {
2232
+ if (status === 401) return {
2233
+ kind: "unauthenticated",
2234
+ status: 401
2235
+ };
2236
+ if (status === 403) return {
2237
+ kind: "forbidden",
2238
+ status: 403
2239
+ };
2240
+ if (status === 404) return {
2241
+ kind: "unmounted",
2242
+ status: 404
2243
+ };
2244
+ return {
2245
+ kind: "server",
2246
+ status
2247
+ };
2248
+ }
2249
+ /**
2250
+ * One request, no retries and no body parsing for the platform's admission answers.
2251
+ * @param doFetch - the fetch in use (the browser's, or a spec's stub).
2252
+ * @param path - the exact route path from the generated ROUTES table.
2253
+ * @param init - standard RequestInit.
2254
+ * @returns the data, or the classified failure (network/parse failures read as 'server').
2255
+ */
2256
+ async function requestJson(doFetch, path, init) {
2257
+ let response;
2258
+ try {
2259
+ response = await doFetch(path, init);
2260
+ } catch (error) {
2261
+ return {
2262
+ ok: false,
2263
+ failure: {
2264
+ kind: "server",
2265
+ status: 0,
2266
+ detail: error instanceof Error ? error.message : String(error)
2267
+ }
2268
+ };
2269
+ }
2270
+ if (!response.ok) return {
2271
+ ok: false,
2272
+ failure: classify(response.status)
2273
+ };
2274
+ let body;
2275
+ try {
2276
+ body = await response.json();
2277
+ } catch {
2278
+ return {
2279
+ ok: false,
2280
+ failure: {
2281
+ kind: "server",
2282
+ status: response.status
2283
+ }
2284
+ };
2285
+ }
2286
+ if (typeof body === "object" && body !== null) {
2287
+ const record = body;
2288
+ if (record.ok === true) return {
2289
+ ok: true,
2290
+ data: record.data
2291
+ };
2292
+ const nested = record.error;
2293
+ const errorRecord = typeof nested === "object" && nested !== null ? nested : void 0;
2294
+ const code = typeof record.code === "string" && record.code !== "" ? record.code : typeof errorRecord?.code === "string" ? errorRecord.code : void 0;
2295
+ const message = typeof record.message === "string" && record.message !== "" ? record.message : typeof errorRecord?.message === "string" ? errorRecord.message : void 0;
2296
+ if (record.ok === false && code !== void 0 && message !== void 0 && message !== "") return {
2297
+ ok: false,
2298
+ failure: {
2299
+ kind: "refused",
2300
+ code,
2301
+ message
2302
+ }
2303
+ };
2304
+ }
2305
+ return {
2306
+ ok: false,
2307
+ failure: {
2308
+ kind: "server",
2309
+ status: response.status
2310
+ }
2311
+ };
2312
+ }
2313
+ /** The one-sentence rendering legacy callers attach to a classified failure. */
2314
+ function failureMessage(failure) {
2315
+ switch (failure.kind) {
2316
+ case "refused": return failure.message;
2317
+ case "server": return failure.status === 0 ? failure.detail === void 0 || failure.detail === "" ? "网络错误:无法连接宿主" : failure.detail : "HTTP " + String(failure.status);
2318
+ case "unauthenticated": return "HTTP 401";
2319
+ case "forbidden": return "HTTP 403";
2320
+ case "unmounted": return "HTTP 404";
2386
2321
  }
2387
- seam.slots.inject(CARD_SLOT, function* () {
2388
- const diagApi = createDiagnosticsApi((input, init) => fetch(input, init));
2389
- yield seam.slots.register({
2390
- name: CARD_SLOT,
2391
- key: DIAGNOSTICS_CARD_KEY,
2392
- inject: () => ({
2393
- t,
2394
- api: diagApi,
2395
- fallback: FALLBACKS.diagnostics
2396
- })
2397
- }, DiagnosticsCard);
2398
- });
2399
2322
  }
2400
2323
  //#endregion
2401
2324
  //#region src/panels/history/client/api.ts
@@ -2600,8 +2523,7 @@ window.__ModuleLoader__.load({
2600
2523
  * independent review flagged that this reader had no case at all (P2-11).
2601
2524
  */
2602
2525
  function confirmModeFrom(user) {
2603
- const value = typeof user === "object" && user !== null ? user.approvalConfirm : void 0;
2604
- return value === "ask" || value === "timeout" ? "ask" : "auto";
2526
+ return (typeof user === "object" && user !== null ? user.approvalConfirm : void 0) === "auto" ? "auto" : "ask";
2605
2527
  }
2606
2528
  /** Which word one age bucket gets; an unknown bucket falls back to the count form. */
2607
2529
  const AGE_KEYS = Object.freeze({
@@ -2638,7 +2560,7 @@ window.__ModuleLoader__.load({
2638
2560
  function PendingCard(props) {
2639
2561
  const { t, api } = props;
2640
2562
  const translate = (key, values) => t(key, values);
2641
- const confirmMode = props.confirmMode?.() ?? "auto";
2563
+ const confirmMode = props.confirmMode?.() ?? "ask";
2642
2564
  const [armed, setArmed] = (0, react.useState)(null);
2643
2565
  const [state, setState] = (0, react.useState)(INITIAL_PENDING_STATE);
2644
2566
  const [open, setOpen] = (0, react.useState)(true);
@@ -2821,7 +2743,7 @@ window.__ModuleLoader__.load({
2821
2743
  }, (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", {
2822
2744
  className: "evolution-param-value",
2823
2745
  title: entry.summary
2824
- }, summaryLine(entry))), (0, react.createElement)("p", { className: "evolution-param-hint" }, age(entry)), armed === entry.id ? (0, react.createElement)("p", {
2746
+ }, summaryLine(entry))), (0, react.createElement)("p", { className: "evolution-param-hint" }, age(entry)), summaryLine(entry) === entry.summary ? null : (0, react.createElement)("details", { className: "evolution-param-details" }, (0, react.createElement)("summary", null, t("approvalRawSummary")), (0, react.createElement)("p", { className: "evolution-param-hint" }, entry.summary)), armed === entry.id ? (0, react.createElement)("p", {
2825
2747
  className: "evolution-param-note",
2826
2748
  role: "status"
2827
2749
  }, (0, react.createElement)("span", { className: "evolution-param-value" }, t("approvalConfirmNamed", { n: summaryLine(entry) })), (0, react.createElement)("button", {
@@ -2900,6 +2822,23 @@ window.__ModuleLoader__.load({
2900
2822
  }, state.refreshing ? t("diagRefreshing") : t("approvalRefresh"))), open ? (0, react.createElement)("div", { className: "evolution-param-body" }, body()) : null);
2901
2823
  }
2902
2824
  //#endregion
2825
+ //#region src/panels/settings/diagnostics-state.ts
2826
+ /**
2827
+ * The diagnostics card's data face (W7 S7): ONE request to `HOST_ROUTES.diagnostics.view` through the
2828
+ * GENERATED route client, and the four-state answer the card renders.
2829
+ *
2830
+ * The Host route is the SAME `buildDoctorView` the CLI prints (W6/W8), so this client owns no facts
2831
+ * and no computation — it only renders.
2832
+ * @module @lmzhen/dsh-evolution-ui/panels/settings/client/diagnostics-state
2833
+ */
2834
+ /**
2835
+ * Build the diagnostics face over one fetch implementation.
2836
+ * @param doFetch - the browser fetch, or a spec stub.
2837
+ */
2838
+ function createDiagnosticsApi(doFetch = fetch) {
2839
+ return { view: async () => await requestJson(doFetch, ROUTES.diagnostics.view, { headers: { accept: "application/json" } }) };
2840
+ }
2841
+ //#endregion
2903
2842
  //#region src/panels/history/client/messages.ts
2904
2843
  /**
2905
2844
  * Panel copy, in a zh/en dictionary registered through the platform’s locale seat.
@@ -2919,6 +2858,7 @@ window.__ModuleLoader__.load({
2919
2858
  "workbench.pending": "待批",
2920
2859
  "workbench.merge": "合并",
2921
2860
  "workbench.health": "健康",
2861
+ "workbench.diagnostics": "诊断",
2922
2862
  hint: "每次技能写入留下的内容版本。回退只改 SKILL.md 正文,标记、使用计数与策展状态不变。内容相同的版本会同时标「当前」。",
2923
2863
  search: "按名字或描述筛选",
2924
2864
  "search.none": "没有匹配的技能。",
@@ -3010,6 +2950,7 @@ window.__ModuleLoader__.load({
3010
2950
  "workbench.pending": "Pending",
3011
2951
  "workbench.merge": "Merge",
3012
2952
  "workbench.health": "Health",
2953
+ "workbench.diagnostics": "Diagnostics",
3013
2954
  hint: "The content versions every skill write leaves behind. Restoring changes the SKILL.md body only: markers, usage counts and curation state stay as they are. Versions holding the same content are all marked “current”.",
3014
2955
  search: "Filter by name or description",
3015
2956
  "search.none": "No skill matches.",
@@ -3784,16 +3725,18 @@ window.__ModuleLoader__.load({
3784
3725
  * @param name - the skill to open.
3785
3726
  * @param onFailure - what to do with a failed read.
3786
3727
  */
3787
- const showVersions = (name, onFailure = failed) => {
3728
+ const showVersions = (name, onFailure = failed, keepContent = false) => {
3788
3729
  const ticket = readTicket.current + 1;
3789
3730
  readTicket.current = ticket;
3790
3731
  setSelected(name);
3791
- setPayload(void 0);
3792
- setVersionsFailed(void 0);
3793
- setPending(void 0);
3794
- setExpanded(void 0);
3795
- setDiffs(/* @__PURE__ */ new Map());
3796
- setBodies(/* @__PURE__ */ new Map());
3732
+ if (!keepContent) {
3733
+ setPayload(void 0);
3734
+ setVersionsFailed(void 0);
3735
+ setPending(void 0);
3736
+ setExpanded(void 0);
3737
+ setDiffs(/* @__PURE__ */ new Map());
3738
+ setBodies(/* @__PURE__ */ new Map());
3739
+ }
3797
3740
  face.loadVersions(name).then((next) => {
3798
3741
  if (readTicket.current === ticket) {
3799
3742
  setPayload(next);
@@ -3816,7 +3759,7 @@ window.__ModuleLoader__.load({
3816
3759
  /** What the refresh control does: both reads again, and the sentence stays. */
3817
3760
  const refresh = () => {
3818
3761
  reloadSkills();
3819
- if (selected !== void 0) showVersions(selected);
3762
+ if (selected !== void 0) showVersions(selected, failed, true);
3820
3763
  };
3821
3764
  (0, react.useEffect)(() => {
3822
3765
  if (face.focusSkill === void 0 || face.focusSkill === "") return;
@@ -3981,6 +3924,128 @@ window.__ModuleLoader__.load({
3981
3924
  return next;
3982
3925
  }
3983
3926
  //#endregion
3927
+ //#region src/panels/settings/DiagnosticsCard.ts
3928
+ /**
3929
+ * The diagnostics card (W7 S7): the settings face of the SAME DiagnosticsView the CLI and the
3930
+ * `/api/dsh-evolution/diagnostics` route answer.
3931
+ *
3932
+ * Read-only by construction: it fetches once on mount and only refetches on an explicit Refresh; a
3933
+ * failed read renders the shared FailureBlock with the command-face fallback. The client owns no
3934
+ * key list and no status vocabulary: it renders the host view's frozen runtime rows and leads with
3935
+ * a plain-language summary. The view's documentation facts are a CLI/JSON concern and are
3936
+ * deliberately NOT rendered here (0.27.2 real-machine feedback: a settings page is not a spec
3937
+ * sheet; the runtime window is the whole reading).
3938
+ * @module @lmzhen/dsh-evolution-ui/panels/settings/client/DiagnosticsCard
3939
+ */
3940
+ /** Which word one status gets (the frozen W1 status set). */
3941
+ const STATUS_KEYS = Object.freeze({
3942
+ ok: "diagStatusOk",
3943
+ warn: "diagStatusWarn",
3944
+ fail: "diagStatusFail",
3945
+ unknown: "diagStatusUnknown"
3946
+ });
3947
+ /** The summary word for a badge status (the same frozen set; ok has no count). */
3948
+ const SUMMARY_KEYS = Object.freeze({
3949
+ ok: "diagSummaryOk",
3950
+ warn: "diagSummaryWarn",
3951
+ fail: "diagSummaryFail",
3952
+ unknown: "diagSummaryUnknown"
3953
+ });
3954
+ /** The one status the card leads with: fail > warn > unknown > ok. */
3955
+ function summaryOf(rows) {
3956
+ const count = (status) => rows.filter((row) => row.status === status).length;
3957
+ for (const status of [
3958
+ "fail",
3959
+ "warn",
3960
+ "unknown"
3961
+ ]) {
3962
+ const n = count(status);
3963
+ if (n > 0) return {
3964
+ status,
3965
+ count: n
3966
+ };
3967
+ }
3968
+ return {
3969
+ status: "ok",
3970
+ count: 0
3971
+ };
3972
+ }
3973
+ /**
3974
+ * Render the card.
3975
+ * @param props - the copy seat, the host call and the command fallback.
3976
+ * @returns the card.
3977
+ */
3978
+ function DiagnosticsCard(props) {
3979
+ const { t, api } = props;
3980
+ const translate = (key, values) => t(key, values);
3981
+ const [state, setState] = (0, react.useState)({ phase: "loading" });
3982
+ const [refreshing, setRefreshing] = (0, react.useState)(false);
3983
+ const [open, setOpen] = (0, react.useState)(false);
3984
+ const ticket = (0, react.useRef)(0);
3985
+ const read = (keepView) => {
3986
+ const current = ticket.current + 1;
3987
+ ticket.current = current;
3988
+ if (keepView) setRefreshing(true);
3989
+ else setState({ phase: "loading" });
3990
+ api.view().then((answer) => {
3991
+ if (ticket.current !== current) return;
3992
+ setState(answer.ok ? {
3993
+ phase: "ready",
3994
+ view: answer.data
3995
+ } : {
3996
+ phase: "failed",
3997
+ failure: answer.failure
3998
+ });
3999
+ }).finally(() => {
4000
+ if (ticket.current === current) setRefreshing(false);
4001
+ });
4002
+ };
4003
+ (0, react.useEffect)(() => {
4004
+ read(false);
4005
+ }, []);
4006
+ const refresh = () => {
4007
+ read(true);
4008
+ };
4009
+ const summary = state.phase === "ready" ? summaryOf(state.view.runtime) : void 0;
4010
+ const header = (0, react.createElement)("button", {
4011
+ type: "button",
4012
+ className: "evolution-param-head",
4013
+ "aria-expanded": open ? "true" : "false",
4014
+ onClick: () => {
4015
+ setOpen(!open);
4016
+ }
4017
+ }, (0, react.createElement)("span", { className: "evolution-param-card-title" }, t("diagTitle")), summary === void 0 ? null : (0, react.createElement)("span", {
4018
+ className: "evo-diag-badge",
4019
+ "data-status": summary.status
4020
+ }, t(SUMMARY_KEYS[summary.status], summary.count === 0 ? void 0 : { n: summary.count })), (0, react.createElement)("span", { className: "evolution-param-chevron" }, open ? "▲" : "▼"));
4021
+ const runtime = (view) => view.runtime.length === 0 ? null : (0, react.createElement)("div", {}, (0, react.createElement)("h3", { className: "evolution-param-group" }, t("diagRuntimeTitle")), (0, react.createElement)("div", { className: "evo-diag-list" }, ...view.runtime.map((row) => (0, react.createElement)("div", {
4022
+ key: "runtime-" + row.key,
4023
+ className: "evo-diag-item",
4024
+ "data-kind": "runtime",
4025
+ "data-status": row.status
4026
+ }, (0, react.createElement)("div", { className: "evo-diag-row-head" }, (0, react.createElement)("span", { className: "evo-diag-dot" }), (0, react.createElement)("span", { className: "evo-diag-label" }, row.label), (0, react.createElement)("span", { className: "evo-diag-status" }, t(STATUS_KEYS[row.status]))), row.detail === void 0 ? null : (0, react.createElement)("span", { className: "evo-diag-detail" }, row.detail), row.remedy === void 0 ? null : (0, react.createElement)("span", { className: "evo-diag-remedy" }, t("diagRemedy") + row.remedy)))));
4027
+ const body = () => {
4028
+ if (state.phase === "loading") return (0, react.createElement)("p", { className: "evolution-param-loading" }, t("stateLoading"));
4029
+ if (state.phase === "failed") return FailureBlock({
4030
+ failure: state.failure,
4031
+ t: translate,
4032
+ fallback: props.fallback
4033
+ });
4034
+ const view = state.view;
4035
+ return (0, react.createElement)("div", {}, (0, react.createElement)("p", { className: "evolution-param-note" }, t("diagSubtitle")), (0, react.createElement)("div", { className: "evolution-param-actions" }, (0, react.createElement)("button", {
4036
+ type: "button",
4037
+ className: "evolution-param-button",
4038
+ onClick: refresh,
4039
+ disabled: refreshing,
4040
+ "data-action": "diag-refresh"
4041
+ }, t(refreshing ? "diagRefreshing" : "diagRefresh"))), runtime(view), view.runtime.length === 0 ? (0, react.createElement)("p", { className: "evolution-param-note" }, t("diagNoRows")) : null);
4042
+ };
4043
+ return (0, react.createElement)("section", {
4044
+ className: "evolution-param-card",
4045
+ "data-card": "diagnostics"
4046
+ }, header, open ? (0, react.createElement)("div", { className: "evolution-param-body" }, body()) : null);
4047
+ }
4048
+ //#endregion
3984
4049
  //#region src/panels/history/client/Workbench.ts
3985
4050
  /**
3986
4051
  * The family's one task page. G3 owns the history view; later groups add health
@@ -3989,11 +4054,6 @@ window.__ModuleLoader__.load({
3989
4054
  */
3990
4055
  /** Stable page id used by the sidebar row, the page slot key, and navigation params. */
3991
4056
  const WORKBENCH_ID = "evolution.workbench";
3992
- /** Convert a navigation params value to a skill name only when it is a non-empty string. */
3993
- function skillOf(params) {
3994
- const value = params?.["skillName"];
3995
- return typeof value === "string" && value !== "" ? value : void 0;
3996
- }
3997
4057
  /**
3998
4058
  * Render the workbench page. The platform keeps it mounted; this component only
3999
4059
  * consumes a new explicit revision exactly once.
@@ -4003,25 +4063,21 @@ window.__ModuleLoader__.load({
4003
4063
  function WorkbenchPage(props) {
4004
4064
  const [selectedSkill, setSelectedSkill] = (0, react.useState)(void 0);
4005
4065
  const [view, setView] = (0, react.useState)("history");
4006
- const lastRevision = (0, react.useRef)(void 0);
4007
- (0, react.useEffect)(() => {
4008
- if (props.revision === void 0 || props.revision === lastRevision.current) return;
4009
- lastRevision.current = props.revision;
4010
- const requested = props.params?.["view"];
4011
- if (requested === "pending" || requested === "merge" || requested === "health") {
4012
- setView(requested);
4013
- return;
4014
- }
4066
+ const [focusRevision, setFocusRevision] = (0, react.useState)(0);
4067
+ const openHistory = (skill) => {
4068
+ setSelectedSkill(skill);
4015
4069
  setView("history");
4016
- const skill = skillOf(props.params);
4017
- if (skill !== void 0) setSelectedSkill(skill);
4018
- }, [props.params, props.revision]);
4070
+ setFocusRevision((value) => value + 1);
4071
+ };
4072
+ const hasMerge = props.hasView?.("merge") ?? true;
4073
+ const hasHealth = props.hasView?.("health") ?? true;
4019
4074
  const viewOrder = [
4020
4075
  "history",
4021
4076
  "pending",
4022
4077
  "merge",
4023
- "health"
4024
- ];
4078
+ "health",
4079
+ "diagnostics"
4080
+ ].filter((value) => (value !== "merge" || hasMerge) && (value !== "health" || hasHealth));
4025
4081
  const onTabKey = (event, current) => {
4026
4082
  const delta = event.key === "ArrowRight" ? 1 : event.key === "ArrowLeft" ? -1 : 0;
4027
4083
  if (delta === 0 && event.key !== "Home" && event.key !== "End") return;
@@ -4033,9 +4089,6 @@ window.__ModuleLoader__.load({
4033
4089
  document.getElementById("evo-workbench-tab-" + next)?.focus();
4034
4090
  });
4035
4091
  };
4036
- const hasMerge = props.hasView?.("merge") ?? true;
4037
- const hasHealth = props.hasView?.("health") ?? true;
4038
- const focusRevision = props.revision === lastRevision.current ? props.revision : void 0;
4039
4092
  return (0, react.createElement)("section", {
4040
4093
  className: "evolution-page-surface evo-workbench",
4041
4094
  "data-page": view
@@ -4099,7 +4152,21 @@ window.__ModuleLoader__.load({
4099
4152
  onKeyDown: (event) => {
4100
4153
  onTabKey(event, "health");
4101
4154
  }
4102
- }, props.t("workbench.health")))), (0, react.createElement)("div", {
4155
+ }, props.t("workbench.health")), (0, react.createElement)("button", {
4156
+ id: "evo-workbench-tab-diagnostics",
4157
+ type: "button",
4158
+ role: "tab",
4159
+ tabIndex: view === "diagnostics" ? 0 : -1,
4160
+ "aria-selected": view === "diagnostics" ? "true" : "false",
4161
+ "aria-controls": "evo-workbench-panel",
4162
+ "data-active": view === "diagnostics" ? "true" : void 0,
4163
+ onClick: () => {
4164
+ setView("diagnostics");
4165
+ },
4166
+ onKeyDown: (event) => {
4167
+ onTabKey(event, "diagnostics");
4168
+ }
4169
+ }, props.t("workbench.diagnostics")))), (0, react.createElement)("div", {
4103
4170
  className: "evo-workbench-body",
4104
4171
  id: "evo-workbench-panel",
4105
4172
  role: "tabpanel"
@@ -4107,27 +4174,30 @@ window.__ModuleLoader__.load({
4107
4174
  ...props.history,
4108
4175
  focusSkill: selectedSkill,
4109
4176
  focusRevision
4110
- }) : view === "pending" ? (0, react.createElement)(PendingCard, props.pending) : null, (0, react.createElement)("div", {
4177
+ }) : view === "pending" ? (0, react.createElement)(PendingCard, props.pending) : view === "diagnostics" ? (0, react.createElement)(DiagnosticsCard, props.diagnostics) : null, (0, react.createElement)("div", {
4111
4178
  hidden: view !== "merge" && view !== "health",
4112
4179
  className: "evo-workbench-feature"
4113
- }, props.renderSlot?.("evolution.workbench.view", { activeView: view }) ?? null)));
4180
+ }, props.renderSlot?.("evolution.workbench.view", {
4181
+ activeView: view,
4182
+ openHistory
4183
+ }) ?? null)));
4114
4184
  }
4115
- /**
4116
- * Render the sidebar entry that opens the workbench.
4117
- * @param props - active page id, opener, and copy.
4118
- * @returns the row button.
4119
- */
4120
- function WorkbenchNav(props) {
4121
- const active = props.activePageId === WORKBENCH_ID;
4122
- return (0, react.createElement)("button", {
4123
- type: "button",
4124
- className: "evo-workbench-nav",
4125
- "data-active": active ? "true" : void 0,
4126
- "aria-current": active ? "page" : void 0,
4127
- onClick: () => {
4128
- props.openPage(WORKBENCH_ID);
4129
- }
4130
- }, props.t("entry.label"));
4185
+ /** The sidebar icon; the platform renders the row label, tooltip and active state. */
4186
+ function WorkbenchIcon(props) {
4187
+ const size = props.size ?? 16;
4188
+ return (0, react.createElement)("svg", {
4189
+ width: size,
4190
+ height: size,
4191
+ viewBox: "0 0 16 16",
4192
+ "aria-hidden": "true",
4193
+ focusable: "false"
4194
+ }, (0, react.createElement)("path", {
4195
+ d: "M2 3.5h12M2 8h12M2 12.5h8",
4196
+ fill: "none",
4197
+ stroke: "currentColor",
4198
+ "stroke-width": props.active === true ? 1.7 : 1.4,
4199
+ "stroke-linecap": "round"
4200
+ }));
4131
4201
  }
4132
4202
  /** Workbench shell and sidebar row; colours come from the platform tokens. */
4133
4203
  const WORKBENCH_CSS = [
@@ -4140,6 +4210,7 @@ window.__ModuleLoader__.load({
4140
4210
  ".evo-workbench-tabs button:focus-visible{outline:var(--evo-focus-ring);outline-offset:var(--evo-focus-offset)}",
4141
4211
  ".evo-workbench-title{margin:0;font-size:var(--evo-type-lg);font-weight:600;line-height:var(--evo-leading-base)}",
4142
4212
  ".evo-workbench-body{display:flex;flex:1;min-height:0;min-width:0;overflow:hidden}",
4213
+ ".evo-workbench-body>.evolution-param-card{flex:1;min-width:0;overflow:auto}",
4143
4214
  ".evo-workbench-nav{display:flex;width:100%;align-items:center;gap:var(--evo-space-8);padding:var(--evo-space-6) var(--evo-space-12);border:0;border-radius:var(--evo-radius-control);background:transparent;color:var(--dsw-alias-label-secondary);font:inherit;font-size:var(--evo-type-sm);text-align:left;cursor:pointer}",
4144
4215
  ".evo-workbench-nav:hover,.evo-workbench-nav[data-active=\"true\"]{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}",
4145
4216
  ".evo-workbench-nav:focus-visible{outline:var(--evo-focus-ring);outline-offset:var(--evo-focus-offset)}",
@@ -4147,55 +4218,37 @@ window.__ModuleLoader__.load({
4147
4218
  ].join("");
4148
4219
  //#endregion
4149
4220
  //#region src/panels/history/client/index.ts
4150
- function injectStyles(documentRef, css) {
4151
- if (documentRef === void 0) return () => {};
4152
- documentRef.querySelector("style[data-plugin-css=\"@lmzhen/dsh-evolution-ui/panel.css\"]")?.remove();
4153
- const tag = documentRef.createElement("style");
4154
- tag.setAttribute("data-plugin-css", CSS_TAG_ID);
4155
- tag.textContent = css;
4156
- documentRef.head.appendChild(tag);
4157
- return () => {
4158
- tag.remove();
4159
- };
4160
- }
4161
4221
  /**
4162
- * Register the locale namespace, the workbench page, and its sidebar entry.
4222
+ * Register the locale namespace, the workbench main occupant and its sidebar row.
4163
4223
  * @param ctx - client cordis context.
4164
4224
  */
4165
4225
  function apply$1(ctx) {
4166
- const seam = ctx;
4167
4226
  const api = createSkillHistoryApi();
4168
- let registered = false;
4169
- ctx.effect(() => {
4227
+ const settingsSeat = createSettingsSeat(ctx);
4228
+ let approvalConfirmMode = "ask";
4229
+ const approvalForm = settingsSeat.get("evolution-approval");
4230
+ const stopApprovalWatch = approvalForm?.subscribe(() => {
4231
+ approvalConfirmMode = confirmModeFrom(approvalForm.getSnapshot().user);
4232
+ });
4233
+ if (stopApprovalWatch !== void 0) ctx.effect(() => stopApprovalWatch, "evolution-history: approval confirm mode");
4234
+ const confirmMode = () => approvalConfirmMode;
4235
+ const hasView = (view) => {
4170
4236
  try {
4171
- const dispose = seam.locale.register(NS, {
4172
- zh,
4173
- en
4174
- });
4175
- registered = true;
4176
- return dispose;
4177
- } catch (error) {
4178
- ctx.logger.warn("evolution-skill-history: the locale seat refused the dictionary (" + String(error) + "); the workbench falls back to the bundle copy");
4179
- return () => {};
4237
+ return (ctx.slots?.entries?.("evolution.workbench.view") ?? []).some((entry) => entry.options?.id === (view === "merge" ? "merge" : "skill-health"));
4238
+ } catch {
4239
+ return false;
4180
4240
  }
4181
- }, "evolution-skill-history: locale");
4182
- ctx.effect(() => injectStyles(typeof document === "undefined" ? void 0 : document, CSS + WORKBENCH_CSS), "evolution-skill-history: styles");
4183
- const fallback = zh;
4184
- const lookup = (key) => {
4185
- const own = fallback[key] ?? key;
4186
- if (!registered) return own;
4241
+ };
4242
+ const pendingT = (key, values) => {
4243
+ let template;
4187
4244
  try {
4188
- return seam.locale.bind(NS)(key);
4245
+ template = ctx.locale.bind(NS$1)(key);
4189
4246
  } catch {
4190
- return own;
4247
+ template = message(key);
4191
4248
  }
4249
+ return values === void 0 ? template : fill(template, values);
4192
4250
  };
4193
- const t = (key, values) => {
4194
- const template = lookup(key);
4195
- if (values === void 0) return template;
4196
- return template.replace(/\{(\w+)\}/g, (match, slot) => values[slot] === void 0 ? match : String(values[slot]));
4197
- };
4198
- const historyFace = () => ({
4251
+ const historyFace = (t) => ({
4199
4252
  t,
4200
4253
  format: (key, values) => t(key, values),
4201
4254
  loadSkills: api.skills,
@@ -4209,61 +4262,42 @@ window.__ModuleLoader__.load({
4209
4262
  footnotes: t("markdown.footnotes")
4210
4263
  }
4211
4264
  });
4212
- const pendingT = (key, values) => {
4213
- let template;
4214
- try {
4215
- template = seam.locale.bind(NS$1)(key);
4216
- } catch {
4217
- template = message(key);
4218
- }
4219
- return values === void 0 ? template : fill(template, values);
4220
- };
4221
- const settingsSeat = createSettingsSeat(ctx);
4222
- let approvalConfirmMode = "ask";
4223
- const approvalForm = settingsSeat.get("evolution-approval");
4224
- const stopApprovalWatch = approvalForm?.subscribe(() => {
4225
- approvalConfirmMode = confirmModeFrom(approvalForm.getSnapshot().user);
4226
- });
4227
- if (stopApprovalWatch !== void 0) ctx.effect(() => stopApprovalWatch, "evolution-history: approval confirm mode");
4228
- const confirmMode = () => approvalConfirmMode;
4229
- const hasView = (view) => {
4230
- try {
4231
- return (seam.slots.entries?.("evolution.workbench.view") ?? []).some((entry) => entry.options?.id === (view === "merge" ? "merge" : "skill-health"));
4232
- } catch {
4233
- return false;
4234
- }
4235
- };
4236
4265
  const pendingFace = () => ({
4237
4266
  t: pendingT,
4238
4267
  api: createApprovalApi((input, init) => fetch(input, init)),
4239
4268
  confirmMode,
4240
4269
  fallback: FALLBACKS.approval
4241
4270
  });
4242
- const label = () => t("entry.label");
4243
- seam.slots.inject("sidebar.pages", function* () {
4244
- yield seam.slots.register({
4245
- name: "sidebar.pages",
4246
- id: WORKBENCH_ID,
4247
- order: 35,
4248
- label,
4249
- inject: () => ({ t })
4250
- }, WorkbenchNav);
4271
+ const diagnosticsFace = () => ({
4272
+ t: pendingT,
4273
+ api: createDiagnosticsApi((input, init) => fetch(input, init)),
4274
+ fallback: FALLBACKS.diagnostics
4251
4275
  });
4252
- seam.slots.inject("layout.page", function* () {
4253
- yield seam.slots.register({
4254
- name: "layout.page",
4255
- key: WORKBENCH_ID,
4256
- children: { "evolution.workbench.view": {
4257
- kind: "list",
4258
- scope: "root"
4259
- } },
4260
- inject: () => ({
4261
- t,
4262
- history: historyFace(),
4263
- pending: pendingFace(),
4264
- hasView
4265
- })
4266
- }, WorkbenchPage);
4276
+ registerSidebarPanel(ctx, {
4277
+ id: WORKBENCH_ID,
4278
+ order: 35,
4279
+ namespace: NS,
4280
+ dictionaries: {
4281
+ zh,
4282
+ en
4283
+ },
4284
+ styles: {
4285
+ css: CSS + WORKBENCH_CSS + PAGE_SURFACE_CSS,
4286
+ tagId: CSS_TAG_ID
4287
+ },
4288
+ icon: WorkbenchIcon,
4289
+ component: WorkbenchPage,
4290
+ body: (t) => ({
4291
+ t,
4292
+ history: historyFace(t),
4293
+ pending: pendingFace(),
4294
+ diagnostics: diagnosticsFace(),
4295
+ hasView
4296
+ }),
4297
+ children: { "evolution.workbench.view": {
4298
+ kind: "list",
4299
+ scope: "root"
4300
+ } }
4267
4301
  });
4268
4302
  }
4269
4303
  //#endregion