@lmzhen/dsh-evolution-ui 0.32.6 → 0.34.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,14 @@ 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
+ readbackStale: "显示的是最后一次成功读取的值;当前回读失败,刷新后可能变化。",
862
+ saveConflict: "这个值没有被接受:设置已被其他页面修改。请重读后再保存。",
1179
863
  resetDone: "已恢复默认值。",
1180
864
  refused: "这个值没有被接受(界面上保持原样)。想看原因:在对话里让它说明。",
1181
865
  resetRefused: "恢复默认值没有生效(这一行还是你的值)。想看原因:在对话里让它说明。",
@@ -1243,6 +927,8 @@ window.__ModuleLoader__.load({
1243
927
  diagStatusUnknown: "未知",
1244
928
  diagRemedy: "建议:",
1245
929
  diagNoRows: "没有可显示的运行状态",
930
+ diagPurpose: "用途:检查插件挂载、平台服务和数据面是否正常,不修改任何状态。",
931
+ diagFooter: "诊断 schema v{version} · 生成于 {at}",
1246
932
  stateLoading: "正在读取…",
1247
933
  failureUnauthorized: "需要登录:重新打开这个页面完成登录后再试。",
1248
934
  failureForbidden: "平台拒绝了这个请求(身份不是 owner)。",
@@ -1274,10 +960,14 @@ window.__ModuleLoader__.load({
1274
960
  invalidSelect: "Not one of the allowed values",
1275
961
  invalidAtLeast: "must not be below {n}",
1276
962
  approvalConfirmNamed: "About to approve: {n}",
963
+ approvalRawSummary: "Raw summary",
1277
964
  approvalConfirmYes: "Approve",
1278
965
  approvalConfirmNo: "Cancel",
1279
966
  resetTo: "Restores {value}",
1280
967
  saved: "Saved.",
968
+ saveReadbackFailed: "The write was accepted, but reading it back failed; refresh and confirm.",
969
+ readbackStale: "Showing the last successfully read values; the current re-read failed and may change after refresh.",
970
+ saveConflict: "The value was not accepted: settings changed in another page. Re-read before saving again.",
1281
971
  resetDone: "Default restored.",
1282
972
  refused: "The value was not accepted (the page keeps what you see). For the reason, ask in the conversation.",
1283
973
  resetRefused: "The reset did not take (this row still holds your value). For the reason, ask in the conversation.",
@@ -1345,6 +1035,8 @@ window.__ModuleLoader__.load({
1345
1035
  diagStatusUnknown: "unknown",
1346
1036
  diagRemedy: "Suggested: ",
1347
1037
  diagNoRows: "no runtime status to show",
1038
+ diagPurpose: "Purpose: check plugin mounting, platform services and data planes; nothing is changed.",
1039
+ diagFooter: "diagnostics schema v{version} · generated {at}",
1348
1040
  stateLoading: "reading…",
1349
1041
  failureUnauthorized: "Sign-in is required: reopen this page and sign in, then retry.",
1350
1042
  failureForbidden: "The platform refused this request (not the owner principal).",
@@ -1589,7 +1281,7 @@ window.__ModuleLoader__.load({
1589
1281
  * @returns the card.
1590
1282
  */
1591
1283
  function ParamCardView(props) {
1592
- const { t, fields, mutateOnce, clear, namespace, snapshot } = props;
1284
+ const { t, fields, mutateOnce, saveOnce, clear, namespace, snapshot } = props;
1593
1285
  const lang = props.lang?.() ?? "zh";
1594
1286
  const [open, setOpen] = (0, react.useState)(false);
1595
1287
  const [draft, setDraft] = (0, react.useState)({});
@@ -1616,8 +1308,12 @@ window.__ModuleLoader__.load({
1616
1308
  setPending(null);
1617
1309
  setBusy(false);
1618
1310
  setClearing(null);
1311
+ if (pending.readbackFailed === true) {
1312
+ setError(t("saveReadbackFailed"));
1313
+ return;
1314
+ }
1619
1315
  if (!pending.accepted) {
1620
- setError(t(pending.resets ? "resetRefused" : "refused"));
1316
+ setError(t(pending.resets ? "resetRefused" : pending.kind === "conflict" ? "saveConflict" : "refused"));
1621
1317
  return;
1622
1318
  }
1623
1319
  if (pending.accepted) if (pending.resets) {
@@ -1708,12 +1404,19 @@ window.__ModuleLoader__.load({
1708
1404
  setNote("");
1709
1405
  (async () => {
1710
1406
  try {
1407
+ const ops = dirty.map((field) => ({
1408
+ op: "set",
1409
+ path: [keyOf(field)],
1410
+ value: parseFor(field.control, textOf(field))
1411
+ }));
1412
+ const result = saveOnce === void 0 ? {
1413
+ written: await mutateOnce(ops),
1414
+ readback: "ok"
1415
+ } : await saveOnce(ops);
1711
1416
  setPending({
1712
- accepted: await mutateOnce(dirty.map((field) => ({
1713
- op: "set",
1714
- path: [keyOf(field)],
1715
- value: parseFor(field.control, textOf(field))
1716
- }))),
1417
+ accepted: result.written && result.readback === "ok",
1418
+ readbackFailed: result.written && result.readback === "failed",
1419
+ ...result.kind === void 0 ? {} : { kind: result.kind },
1717
1420
  resets: false
1718
1421
  });
1719
1422
  } catch (caught) {
@@ -1729,7 +1432,11 @@ window.__ModuleLoader__.load({
1729
1432
  onClick: () => {
1730
1433
  setOpen(!open);
1731
1434
  }
1732
- }, (0, react.createElement)("span", { className: "evolution-param-card-title" }, title), changed === 0 ? null : (0, react.createElement)("span", { className: "evolution-param-count" }, t("changed", { n: changed })), (0, react.createElement)("span", { className: "evolution-param-chevron" }, open ? "▲" : "▼")), open ? (0, react.createElement)("div", { className: "evolution-param-body" }, hintKey === void 0 ? null : (0, react.createElement)("p", { className: "evolution-param-note" }, t(hintKey)), disabled ? (0, react.createElement)("p", {
1435
+ }, (0, react.createElement)("span", { className: "evolution-param-card-title" }, title), changed === 0 ? null : (0, react.createElement)("span", { className: "evolution-param-count" }, t("changed", { n: changed })), (0, react.createElement)("span", { className: "evolution-param-chevron" }, open ? "▲" : "▼")), open ? (0, react.createElement)("div", { className: "evolution-param-body" }, hintKey === void 0 ? null : (0, react.createElement)("p", { className: "evolution-param-note" }, t(hintKey)), snapshot.stale === true ? (0, react.createElement)("p", {
1436
+ className: "evolution-param-note",
1437
+ role: "status",
1438
+ "data-role": "stale-note"
1439
+ }, t("readbackStale")) : null, disabled ? (0, react.createElement)("p", {
1733
1440
  className: "evolution-param-note",
1734
1441
  role: "status"
1735
1442
  }, t("readonly")) : null, fields.length === 0 ? (0, react.createElement)("p", {
@@ -1787,17 +1494,6 @@ window.__ModuleLoader__.load({
1787
1494
  * @param props - the injected card face.
1788
1495
  * @returns the card with its live row snapshot.
1789
1496
  */
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
1497
  const MISSING_SNAPSHOT = {
1802
1498
  status: "unavailable",
1803
1499
  value: void 0,
@@ -1812,7 +1508,7 @@ window.__ModuleLoader__.load({
1812
1508
  revive: () => {}
1813
1509
  };
1814
1510
  function PluginParamCard(props) {
1815
- const source = props.hooks?.paramSection ?? pluginSources.get(props.namespace) ?? MISSING_SOURCE;
1511
+ const source = props.hooks?.paramSection ?? MISSING_SOURCE;
1816
1512
  const snapshot = (0, react.useSyncExternalStore)(source.subscribe, source.getSnapshot, source.getSnapshot);
1817
1513
  return ParamCardView({
1818
1514
  namespace: props.namespace,
@@ -1820,16 +1516,114 @@ window.__ModuleLoader__.load({
1820
1516
  t: props.t,
1821
1517
  ...props.lang === void 0 ? {} : { lang: props.lang },
1822
1518
  mutateOnce: props.mutateOnce,
1519
+ ...props.saveOnce === void 0 ? {} : { saveOnce: props.saveOnce },
1823
1520
  clear: props.clear,
1824
1521
  snapshot
1825
1522
  });
1826
1523
  }
1827
1524
  //#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";
1525
+ //#region ../evolution-client-kit/src/panel.ts
1526
+ function interpolate(text, values) {
1527
+ if (values === void 0) return text;
1528
+ return text.replace(/\{(\w+)\}/g, (_match, key) => String(values[key] ?? ""));
1529
+ }
1530
+ function injectStyles$1(doc, css, tagId) {
1531
+ if (doc === void 0) return () => {};
1532
+ doc.querySelector("style[data-plugin-css=\"" + tagId + "\"]")?.remove();
1533
+ const tag = doc.createElement("style");
1534
+ tag.setAttribute("data-plugin-css", tagId);
1535
+ tag.textContent = css;
1536
+ doc.head.append(tag);
1537
+ return () => {
1538
+ tag.remove();
1539
+ };
1540
+ }
1541
+ /**
1542
+ * Register one sidebar row and its main body under one lifecycle.
1543
+ * @param ctx - the client context carrying the slots and locale seats.
1544
+ * @param spec - the feature-specific row/body descriptor.
1545
+ */
1546
+ function registerSidebarPanel(ctx, spec) {
1547
+ const seam = ctx;
1548
+ let registered = false;
1549
+ ctx.effect(() => {
1550
+ try {
1551
+ const dispose = seam.locale.register(spec.namespace, spec.dictionaries);
1552
+ registered = true;
1553
+ return dispose;
1554
+ } catch (error) {
1555
+ ctx.logger.warn(spec.namespace + ": the locale seat refused the dictionary (" + String(error) + "); the panel falls back to the bundle copy");
1556
+ return () => {};
1557
+ }
1558
+ }, spec.namespace + ": locale");
1559
+ ctx.effect(() => injectStyles$1(typeof document === "undefined" ? void 0 : document, spec.styles.css, spec.styles.tagId), spec.namespace + ": styles");
1560
+ const fallback = spec.dictionaries[spec.fallback ?? "zh"];
1561
+ const lookup = (key) => {
1562
+ const own = fallback[key] ?? key;
1563
+ if (!registered) return own;
1564
+ try {
1565
+ return seam.locale.bind(spec.namespace)(key);
1566
+ } catch {
1567
+ return own;
1568
+ }
1569
+ };
1570
+ const t = (key, values) => interpolate(lookup(key), values);
1571
+ const label = () => lookup("entry.label");
1572
+ seam.slots.inject("sidebar.panellist", function* () {
1573
+ yield seam.slots.register({
1574
+ name: "sidebar.panellist",
1575
+ id: spec.id,
1576
+ order: spec.order,
1577
+ label
1578
+ }, spec.icon);
1579
+ yield seam.slots.register({
1580
+ name: "main",
1581
+ key: spec.id,
1582
+ inject: () => spec.body(t),
1583
+ ...spec.children === void 0 ? {} : { children: spec.children },
1584
+ ...spec.store === void 0 ? {} : { store: spec.store }
1585
+ }, spec.component);
1586
+ });
1587
+ }
1588
+ //#endregion
1589
+ //#region ../evolution-client-kit/src/generated/routes.generated.ts
1590
+ /**
1591
+ * Every family host path, generated from evolution-core's HOST_ROUTES (the ONE declaration).
1592
+ * A browser half cannot import the host package at runtime (client bundles keep @deepseek-ai/*
1593
+ * external), so the table is mirrored here mechanically; a rename in core regenerates all three
1594
+ * client copies, and the generator's --check makes a stale copy red.
1595
+ */
1596
+ const ROUTES = {
1597
+ approval: {
1598
+ pending: "/api/dsh-evolution/approval/pending",
1599
+ preview: "/api/dsh-evolution/approval/preview",
1600
+ approve: "/api/dsh-evolution/approval/approve",
1601
+ reject: "/api/dsh-evolution/approval/reject",
1602
+ release: "/api/dsh-evolution/approval/release"
1603
+ },
1604
+ "skill-history": {
1605
+ skills: "/api/dsh-evolution/skill-history/skills",
1606
+ versions: "/api/dsh-evolution/skill-history/versions",
1607
+ diff: "/api/dsh-evolution/skill-history/versions/diff",
1608
+ body: "/api/dsh-evolution/skill-history/versions/body",
1609
+ undo: "/api/dsh-evolution/skill-history/undo",
1610
+ health: "/api/dsh-evolution/skill-history/health"
1611
+ },
1612
+ "skill-health": { rows: "/api/dsh-evolution/skill-health/rows" },
1613
+ merge: {
1614
+ view: "/api/dsh-evolution/merge/view",
1615
+ rows: "/api/dsh-evolution/merge/rows",
1616
+ run: "/api/dsh-evolution/merge/run",
1617
+ runs: "/api/dsh-evolution/merge/runs",
1618
+ body: "/api/dsh-evolution/merge/body",
1619
+ history: "/api/dsh-evolution/merge/history"
1620
+ },
1621
+ diagnostics: { view: "/api/dsh-evolution/diagnostics" },
1622
+ settings: {
1623
+ describe: "/api/dsh-evolution/settings/describe",
1624
+ mutate: "/api/dsh-evolution/settings/mutate"
1625
+ }
1626
+ };
1833
1627
  //#endregion
1834
1628
  //#region ../evolution-client-kit/src/settings-seat.ts
1835
1629
  const SETTINGS_DESCRIBE = ROUTES.settings.describe;
@@ -1893,34 +1687,51 @@ window.__ModuleLoader__.load({
1893
1687
  ...EMPTY,
1894
1688
  reason: "seat-missing"
1895
1689
  };
1690
+ let lastReady;
1691
+ let generation = 0;
1896
1692
  const listeners = /* @__PURE__ */ new Set();
1897
- let started = false;
1898
- let listening = false;
1693
+ let remoteOff;
1899
1694
  const notify = () => {
1900
1695
  for (const listener of listeners) listener();
1901
1696
  };
1902
- const refresh = async () => {
1903
- snapshot = await describe(namespace);
1697
+ const absorb = (next) => {
1698
+ if (next.status === "ready") {
1699
+ lastReady = next;
1700
+ snapshot = next;
1701
+ } else if (lastReady !== void 0) snapshot = {
1702
+ ...lastReady,
1703
+ stale: true,
1704
+ reason: next.reason ?? "readback-failed"
1705
+ };
1706
+ else snapshot = next;
1904
1707
  notify();
1905
1708
  };
1906
- const start = () => {
1907
- if (started) return;
1908
- started = true;
1909
- if (!listening) {
1910
- listening = true;
1911
- (ctx.get?.("remote"))?.$on?.("settings/document-updated", (changed) => {
1912
- if (changed === namespace) refresh();
1913
- });
1914
- }
1915
- refresh();
1709
+ const refresh = async () => {
1710
+ const ticket = ++generation;
1711
+ const next = await describe(namespace);
1712
+ if (ticket !== generation) return;
1713
+ absorb(next);
1714
+ };
1715
+ const remote = () => ctx.get?.("remote");
1716
+ const stopRemote = () => {
1717
+ remoteOff?.();
1718
+ remoteOff = void 0;
1719
+ };
1720
+ const startRemote = () => {
1721
+ if (remoteOff !== void 0) return;
1722
+ remoteOff = remote()?.$on?.("settings/document-updated", (changed) => {
1723
+ if (changed === namespace) refresh();
1724
+ });
1916
1725
  };
1917
1726
  const form = {
1918
1727
  getSnapshot: () => snapshot,
1919
1728
  subscribe: (listener) => {
1920
1729
  listeners.add(listener);
1921
- start();
1730
+ startRemote();
1731
+ refresh();
1922
1732
  return () => {
1923
1733
  listeners.delete(listener);
1734
+ if (listeners.size === 0) stopRemote();
1924
1735
  };
1925
1736
  },
1926
1737
  set: async (field, value) => await form.mutate([{
@@ -1934,35 +1745,39 @@ window.__ModuleLoader__.load({
1934
1745
  }]),
1935
1746
  mutate: async (ops, expectedRevision) => {
1936
1747
  const outcome = await write(namespace, ops, expectedRevision ?? snapshot.revision);
1937
- if (outcome.snapshot !== void 0) {
1938
- snapshot = outcome.snapshot;
1939
- notify();
1940
- } else await refresh();
1748
+ if (outcome.snapshot !== void 0) absorb(outcome.snapshot);
1749
+ else await refresh();
1941
1750
  return outcome.ok;
1751
+ },
1752
+ refresh,
1753
+ dispose: () => {
1754
+ stopRemote();
1755
+ listeners.clear();
1942
1756
  }
1943
1757
  };
1944
1758
  return form;
1945
1759
  }
1946
1760
  /**
1947
- * Build one cached form per family namespace over the family settings routes.
1948
- * @param ctx - the client plugin context (remote subscription is optional).
1949
- * @returns the seat; each namespace is served by /api/dsh-evolution/settings/*.
1761
+ * Build one seat per client registration; every form's remote listener is released by
1762
+ * the caller's effect. No module-level WeakMap: a replaced registration must not share
1763
+ * a dead seat with the previous one.
1950
1764
  */
1951
- const seats = /* @__PURE__ */ new WeakMap();
1952
1765
  function createSettingsSeat(ctx) {
1953
- const cached = seats.get(ctx);
1954
- if (cached !== void 0) return cached;
1955
1766
  const forms = /* @__PURE__ */ new Map();
1956
- const seat = { get(namespace) {
1957
- let form = forms.get(namespace);
1958
- if (form === void 0) {
1959
- form = createForm(namespace, ctx);
1960
- forms.set(namespace, form);
1767
+ return {
1768
+ get(namespace) {
1769
+ let form = forms.get(namespace);
1770
+ if (form === void 0) {
1771
+ form = createForm(namespace, ctx);
1772
+ forms.set(namespace, form);
1773
+ }
1774
+ return form;
1775
+ },
1776
+ dispose: () => {
1777
+ for (const form of forms.values()) form.dispose?.();
1778
+ forms.clear();
1961
1779
  }
1962
- return form;
1963
- } };
1964
- seats.set(ctx, seat);
1965
- return seat;
1780
+ };
1966
1781
  }
1967
1782
  //#endregion
1968
1783
  //#region ../evolution-client-kit/src/action.ts
@@ -1972,6 +1787,53 @@ window.__ModuleLoader__.load({
1972
1787
  * @module @lmzhen/dsh-evolution-client-kit
1973
1788
  */
1974
1789
  //#endregion
1790
+ //#region ../evolution-client-kit/src/generated/fallbacks.generated.ts
1791
+ /**
1792
+ * The command-face fallback for every Host surface, generated from evolution-commands'
1793
+ * COMMAND_FALLBACKS (the ONE home). When the platform's connection carrier is absent (E-325) the
1794
+ * panels render this hint instead of a bare HTTP failure — same use case, command face.
1795
+ */
1796
+ const FALLBACKS = {
1797
+ approval: { command: "/evolution pending … /evolution approve <id> … /evolution reject <id> … /evolution release <id>" },
1798
+ "skill-history": { command: "/evolution skill undo <name> [--to v<N>](查看:/evolution skill history <name>)" },
1799
+ "skill-health": { command: "/evolution skills refresh(健康行:/evolution doctor)" },
1800
+ merge: { command: "/evolution merge status / pending / approve <id> / report" },
1801
+ diagnostics: {
1802
+ command: "/evolution doctor",
1803
+ platformCli: "dsh --json /evolution doctor"
1804
+ },
1805
+ settings: { command: "/evolution policy set <id> <value>(查看:/evolution policy)" }
1806
+ };
1807
+ //#endregion
1808
+ //#region ../evolution-client-kit/src/generated/states.generated.ts
1809
+ /** A neutral/ warn/ fail block; a fail block is role=alert so assistive tech announces it. */
1810
+ function Block(props) {
1811
+ const tone = props.tone ?? "neutral";
1812
+ return (0, react.createElement)("div", {
1813
+ "data-tone": tone,
1814
+ ...tone === "fail" ? { role: "alert" } : {}
1815
+ }, props.title === void 0 ? null : (0, react.createElement)("div", { "data-tone-title": tone }, props.title), props.children);
1816
+ }
1817
+ /** One classified failure as a reader-facing sentence. */
1818
+ function failureText(failure, t) {
1819
+ switch (failure.kind) {
1820
+ case "unauthenticated": return t("failureUnauthorized");
1821
+ case "forbidden": return t("failureForbidden");
1822
+ case "unmounted": return t("failureUnmounted");
1823
+ case "server": return failure.status === 0 ? t("failureServer") : t("failureServer") + " (HTTP " + String(failure.status) + ")";
1824
+ case "refused": return failure.message;
1825
+ }
1826
+ }
1827
+ /** The failed-state block: sentence + (when the surface exists but is unreachable) the command fallback. */
1828
+ function FailureBlock(props) {
1829
+ const hint = props.fallback === void 0 ? null : (0, react.createElement)("div", { "data-tone-hint": "true" }, props.t("fallbackHint", { command: props.fallback.command }));
1830
+ return Block({
1831
+ tone: "fail",
1832
+ title: failureText(props.failure, props.t),
1833
+ children: hint
1834
+ });
1835
+ }
1836
+ //#endregion
1975
1837
  //#region ../evolution-client-kit/src/tokens.ts
1976
1838
  /**
1977
1839
  * The scale, declared on one element so it never leaks into the host shell.
@@ -1981,29 +1843,8 @@ window.__ModuleLoader__.load({
1981
1843
  function tokenVars(scope) {
1982
1844
  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
1845
  }
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
- }
1846
+ /** Stylesheet for {@link PAGE_SURFACE_CLASS}; consumers append it to their own CSS. */
1847
+ const PAGE_SURFACE_CSS = ".evolution-page-surface{min-height:100%;background:var(--dsw-alias-bg-base);color:var(--dsw-alias-label-primary)}";
2007
1848
  //#endregion
2008
1849
  //#region src/panels/settings/source.ts
2009
1850
  /** What a card shows when this UI found no settings seat at all. */
@@ -2038,7 +1879,8 @@ window.__ModuleLoader__.load({
2038
1879
  user: snapshot.user,
2039
1880
  writable: snapshot.writable,
2040
1881
  base: isRecord(snapshot.base) ? snapshot.base : void 0,
2041
- ...snapshot.status === "unavailable" ? { reason: snapshot.reason ?? "not-served" } : {}
1882
+ ...snapshot.status === "unavailable" ? { reason: snapshot.reason ?? "not-served" } : {},
1883
+ ...snapshot.stale === true ? { stale: true } : {}
2042
1884
  };
2043
1885
  }
2044
1886
  /**
@@ -2141,12 +1983,50 @@ window.__ModuleLoader__.load({
2141
1983
  */
2142
1984
  function writeFaceFor(probe, namespace) {
2143
1985
  const form = () => probe()?.get(namespace);
1986
+ const saveOnce = async (ops) => {
1987
+ const seat = form();
1988
+ if (seat === void 0) return {
1989
+ written: false,
1990
+ readback: "failed",
1991
+ kind: "transport"
1992
+ };
1993
+ let written = false;
1994
+ try {
1995
+ written = await seat.mutate(ops);
1996
+ } catch (failure) {
1997
+ return {
1998
+ written: false,
1999
+ readback: "failed",
2000
+ kind: "transport",
2001
+ failure
2002
+ };
2003
+ }
2004
+ if (!written) return {
2005
+ written: false,
2006
+ readback: "failed",
2007
+ kind: "conflict"
2008
+ };
2009
+ try {
2010
+ await seat.refresh?.();
2011
+ } catch {}
2012
+ const snapshot = seat.getSnapshot();
2013
+ if (snapshot.status !== "ready") return {
2014
+ written: true,
2015
+ readback: "failed",
2016
+ kind: "transport"
2017
+ };
2018
+ return {
2019
+ written: true,
2020
+ readback: snapshot.stale === true ? "failed" : "ok",
2021
+ ...snapshot.stale === true ? { kind: "transport" } : {}
2022
+ };
2023
+ };
2144
2024
  return {
2145
2025
  mutateOnce: async (ops) => {
2146
- const seat = form();
2147
- if (seat === void 0) return false;
2148
- return await seat.mutate(ops);
2026
+ if (form() === void 0) return false;
2027
+ return (await saveOnce(ops)).written;
2149
2028
  },
2029
+ saveOnce,
2150
2030
  clear: async (field) => {
2151
2031
  const seat = form();
2152
2032
  if (seat === void 0) return false;
@@ -2283,7 +2163,7 @@ window.__ModuleLoader__.load({
2283
2163
  * @param host - the document to inject into; defaults to the global one.
2284
2164
  * @returns the disposal; a no-op when there is no document or the tag was already present.
2285
2165
  */
2286
- function injectStyles$1(host) {
2166
+ function injectStyles(host) {
2287
2167
  const doc = host ?? globalThis.document;
2288
2168
  if (doc === void 0) return () => {};
2289
2169
  doc.querySelector("style[data-plugin-css=\"@lmzhen/dsh-evolution-ui/settings.css\"]")?.remove?.();
@@ -2305,7 +2185,7 @@ window.__ModuleLoader__.load({
2305
2185
  * @param ctx - client cordis context.
2306
2186
  */
2307
2187
  function apply$2(ctx) {
2308
- ctx.effect(() => injectStyles$1(), "evolution-settings-ui: styles");
2188
+ ctx.effect(() => injectStyles(), "evolution-settings-ui: styles");
2309
2189
  const seam = ctx;
2310
2190
  let registered = false;
2311
2191
  ctx.effect(() => {
@@ -2348,54 +2228,132 @@ window.__ModuleLoader__.load({
2348
2228
  ctx.inject?.(["remote"], () => {
2349
2229
  sources.revive();
2350
2230
  });
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()) {
2231
+ const cards = CLIENT_PARAM_SECTIONS.map((section) => {
2365
2232
  const source = sources.sourceFor(section.seatKey);
2366
- registerPluginParamSource(section.namespace, source);
2367
- const { mutateOnce, clear } = writeFaceFor(probe, section.namespace);
2368
- const face = () => ({
2233
+ const { mutateOnce, saveOnce, clear } = writeFaceFor(probe, section.namespace);
2234
+ return {
2369
2235
  namespace: section.namespace,
2370
2236
  fields: section.fields,
2371
2237
  t,
2372
2238
  lang: language,
2373
2239
  mutateOnce,
2240
+ saveOnce,
2374
2241
  clear,
2375
2242
  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
- });
2243
+ };
2244
+ });
2245
+ seam.slots.inject("settings.plugins.tab", function* () {
2246
+ yield seam.slots.register({
2247
+ name: "settings.plugins.tab",
2248
+ id: SECTION_ID,
2249
+ order: 25,
2250
+ label: () => t("title"),
2251
+ locale: NS$1,
2252
+ inject: () => ({ cards })
2253
+ }, EvolutionSettingsTab);
2254
+ });
2255
+ }
2256
+ /** One family tab: every parameter card lives inside the platform's Plugins page. */
2257
+ function EvolutionSettingsTab(props) {
2258
+ return (0, react.createElement)("div", { className: "evolution-settings-tab" }, ...props.cards.map((face, index) => (0, react.createElement)(PluginParamCard, {
2259
+ key: face.namespace + ":" + String(index),
2260
+ ...face
2261
+ })));
2262
+ }
2263
+ //#endregion
2264
+ //#region ../evolution-client-kit/src/generated/route-client.generated.ts
2265
+ /** Classify a non-2xx status: 401/403/404 keep their own kind, everything else is a server answer. */
2266
+ function classify(status) {
2267
+ if (status === 401) return {
2268
+ kind: "unauthenticated",
2269
+ status: 401
2270
+ };
2271
+ if (status === 403) return {
2272
+ kind: "forbidden",
2273
+ status: 403
2274
+ };
2275
+ if (status === 404) return {
2276
+ kind: "unmounted",
2277
+ status: 404
2278
+ };
2279
+ return {
2280
+ kind: "server",
2281
+ status
2282
+ };
2283
+ }
2284
+ /**
2285
+ * One request, no retries and no body parsing for the platform's admission answers.
2286
+ * @param doFetch - the fetch in use (the browser's, or a spec's stub).
2287
+ * @param path - the exact route path from the generated ROUTES table.
2288
+ * @param init - standard RequestInit.
2289
+ * @returns the data, or the classified failure (network/parse failures read as 'server').
2290
+ */
2291
+ async function requestJson(doFetch, path, init) {
2292
+ let response;
2293
+ try {
2294
+ response = await doFetch(path, init);
2295
+ } catch (error) {
2296
+ return {
2297
+ ok: false,
2298
+ failure: {
2299
+ kind: "server",
2300
+ status: 0,
2301
+ detail: error instanceof Error ? error.message : String(error)
2302
+ }
2303
+ };
2304
+ }
2305
+ if (!response.ok) return {
2306
+ ok: false,
2307
+ failure: classify(response.status)
2308
+ };
2309
+ let body;
2310
+ try {
2311
+ body = await response.json();
2312
+ } catch {
2313
+ return {
2314
+ ok: false,
2315
+ failure: {
2316
+ kind: "server",
2317
+ status: response.status
2318
+ }
2319
+ };
2320
+ }
2321
+ if (typeof body === "object" && body !== null) {
2322
+ const record = body;
2323
+ if (record.ok === true) return {
2324
+ ok: true,
2325
+ data: record.data
2326
+ };
2327
+ const nested = record.error;
2328
+ const errorRecord = typeof nested === "object" && nested !== null ? nested : void 0;
2329
+ const code = typeof record.code === "string" && record.code !== "" ? record.code : typeof errorRecord?.code === "string" ? errorRecord.code : void 0;
2330
+ const message = typeof record.message === "string" && record.message !== "" ? record.message : typeof errorRecord?.message === "string" ? errorRecord.message : void 0;
2331
+ if (record.ok === false && code !== void 0 && message !== void 0 && message !== "") return {
2332
+ ok: false,
2333
+ failure: {
2334
+ kind: "refused",
2335
+ code,
2336
+ message
2337
+ }
2338
+ };
2339
+ }
2340
+ return {
2341
+ ok: false,
2342
+ failure: {
2343
+ kind: "server",
2344
+ status: response.status
2345
+ }
2346
+ };
2347
+ }
2348
+ /** The one-sentence rendering legacy callers attach to a classified failure. */
2349
+ function failureMessage(failure) {
2350
+ switch (failure.kind) {
2351
+ case "refused": return failure.message;
2352
+ case "server": return failure.status === 0 ? failure.detail === void 0 || failure.detail === "" ? "网络错误:无法连接宿主" : failure.detail : "HTTP " + String(failure.status);
2353
+ case "unauthenticated": return "HTTP 401";
2354
+ case "forbidden": return "HTTP 403";
2355
+ case "unmounted": return "HTTP 404";
2386
2356
  }
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
2357
  }
2400
2358
  //#endregion
2401
2359
  //#region src/panels/history/client/api.ts
@@ -2600,8 +2558,7 @@ window.__ModuleLoader__.load({
2600
2558
  * independent review flagged that this reader had no case at all (P2-11).
2601
2559
  */
2602
2560
  function confirmModeFrom(user) {
2603
- const value = typeof user === "object" && user !== null ? user.approvalConfirm : void 0;
2604
- return value === "ask" || value === "timeout" ? "ask" : "auto";
2561
+ return (typeof user === "object" && user !== null ? user.approvalConfirm : void 0) === "auto" ? "auto" : "ask";
2605
2562
  }
2606
2563
  /** Which word one age bucket gets; an unknown bucket falls back to the count form. */
2607
2564
  const AGE_KEYS = Object.freeze({
@@ -2638,7 +2595,7 @@ window.__ModuleLoader__.load({
2638
2595
  function PendingCard(props) {
2639
2596
  const { t, api } = props;
2640
2597
  const translate = (key, values) => t(key, values);
2641
- const confirmMode = props.confirmMode?.() ?? "auto";
2598
+ const confirmMode = props.confirmMode?.() ?? "ask";
2642
2599
  const [armed, setArmed] = (0, react.useState)(null);
2643
2600
  const [state, setState] = (0, react.useState)(INITIAL_PENDING_STATE);
2644
2601
  const [open, setOpen] = (0, react.useState)(true);
@@ -2664,6 +2621,8 @@ window.__ModuleLoader__.load({
2664
2621
  * read, because a half-list would read as a complete window.
2665
2622
  */
2666
2623
  const readTicket = (0, react.useRef)(0);
2624
+ const previewTicket = (0, react.useRef)(0);
2625
+ const actionTickets = (0, react.useRef)(/* @__PURE__ */ new Map());
2667
2626
  const load = async () => {
2668
2627
  apply(loading);
2669
2628
  const ticket = readTicket.current + 1;
@@ -2691,9 +2650,13 @@ window.__ModuleLoader__.load({
2691
2650
  };
2692
2651
  }, []);
2693
2652
  const decide = (id, action) => {
2653
+ if (latest.current.busy.includes(id)) return;
2654
+ const ticket = (actionTickets.current.get(id) ?? 0) + 1;
2655
+ actionTickets.current.set(id, ticket);
2694
2656
  apply((current) => beginAction(current, id));
2695
2657
  (async () => {
2696
2658
  const answer = action === "approve" ? await api.approve(id) : action === "reject" ? await api.reject(id) : await api.release(id);
2659
+ if (actionTickets.current.get(id) !== ticket) return;
2697
2660
  if (!answer.ok) {
2698
2661
  apply((current) => noticed(endAction(current, id), failureText(answer.failure, translate)));
2699
2662
  return;
@@ -2709,7 +2672,9 @@ window.__ModuleLoader__.load({
2709
2672
  };
2710
2673
  /** Ask the host what this record would store; the answer lands only if the row is still open. */
2711
2674
  const loadPreview = async (id) => {
2675
+ const ticket = ++previewTicket.current;
2712
2676
  const answer = await api.preview(id);
2677
+ if (ticket !== previewTicket.current || latest.current.open !== id) return;
2713
2678
  const view = answer.ok ? answer.data.available ? {
2714
2679
  kind: "ready",
2715
2680
  answer: answer.data
@@ -2763,7 +2728,13 @@ window.__ModuleLoader__.load({
2763
2728
  }, t("approvalDiffCut"))] : []
2764
2729
  ];
2765
2730
  const lines = [];
2731
+ const multiHunk = view.answer.hunks.length > 1 || view.answer.hunks.some((hunk) => hunk.path !== view.answer.path);
2766
2732
  for (const [hunkIndex, hunk] of view.answer.hunks.entries()) {
2733
+ if (multiHunk) lines.push((0, react.createElement)("div", {
2734
+ key: "hunk-" + String(hunkIndex),
2735
+ className: "evolution-param-hunk-path",
2736
+ "data-role": "hunk-path"
2737
+ }, hunk.path));
2767
2738
  for (const [index, line] of hunk.oldText.split("\n").entries()) {
2768
2739
  if (hunk.oldText === "" && line === "") continue;
2769
2740
  lines.push((0, react.createElement)("div", {
@@ -2821,7 +2792,7 @@ window.__ModuleLoader__.load({
2821
2792
  }, (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
2793
  className: "evolution-param-value",
2823
2794
  title: entry.summary
2824
- }, summaryLine(entry))), (0, react.createElement)("p", { className: "evolution-param-hint" }, age(entry)), armed === entry.id ? (0, react.createElement)("p", {
2795
+ }, 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
2796
  className: "evolution-param-note",
2826
2797
  role: "status"
2827
2798
  }, (0, react.createElement)("span", { className: "evolution-param-value" }, t("approvalConfirmNamed", { n: summaryLine(entry) })), (0, react.createElement)("button", {
@@ -2900,6 +2871,23 @@ window.__ModuleLoader__.load({
2900
2871
  }, state.refreshing ? t("diagRefreshing") : t("approvalRefresh"))), open ? (0, react.createElement)("div", { className: "evolution-param-body" }, body()) : null);
2901
2872
  }
2902
2873
  //#endregion
2874
+ //#region src/panels/settings/diagnostics-state.ts
2875
+ /**
2876
+ * The diagnostics card's data face (W7 S7): ONE request to `HOST_ROUTES.diagnostics.view` through the
2877
+ * GENERATED route client, and the four-state answer the card renders.
2878
+ *
2879
+ * The Host route is the SAME `buildDoctorView` the CLI prints (W6/W8), so this client owns no facts
2880
+ * and no computation — it only renders.
2881
+ * @module @lmzhen/dsh-evolution-ui/panels/settings/client/diagnostics-state
2882
+ */
2883
+ /**
2884
+ * Build the diagnostics face over one fetch implementation.
2885
+ * @param doFetch - the browser fetch, or a spec stub.
2886
+ */
2887
+ function createDiagnosticsApi(doFetch = fetch) {
2888
+ return { view: async () => await requestJson(doFetch, ROUTES.diagnostics.view, { headers: { accept: "application/json" } }) };
2889
+ }
2890
+ //#endregion
2903
2891
  //#region src/panels/history/client/messages.ts
2904
2892
  /**
2905
2893
  * Panel copy, in a zh/en dictionary registered through the platform’s locale seat.
@@ -2919,6 +2907,7 @@ window.__ModuleLoader__.load({
2919
2907
  "workbench.pending": "待批",
2920
2908
  "workbench.merge": "合并",
2921
2909
  "workbench.health": "健康",
2910
+ "workbench.diagnostics": "诊断",
2922
2911
  hint: "每次技能写入留下的内容版本。回退只改 SKILL.md 正文,标记、使用计数与策展状态不变。内容相同的版本会同时标「当前」。",
2923
2912
  search: "按名字或描述筛选",
2924
2913
  "search.none": "没有匹配的技能。",
@@ -3010,6 +2999,7 @@ window.__ModuleLoader__.load({
3010
2999
  "workbench.pending": "Pending",
3011
3000
  "workbench.merge": "Merge",
3012
3001
  "workbench.health": "Health",
3002
+ "workbench.diagnostics": "Diagnostics",
3013
3003
  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
3004
  search: "Filter by name or description",
3015
3005
  "search.none": "No skill matches.",
@@ -3784,16 +3774,18 @@ window.__ModuleLoader__.load({
3784
3774
  * @param name - the skill to open.
3785
3775
  * @param onFailure - what to do with a failed read.
3786
3776
  */
3787
- const showVersions = (name, onFailure = failed) => {
3777
+ const showVersions = (name, onFailure = failed, keepContent = false) => {
3788
3778
  const ticket = readTicket.current + 1;
3789
3779
  readTicket.current = ticket;
3790
3780
  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());
3781
+ if (!keepContent) {
3782
+ setPayload(void 0);
3783
+ setVersionsFailed(void 0);
3784
+ setPending(void 0);
3785
+ setExpanded(void 0);
3786
+ setDiffs(/* @__PURE__ */ new Map());
3787
+ setBodies(/* @__PURE__ */ new Map());
3788
+ }
3797
3789
  face.loadVersions(name).then((next) => {
3798
3790
  if (readTicket.current === ticket) {
3799
3791
  setPayload(next);
@@ -3816,7 +3808,7 @@ window.__ModuleLoader__.load({
3816
3808
  /** What the refresh control does: both reads again, and the sentence stays. */
3817
3809
  const refresh = () => {
3818
3810
  reloadSkills();
3819
- if (selected !== void 0) showVersions(selected);
3811
+ if (selected !== void 0) showVersions(selected, failed, true);
3820
3812
  };
3821
3813
  (0, react.useEffect)(() => {
3822
3814
  if (face.focusSkill === void 0 || face.focusSkill === "") return;
@@ -3981,6 +3973,137 @@ window.__ModuleLoader__.load({
3981
3973
  return next;
3982
3974
  }
3983
3975
  //#endregion
3976
+ //#region src/panels/settings/DiagnosticsCard.ts
3977
+ /**
3978
+ * The diagnostics card (W7 S7): the settings face of the SAME DiagnosticsView the CLI and the
3979
+ * `/api/dsh-evolution/diagnostics` route answer.
3980
+ *
3981
+ * Read-only by construction: it fetches once on mount and only refetches on an explicit Refresh; a
3982
+ * failed read renders the shared FailureBlock with the command-face fallback. The client owns no
3983
+ * key list and no status vocabulary: it renders the host view's frozen runtime rows and leads with
3984
+ * a plain-language summary. The view's documentation facts are a CLI/JSON concern and are
3985
+ * deliberately NOT rendered here (0.27.2 real-machine feedback: a settings page is not a spec
3986
+ * sheet; the runtime window is the whole reading).
3987
+ * @module @lmzhen/dsh-evolution-ui/panels/settings/client/DiagnosticsCard
3988
+ */
3989
+ /** Which word one status gets (the frozen W1 status set). */
3990
+ const STATUS_KEYS = Object.freeze({
3991
+ ok: "diagStatusOk",
3992
+ warn: "diagStatusWarn",
3993
+ fail: "diagStatusFail",
3994
+ unknown: "diagStatusUnknown"
3995
+ });
3996
+ /** The summary word for a badge status (the same frozen set; ok has no count). */
3997
+ const SUMMARY_KEYS = Object.freeze({
3998
+ ok: "diagSummaryOk",
3999
+ warn: "diagSummaryWarn",
4000
+ fail: "diagSummaryFail",
4001
+ unknown: "diagSummaryUnknown"
4002
+ });
4003
+ /** The one status the card leads with: fail > warn > unknown > ok. */
4004
+ function summaryOf(rows) {
4005
+ const count = (status) => rows.filter((row) => row.status === status).length;
4006
+ for (const status of [
4007
+ "fail",
4008
+ "warn",
4009
+ "unknown"
4010
+ ]) {
4011
+ const n = count(status);
4012
+ if (n > 0) return {
4013
+ status,
4014
+ count: n
4015
+ };
4016
+ }
4017
+ return {
4018
+ status: "ok",
4019
+ count: 0
4020
+ };
4021
+ }
4022
+ /**
4023
+ * Render the card.
4024
+ * @param props - the copy seat, the host call and the command fallback.
4025
+ * @returns the card.
4026
+ */
4027
+ function DiagnosticsCard(props) {
4028
+ const { t, api } = props;
4029
+ const translate = (key, values) => t(key, values);
4030
+ const [state, setState] = (0, react.useState)({ phase: "loading" });
4031
+ const [refreshing, setRefreshing] = (0, react.useState)(false);
4032
+ const [open, setOpen] = (0, react.useState)(false);
4033
+ const ticket = (0, react.useRef)(0);
4034
+ const read = (keepView) => {
4035
+ const current = ticket.current + 1;
4036
+ ticket.current = current;
4037
+ if (keepView) setRefreshing(true);
4038
+ else setState({ phase: "loading" });
4039
+ api.view().then((answer) => {
4040
+ if (ticket.current !== current) return;
4041
+ setState(answer.ok ? {
4042
+ phase: "ready",
4043
+ view: answer.data
4044
+ } : {
4045
+ phase: "failed",
4046
+ failure: answer.failure
4047
+ });
4048
+ }).finally(() => {
4049
+ if (ticket.current === current) setRefreshing(false);
4050
+ });
4051
+ };
4052
+ (0, react.useEffect)(() => {
4053
+ read(false);
4054
+ }, []);
4055
+ const refresh = () => {
4056
+ read(true);
4057
+ };
4058
+ const summary = state.phase === "ready" ? summaryOf(state.view.runtime) : void 0;
4059
+ const header = (0, react.createElement)("button", {
4060
+ type: "button",
4061
+ className: "evolution-param-head",
4062
+ "aria-expanded": open ? "true" : "false",
4063
+ onClick: () => {
4064
+ setOpen(!open);
4065
+ }
4066
+ }, (0, react.createElement)("span", { className: "evolution-param-card-title" }, t("diagTitle")), summary === void 0 ? null : (0, react.createElement)("span", {
4067
+ className: "evo-diag-badge",
4068
+ "data-status": summary.status
4069
+ }, t(SUMMARY_KEYS[summary.status], summary.count === 0 ? void 0 : { n: summary.count })), (0, react.createElement)("span", { className: "evolution-param-chevron" }, open ? "▲" : "▼"));
4070
+ 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", {
4071
+ key: "runtime-" + row.key,
4072
+ className: "evo-diag-item",
4073
+ "data-kind": "runtime",
4074
+ "data-status": row.status
4075
+ }, (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)))));
4076
+ const body = () => {
4077
+ if (state.phase === "loading") return (0, react.createElement)("p", { className: "evolution-param-loading" }, t("stateLoading"));
4078
+ if (state.phase === "failed") return FailureBlock({
4079
+ failure: state.failure,
4080
+ t: translate,
4081
+ fallback: props.fallback
4082
+ });
4083
+ const view = state.view;
4084
+ 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", {
4085
+ type: "button",
4086
+ className: "evolution-param-button",
4087
+ onClick: refresh,
4088
+ disabled: refreshing,
4089
+ "data-action": "diag-refresh"
4090
+ }, t(refreshing ? "diagRefreshing" : "diagRefresh"))), runtime(view), view.runtime.length === 0 ? (0, react.createElement)("p", { className: "evolution-param-note" }, t("diagNoRows")) : null, (0, react.createElement)("p", {
4091
+ className: "evolution-param-note",
4092
+ "data-role": "diag-purpose"
4093
+ }, t("diagPurpose")), (0, react.createElement)("p", {
4094
+ className: "evolution-param-hint",
4095
+ "data-role": "diag-footer"
4096
+ }, t("diagFooter", {
4097
+ version: String(view.schemaVersion),
4098
+ at: new Date(view.generatedAt).toLocaleString()
4099
+ })));
4100
+ };
4101
+ return (0, react.createElement)("section", {
4102
+ className: "evolution-param-card",
4103
+ "data-card": "diagnostics"
4104
+ }, header, open ? (0, react.createElement)("div", { className: "evolution-param-body" }, body()) : null);
4105
+ }
4106
+ //#endregion
3984
4107
  //#region src/panels/history/client/Workbench.ts
3985
4108
  /**
3986
4109
  * The family's one task page. G3 owns the history view; later groups add health
@@ -3989,11 +4112,6 @@ window.__ModuleLoader__.load({
3989
4112
  */
3990
4113
  /** Stable page id used by the sidebar row, the page slot key, and navigation params. */
3991
4114
  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
4115
  /**
3998
4116
  * Render the workbench page. The platform keeps it mounted; this component only
3999
4117
  * consumes a new explicit revision exactly once.
@@ -4001,41 +4119,41 @@ window.__ModuleLoader__.load({
4001
4119
  * @returns the page surface.
4002
4120
  */
4003
4121
  function WorkbenchPage(props) {
4004
- const [selectedSkill, setSelectedSkill] = (0, react.useState)(void 0);
4005
- const [view, setView] = (0, react.useState)("history");
4006
- const lastRevision = (0, react.useRef)(void 0);
4122
+ const state = (0, react.useSyncExternalStore)(props.store.subscribe, props.store.getState, props.store.getState);
4123
+ const view = state.view;
4124
+ const selectedSkill = state.selectedSkill;
4125
+ const focusRevision = state.focusRevision;
4126
+ const openHistory = (skill) => {
4127
+ props.store.openHistory(skill);
4128
+ };
4129
+ const hasMerge = props.hasView?.("merge") ?? true;
4130
+ const hasHealth = props.hasView?.("health") ?? true;
4007
4131
  (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
- }
4015
- setView("history");
4016
- const skill = skillOf(props.params);
4017
- if (skill !== void 0) setSelectedSkill(skill);
4018
- }, [props.params, props.revision]);
4132
+ if (view === "merge" && !hasMerge || view === "health" && !hasHealth) props.store.setView("history");
4133
+ }, [
4134
+ view,
4135
+ hasMerge,
4136
+ hasHealth,
4137
+ props.store
4138
+ ]);
4019
4139
  const viewOrder = [
4020
4140
  "history",
4021
4141
  "pending",
4022
4142
  "merge",
4023
- "health"
4024
- ];
4143
+ "health",
4144
+ "diagnostics"
4145
+ ].filter((value) => (value !== "merge" || hasMerge) && (value !== "health" || hasHealth));
4025
4146
  const onTabKey = (event, current) => {
4026
4147
  const delta = event.key === "ArrowRight" ? 1 : event.key === "ArrowLeft" ? -1 : 0;
4027
4148
  if (delta === 0 && event.key !== "Home" && event.key !== "End") return;
4028
4149
  event.preventDefault();
4029
4150
  const index = viewOrder.indexOf(current);
4030
4151
  const next = event.key === "Home" ? viewOrder[0] : event.key === "End" ? viewOrder[viewOrder.length - 1] : viewOrder[(index + delta + viewOrder.length) % viewOrder.length];
4031
- setView(next);
4152
+ props.store.setView(next);
4032
4153
  requestAnimationFrame(() => {
4033
4154
  document.getElementById("evo-workbench-tab-" + next)?.focus();
4034
4155
  });
4035
4156
  };
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
4157
  return (0, react.createElement)("section", {
4040
4158
  className: "evolution-page-surface evo-workbench",
4041
4159
  "data-page": view
@@ -4052,7 +4170,7 @@ window.__ModuleLoader__.load({
4052
4170
  "aria-controls": "evo-workbench-panel",
4053
4171
  "data-active": view === "history" ? "true" : void 0,
4054
4172
  onClick: () => {
4055
- setView("history");
4173
+ props.store.setView("history");
4056
4174
  },
4057
4175
  onKeyDown: (event) => {
4058
4176
  onTabKey(event, "history");
@@ -4066,7 +4184,7 @@ window.__ModuleLoader__.load({
4066
4184
  "aria-controls": "evo-workbench-panel",
4067
4185
  "data-active": view === "pending" ? "true" : void 0,
4068
4186
  onClick: () => {
4069
- setView("pending");
4187
+ props.store.setView("pending");
4070
4188
  },
4071
4189
  onKeyDown: (event) => {
4072
4190
  onTabKey(event, "pending");
@@ -4080,7 +4198,7 @@ window.__ModuleLoader__.load({
4080
4198
  "aria-controls": "evo-workbench-panel",
4081
4199
  "data-active": view === "merge" ? "true" : void 0,
4082
4200
  onClick: () => {
4083
- setView("merge");
4201
+ props.store.setView("merge");
4084
4202
  },
4085
4203
  onKeyDown: (event) => {
4086
4204
  onTabKey(event, "merge");
@@ -4094,12 +4212,26 @@ window.__ModuleLoader__.load({
4094
4212
  "aria-controls": "evo-workbench-panel",
4095
4213
  "data-active": view === "health" ? "true" : void 0,
4096
4214
  onClick: () => {
4097
- setView("health");
4215
+ props.store.setView("health");
4098
4216
  },
4099
4217
  onKeyDown: (event) => {
4100
4218
  onTabKey(event, "health");
4101
4219
  }
4102
- }, props.t("workbench.health")))), (0, react.createElement)("div", {
4220
+ }, props.t("workbench.health")), (0, react.createElement)("button", {
4221
+ id: "evo-workbench-tab-diagnostics",
4222
+ type: "button",
4223
+ role: "tab",
4224
+ tabIndex: view === "diagnostics" ? 0 : -1,
4225
+ "aria-selected": view === "diagnostics" ? "true" : "false",
4226
+ "aria-controls": "evo-workbench-panel",
4227
+ "data-active": view === "diagnostics" ? "true" : void 0,
4228
+ onClick: () => {
4229
+ props.store.setView("diagnostics");
4230
+ },
4231
+ onKeyDown: (event) => {
4232
+ onTabKey(event, "diagnostics");
4233
+ }
4234
+ }, props.t("workbench.diagnostics")))), (0, react.createElement)("div", {
4103
4235
  className: "evo-workbench-body",
4104
4236
  id: "evo-workbench-panel",
4105
4237
  role: "tabpanel"
@@ -4107,27 +4239,30 @@ window.__ModuleLoader__.load({
4107
4239
  ...props.history,
4108
4240
  focusSkill: selectedSkill,
4109
4241
  focusRevision
4110
- }) : view === "pending" ? (0, react.createElement)(PendingCard, props.pending) : null, (0, react.createElement)("div", {
4242
+ }) : view === "pending" ? (0, react.createElement)(PendingCard, props.pending) : view === "diagnostics" ? (0, react.createElement)(DiagnosticsCard, props.diagnostics) : null, (0, react.createElement)("div", {
4111
4243
  hidden: view !== "merge" && view !== "health",
4112
4244
  className: "evo-workbench-feature"
4113
- }, props.renderSlot?.("evolution.workbench.view", { activeView: view }) ?? null)));
4245
+ }, props.renderSlot?.("evolution.workbench.view", {
4246
+ activeView: view,
4247
+ openHistory
4248
+ }) ?? null)));
4114
4249
  }
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"));
4250
+ /** The sidebar icon; the platform renders the row label, tooltip and active state. */
4251
+ function WorkbenchIcon(props) {
4252
+ const size = props.size ?? 16;
4253
+ return (0, react.createElement)("svg", {
4254
+ width: size,
4255
+ height: size,
4256
+ viewBox: "0 0 16 16",
4257
+ "aria-hidden": "true",
4258
+ focusable: "false"
4259
+ }, (0, react.createElement)("path", {
4260
+ d: "M2 3.5h12M2 8h12M2 12.5h8",
4261
+ fill: "none",
4262
+ stroke: "currentColor",
4263
+ "stroke-width": props.active === true ? 1.7 : 1.4,
4264
+ "stroke-linecap": "round"
4265
+ }));
4131
4266
  }
4132
4267
  /** Workbench shell and sidebar row; colours come from the platform tokens. */
4133
4268
  const WORKBENCH_CSS = [
@@ -4140,62 +4275,87 @@ window.__ModuleLoader__.load({
4140
4275
  ".evo-workbench-tabs button:focus-visible{outline:var(--evo-focus-ring);outline-offset:var(--evo-focus-offset)}",
4141
4276
  ".evo-workbench-title{margin:0;font-size:var(--evo-type-lg);font-weight:600;line-height:var(--evo-leading-base)}",
4142
4277
  ".evo-workbench-body{display:flex;flex:1;min-height:0;min-width:0;overflow:hidden}",
4143
- ".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
- ".evo-workbench-nav:hover,.evo-workbench-nav[data-active=\"true\"]{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}",
4145
- ".evo-workbench-nav:focus-visible{outline:var(--evo-focus-ring);outline-offset:var(--evo-focus-offset)}",
4278
+ ".evo-workbench-body>.evolution-param-card{flex:1;min-width:0;overflow:auto}",
4146
4279
  "@media (max-width:720px){.evo-workbench-head{flex-wrap:wrap}.evo-workbench-tabs{flex-wrap:wrap}}"
4147
4280
  ].join("");
4148
4281
  //#endregion
4149
- //#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();
4282
+ //#region src/panels/history/client/WorkbenchStore.ts
4283
+ function createWorkbenchStore(initial = {
4284
+ view: "history",
4285
+ focusRevision: 0
4286
+ }) {
4287
+ let state = initial;
4288
+ const listeners = /* @__PURE__ */ new Set();
4289
+ const emit = (next) => {
4290
+ state = next;
4291
+ for (const listener of listeners) listener();
4292
+ };
4293
+ return {
4294
+ getState: () => state,
4295
+ subscribe(listener) {
4296
+ listeners.add(listener);
4297
+ return () => {
4298
+ listeners.delete(listener);
4299
+ };
4300
+ },
4301
+ setView(view) {
4302
+ if (state.view !== view) emit({
4303
+ ...state,
4304
+ view
4305
+ });
4306
+ },
4307
+ openHistory(skill) {
4308
+ emit({
4309
+ view: "history",
4310
+ ...skill === void 0 ? {} : { selectedSkill: skill },
4311
+ focusRevision: state.focusRevision + 1
4312
+ });
4313
+ },
4314
+ clearSelection() {
4315
+ const { selectedSkill: _selected, ...rest } = state;
4316
+ emit({
4317
+ ...rest,
4318
+ focusRevision: state.focusRevision + 1
4319
+ });
4320
+ }
4159
4321
  };
4160
4322
  }
4323
+ //#endregion
4324
+ //#region src/panels/history/client/index.ts
4161
4325
  /**
4162
- * Register the locale namespace, the workbench page, and its sidebar entry.
4326
+ * Register the locale namespace, the workbench main occupant and its sidebar row.
4163
4327
  * @param ctx - client cordis context.
4164
4328
  */
4165
4329
  function apply$1(ctx) {
4166
- const seam = ctx;
4167
4330
  const api = createSkillHistoryApi();
4168
- let registered = false;
4169
- ctx.effect(() => {
4331
+ const settingsSeat = createSettingsSeat(ctx);
4332
+ ctx.effect(() => () => {
4333
+ settingsSeat.dispose?.();
4334
+ }, "evolution-ui: settings seat");
4335
+ let approvalConfirmMode = "ask";
4336
+ const approvalForm = settingsSeat.get("evolution-approval");
4337
+ const stopApprovalWatch = approvalForm?.subscribe(() => {
4338
+ approvalConfirmMode = confirmModeFrom(approvalForm.getSnapshot().user);
4339
+ });
4340
+ if (stopApprovalWatch !== void 0) ctx.effect(() => stopApprovalWatch, "evolution-history: approval confirm mode");
4341
+ const confirmMode = () => approvalConfirmMode;
4342
+ const hasView = (view) => {
4170
4343
  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 () => {};
4344
+ return (ctx.slots?.entries?.("evolution.workbench.view") ?? []).some((entry) => entry.options?.id === (view === "merge" ? "merge" : "skill-health"));
4345
+ } catch {
4346
+ return false;
4180
4347
  }
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;
4348
+ };
4349
+ const pendingT = (key, values) => {
4350
+ let template;
4187
4351
  try {
4188
- return seam.locale.bind(NS)(key);
4352
+ template = ctx.locale.bind(NS$1)(key);
4189
4353
  } catch {
4190
- return own;
4354
+ template = message(key);
4191
4355
  }
4356
+ return values === void 0 ? template : fill(template, values);
4192
4357
  };
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 = () => ({
4358
+ const historyFace = (t) => ({
4199
4359
  t,
4200
4360
  format: (key, values) => t(key, values),
4201
4361
  loadSkills: api.skills,
@@ -4209,61 +4369,44 @@ window.__ModuleLoader__.load({
4209
4369
  footnotes: t("markdown.footnotes")
4210
4370
  }
4211
4371
  });
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
4372
  const pendingFace = () => ({
4237
4373
  t: pendingT,
4238
4374
  api: createApprovalApi((input, init) => fetch(input, init)),
4239
4375
  confirmMode,
4240
4376
  fallback: FALLBACKS.approval
4241
4377
  });
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);
4378
+ const diagnosticsFace = () => ({
4379
+ t: pendingT,
4380
+ api: createDiagnosticsApi((input, init) => fetch(input, init)),
4381
+ fallback: FALLBACKS.diagnostics
4251
4382
  });
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);
4383
+ const workbenchStore = createWorkbenchStore();
4384
+ registerSidebarPanel(ctx, {
4385
+ id: WORKBENCH_ID,
4386
+ order: 35,
4387
+ namespace: NS,
4388
+ dictionaries: {
4389
+ zh,
4390
+ en
4391
+ },
4392
+ styles: {
4393
+ css: CSS + WORKBENCH_CSS + PAGE_SURFACE_CSS,
4394
+ tagId: CSS_TAG_ID
4395
+ },
4396
+ icon: WorkbenchIcon,
4397
+ component: WorkbenchPage,
4398
+ body: (t) => ({
4399
+ t,
4400
+ store: workbenchStore,
4401
+ history: historyFace(t),
4402
+ pending: pendingFace(),
4403
+ diagnostics: diagnosticsFace(),
4404
+ hasView
4405
+ }),
4406
+ children: { "evolution.workbench.view": {
4407
+ kind: "list",
4408
+ scope: "root"
4409
+ } }
4267
4410
  });
4268
4411
  }
4269
4412
  //#endregion