@lmzhen/dsh-evolution-settings-ui 0.26.15 → 0.27.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
@@ -5,73 +5,138 @@ window.__ModuleLoader__.load({
5
5
  var exports = module.exports;
6
6
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
7
7
  let react = require("react");
8
- //#region src/client/approval-routes.ts
8
+ //#region src/client/routes.generated.ts
9
9
  /**
10
- * The approval paths this browser half calls.
11
- *
12
- * A browser half cannot import the host package's table at runtime (the family's client bundles treat
13
- * every `@deepseek-ai/*` specifier as external), so the literals are declared here and a spec compares
14
- * them against `APPROVAL_ROUTES` in `evolution-approval` — a rename on one side would otherwise surface
15
- * as a 404 in the GUI instead of in CI.
16
- * @module @lmzhen/dsh-evolution-settings-ui/client/approval-routes
10
+ * Every family host path, generated from evolution-core's HOST_ROUTES (the ONE declaration).
11
+ * A browser half cannot import the host package at runtime (client bundles keep @deepseek-ai/*
12
+ * external), so the table is mirrored here mechanically; a rename in core regenerates all three
13
+ * client copies, and the generator's --check makes a stale copy red.
17
14
  */
18
- /** One path per call the pending card makes. */
19
- const APPROVAL_CLIENT_ROUTES = {
20
- pending: "/api/dsh-evolution/approval/pending",
21
- approve: "/api/dsh-evolution/approval/approve",
22
- reject: "/api/dsh-evolution/approval/reject",
23
- preview: "/api/dsh-evolution/approval/preview"
15
+ const ROUTES = {
16
+ approval: {
17
+ pending: "/api/dsh-evolution/approval/pending",
18
+ preview: "/api/dsh-evolution/approval/preview",
19
+ approve: "/api/dsh-evolution/approval/approve",
20
+ reject: "/api/dsh-evolution/approval/reject"
21
+ },
22
+ "skill-history": {
23
+ skills: "/api/dsh-evolution/skill-history/skills",
24
+ versions: "/api/dsh-evolution/skill-history/versions",
25
+ diff: "/api/dsh-evolution/skill-history/versions/diff",
26
+ body: "/api/dsh-evolution/skill-history/versions/body",
27
+ undo: "/api/dsh-evolution/skill-history/undo",
28
+ health: "/api/dsh-evolution/skill-history/health"
29
+ },
30
+ "skill-health": { rows: "/api/dsh-evolution/skill-health/rows" },
31
+ diagnostics: { view: "/api/dsh-evolution/diagnostics" }
24
32
  };
25
33
  //#endregion
26
- //#region src/client/api.ts
34
+ //#region src/client/route-client.generated.ts
35
+ /** Classify a non-2xx status: 401/403/404 keep their own kind, everything else is a server answer. */
36
+ function classify(status) {
37
+ if (status === 401) return {
38
+ kind: "unauthenticated",
39
+ status: 401
40
+ };
41
+ if (status === 403) return {
42
+ kind: "forbidden",
43
+ status: 403
44
+ };
45
+ if (status === 404) return {
46
+ kind: "unmounted",
47
+ status: 404
48
+ };
49
+ return {
50
+ kind: "server",
51
+ status
52
+ };
53
+ }
27
54
  /**
28
- * The approval routes this bundle calls, and the ONE place a request is made.
29
- *
30
- * Structural types on purpose: the browser half cannot import the host package at runtime, so the row
31
- * shape is declared by what the route hands over. Every answer — including a refusal — is parsed the
32
- * same way, because a refusal is a business answer the card shows as a sentence, not a transport error.
33
- * @module @lmzhen/dsh-evolution-settings-ui/client/api
55
+ * One request, no retries and no body parsing for the platform's admission answers.
56
+ * @param doFetch - the fetch in use (the browser's, or a spec's stub).
57
+ * @param path - the exact route path from the generated ROUTES table.
58
+ * @param init - standard RequestInit.
59
+ * @returns the data, or the classified failure (network/parse failures read as 'server').
34
60
  */
35
- /** Read one JSON answer, mapping every failure onto the same refusal shape. */
36
- async function read(doFetch, path, init) {
61
+ async function requestJson(doFetch, path, init) {
62
+ let response;
37
63
  try {
38
- const response = await doFetch(path, init);
39
- if (!response.ok) return {
64
+ response = await doFetch(path, init);
65
+ } catch (error) {
66
+ return {
40
67
  ok: false,
41
- message: "HTTP " + String(response.status)
42
- };
43
- const body = await response.json();
44
- if (body.ok === true) return {
45
- ok: true,
46
- data: body.data
68
+ failure: {
69
+ kind: "server",
70
+ status: 0,
71
+ detail: error instanceof Error ? error.message : String(error)
72
+ }
47
73
  };
74
+ }
75
+ if (!response.ok) return {
76
+ ok: false,
77
+ failure: classify(response.status)
78
+ };
79
+ let body;
80
+ try {
81
+ body = await response.json();
82
+ } catch {
48
83
  return {
49
84
  ok: false,
50
- message: typeof body.message === "string" && body.message !== "" ? body.message : "宿主拒绝了该请求"
85
+ failure: {
86
+ kind: "server",
87
+ status: response.status
88
+ }
51
89
  };
52
- } catch (error) {
53
- return {
90
+ }
91
+ if (typeof body === "object" && body !== null) {
92
+ const record = body;
93
+ if (record.ok === true) return {
94
+ ok: true,
95
+ data: record.data
96
+ };
97
+ if (record.ok === false && typeof record.message === "string" && record.message !== "") return {
54
98
  ok: false,
55
- message: error instanceof Error ? error.message : String(error)
99
+ failure: {
100
+ kind: "refused",
101
+ code: typeof record.code === "string" && record.code !== "" ? record.code : "refused",
102
+ message: record.message
103
+ }
56
104
  };
57
105
  }
106
+ return {
107
+ ok: false,
108
+ failure: {
109
+ kind: "server",
110
+ status: response.status
111
+ }
112
+ };
58
113
  }
114
+ //#endregion
115
+ //#region src/client/api.ts
116
+ /**
117
+ * The approval routes this bundle calls, and the ONE place a request is made.
118
+ *
119
+ * Structural types on purpose: the browser half cannot import the host package at runtime, so the row
120
+ * shape is declared by what the route hands over. Every answer — including a refusal — is parsed the
121
+ * same way, because a refusal is a business answer the card shows as a sentence, not a transport error.
122
+ * @module @lmzhen/dsh-evolution-settings-ui/client/api
123
+ */
59
124
  /**
60
125
  * Build the approval face over one fetch implementation.
61
126
  * @param doFetch - the fetch to use (the platform's global in the browser, a fake in a spec).
62
127
  * @returns the three calls the pending card makes.
63
128
  */
64
129
  function createApprovalApi(doFetch) {
65
- const post = (path, id) => read(doFetch, path, {
130
+ const post = async (path, id) => await requestJson(doFetch, path, {
66
131
  method: "POST",
67
132
  headers: { "content-type": "application/json" },
68
133
  body: JSON.stringify({ id })
69
134
  });
70
135
  return {
71
- pending: async (status) => await read(doFetch, status === void 0 ? APPROVAL_CLIENT_ROUTES.pending : APPROVAL_CLIENT_ROUTES.pending + "?status=" + status),
72
- approve: async (id) => await post(APPROVAL_CLIENT_ROUTES.approve, id),
73
- reject: async (id) => await post(APPROVAL_CLIENT_ROUTES.reject, id),
74
- preview: async (id) => await read(doFetch, APPROVAL_CLIENT_ROUTES.preview + "?id=" + encodeURIComponent(id))
136
+ pending: async (status) => await requestJson(doFetch, status === void 0 ? ROUTES.approval.pending : ROUTES.approval.pending + "?status=" + status),
137
+ approve: async (id) => await post(ROUTES.approval.approve, id),
138
+ reject: async (id) => await post(ROUTES.approval.reject, id),
139
+ preview: async (id) => await requestJson(doFetch, ROUTES.approval.preview + "?id=" + encodeURIComponent(id))
75
140
  };
76
141
  }
77
142
  //#endregion
@@ -607,6 +672,35 @@ window.__ModuleLoader__.load({
607
672
  }
608
673
  ];
609
674
  //#endregion
675
+ //#region src/client/states.generated.ts
676
+ /** A neutral/ warn/ fail block; a fail block is role=alert so assistive tech announces it. */
677
+ function Block(props) {
678
+ const tone = props.tone ?? "neutral";
679
+ return (0, react.createElement)("div", {
680
+ "data-tone": tone,
681
+ ...tone === "fail" ? { role: "alert" } : {}
682
+ }, props.title === void 0 ? null : (0, react.createElement)("div", { "data-tone-title": tone }, props.title), props.children);
683
+ }
684
+ /** One classified failure as a reader-facing sentence. */
685
+ function failureText(failure, t) {
686
+ switch (failure.kind) {
687
+ case "unauthenticated": return t("failureUnauthorized");
688
+ case "forbidden": return t("failureForbidden");
689
+ case "unmounted": return t("failureUnmounted");
690
+ case "server": return failure.status === 0 ? t("failureServer") : t("failureServer") + " (HTTP " + String(failure.status) + ")";
691
+ case "refused": return failure.message;
692
+ }
693
+ }
694
+ /** The failed-state block: sentence + (when the surface exists but is unreachable) the command fallback. */
695
+ function FailureBlock(props) {
696
+ const hint = props.fallback === void 0 ? null : (0, react.createElement)("div", { "data-tone-hint": "true" }, props.t("fallbackHint", { command: props.fallback.command }));
697
+ return Block({
698
+ tone: "fail",
699
+ title: failureText(props.failure, props.t),
700
+ children: hint
701
+ });
702
+ }
703
+ //#endregion
610
704
  //#region src/client/pending-state.ts
611
705
  /** The state a card mounts in: still loading, nothing in flight, nothing said yet. */
612
706
  const INITIAL_PENDING_STATE = {
@@ -662,13 +756,13 @@ window.__ModuleLoader__.load({
662
756
  }
663
757
  };
664
758
  }
665
- /** The list could not be read: the reason stays on screen next to a retry control. */
666
- function failed(state, message) {
759
+ /** The list could not be read: the classified failure stays on screen next to a retry control. */
760
+ function failed(state, failure) {
667
761
  return {
668
762
  ...state,
669
763
  view: {
670
764
  kind: "failed",
671
- message
765
+ failure
672
766
  }
673
767
  };
674
768
  }
@@ -741,6 +835,7 @@ window.__ModuleLoader__.load({
741
835
  */
742
836
  function PendingCard(props) {
743
837
  const { t, api } = props;
838
+ const translate = (key, values) => t(key, values);
744
839
  const confirmMode = props.confirmMode?.() ?? "auto";
745
840
  const [armed, setArmed] = (0, react.useState)(null);
746
841
  const [state, setState] = (0, react.useState)(INITIAL_PENDING_STATE);
@@ -760,15 +855,19 @@ window.__ModuleLoader__.load({
760
855
  * answers are ordered the way the CLI orders them (waiting first); either one failing fails the
761
856
  * read, because a half-list would read as a complete window.
762
857
  */
858
+ const readTicket = (0, react.useRef)(0);
763
859
  const load = async () => {
764
860
  apply(loading);
861
+ const ticket = readTicket.current + 1;
862
+ readTicket.current = ticket;
765
863
  const [waiting, running] = await Promise.all([api.pending(), api.pending("executing")]);
864
+ if (readTicket.current !== ticket) return;
766
865
  if (!waiting.ok) {
767
- apply((current) => failed(current, waiting.message));
866
+ apply((current) => failed(current, waiting.failure));
768
867
  return;
769
868
  }
770
869
  if (!running.ok) {
771
- apply((current) => failed(current, running.message));
870
+ apply((current) => failed(current, running.failure));
772
871
  return;
773
872
  }
774
873
  apply((current) => loaded(current, [...waiting.data, ...running.data]));
@@ -788,7 +887,7 @@ window.__ModuleLoader__.load({
788
887
  (async () => {
789
888
  const answer = action === "approve" ? await api.approve(id) : await api.reject(id);
790
889
  if (!answer.ok) {
791
- apply((current) => noticed(endAction(current, id), answer.message));
890
+ apply((current) => noticed(endAction(current, id), failureText(answer.failure, translate)));
792
891
  return;
793
892
  }
794
893
  apply((current) => noticed(endAction(current, id), null));
@@ -811,7 +910,7 @@ window.__ModuleLoader__.load({
811
910
  reason: answer.data.reason
812
911
  } : {
813
912
  kind: "failed",
814
- message: answer.message
913
+ failure: answer.failure
815
914
  };
816
915
  apply((current) => previewArrived(current, id, view));
817
916
  };
@@ -832,7 +931,7 @@ window.__ModuleLoader__.load({
832
931
  if (view.kind === "failed") return (0, react.createElement)("p", {
833
932
  className: "evolution-param-error",
834
933
  role: "status"
835
- }, view.message);
934
+ }, failureText(view.failure, translate));
836
935
  if (view.kind === "unavailable") return (0, react.createElement)("p", {
837
936
  className: "evolution-param-error",
838
937
  role: "status"
@@ -852,18 +951,18 @@ window.__ModuleLoader__.load({
852
951
  }, t("approvalDiffCut"))] : []
853
952
  ];
854
953
  const lines = [];
855
- for (const hunk of view.answer.hunks) {
954
+ for (const [hunkIndex, hunk] of view.answer.hunks.entries()) {
856
955
  for (const [index, line] of hunk.oldText.split("\n").entries()) {
857
956
  if (hunk.oldText === "" && line === "") continue;
858
957
  lines.push((0, react.createElement)("div", {
859
- key: "old-" + String(index),
958
+ key: "old-" + String(hunkIndex) + "-" + String(index),
860
959
  className: "evolution-param-line-del"
861
960
  }, "- " + line));
862
961
  }
863
962
  for (const [index, line] of hunk.newText.split("\n").entries()) {
864
963
  if (hunk.newText === "" && line === "") continue;
865
964
  lines.push((0, react.createElement)("div", {
866
- key: "new-" + String(index),
965
+ key: "new-" + String(hunkIndex) + "-" + String(index),
867
966
  className: "evolution-param-line-add"
868
967
  }, "+ " + line));
869
968
  }
@@ -933,10 +1032,11 @@ window.__ModuleLoader__.load({
933
1032
  className: "evolution-param-note",
934
1033
  role: "status"
935
1034
  }, t("approvalEmpty"));
936
- if (state.view.kind === "failed") return (0, react.createElement)("div", { className: "evolution-param-actions" }, (0, react.createElement)("p", {
937
- className: "evolution-param-error",
938
- role: "status"
939
- }, t("approvalFailed") + ": " + state.view.message), (0, react.createElement)("button", {
1035
+ if (state.view.kind === "failed") return (0, react.createElement)("div", { className: "evolution-param-actions" }, FailureBlock({
1036
+ failure: state.view.failure,
1037
+ t: translate,
1038
+ fallback: props.fallback
1039
+ }), (0, react.createElement)("button", {
940
1040
  className: "evolution-param-button",
941
1041
  type: "button",
942
1042
  onClick: () => {
@@ -967,6 +1067,120 @@ window.__ModuleLoader__.load({
967
1067
  }, t("approvalRefresh"))), body()) : null);
968
1068
  }
969
1069
  //#endregion
1070
+ //#region src/client/DiagnosticsCard.ts
1071
+ /**
1072
+ * The diagnostics card (W7 S7): the browser face of the SAME DiagnosticsView the CLI and the
1073
+ * `/api/dsh-evolution/diagnostics` route answer.
1074
+ *
1075
+ * Read-only by construction: it fetches once on mount and only refetches on an explicit Refresh; a
1076
+ * failed read renders the shared FailureBlock with the command-face fallback. Facts and runtime rows
1077
+ * are rendered in the view's own order — the client owns no key list and no status vocabulary.
1078
+ * @module @lmzhen/dsh-evolution-settings-ui/client/DiagnosticsCard
1079
+ */
1080
+ /** Which word one status gets (the frozen W1 status set). */
1081
+ const STATUS_KEYS = Object.freeze({
1082
+ ok: "diagStatusOk",
1083
+ warn: "diagStatusWarn",
1084
+ fail: "diagStatusFail",
1085
+ unknown: "diagStatusUnknown"
1086
+ });
1087
+ /**
1088
+ * Render the card.
1089
+ * @param props - the copy seat, the host call and the command fallback.
1090
+ * @returns the card.
1091
+ */
1092
+ function DiagnosticsCard(props) {
1093
+ const { t, api } = props;
1094
+ const translate = (key, values) => t(key, values);
1095
+ const [state, setState] = (0, react.useState)({ phase: "loading" });
1096
+ const ticket = (0, react.useRef)(0);
1097
+ const read = (keepView) => {
1098
+ const current = ticket.current + 1;
1099
+ ticket.current = current;
1100
+ if (!keepView) setState({ phase: "loading" });
1101
+ api.view().then((answer) => {
1102
+ if (ticket.current !== current) return;
1103
+ setState(answer.ok ? {
1104
+ phase: "ready",
1105
+ view: answer.data
1106
+ } : {
1107
+ phase: "failed",
1108
+ failure: answer.failure
1109
+ });
1110
+ });
1111
+ };
1112
+ (0, react.useEffect)(() => {
1113
+ read(false);
1114
+ }, []);
1115
+ const refresh = () => {
1116
+ read(true);
1117
+ };
1118
+ const header = (0, react.createElement)("div", { className: "evolution-param-card-head" }, (0, react.createElement)("span", { className: "evolution-param-card-title" }, t("diagTitle")), (0, react.createElement)("button", {
1119
+ type: "button",
1120
+ className: "evolution-param-button",
1121
+ onClick: refresh,
1122
+ "data-action": "diag-refresh"
1123
+ }, state.phase === "loading" ? t("diagRefreshing") : t("diagRefresh")));
1124
+ const body = () => {
1125
+ if (state.phase === "loading") return (0, react.createElement)("p", { className: "evolution-param-loading" }, t("stateLoading"));
1126
+ if (state.phase === "failed") return FailureBlock({
1127
+ failure: state.failure,
1128
+ t: translate,
1129
+ fallback: props.fallback
1130
+ });
1131
+ const view = state.view;
1132
+ const factRows = view.facts.map((fact) => (0, react.createElement)("div", {
1133
+ key: "fact-" + fact.key,
1134
+ className: "evo-diag-row",
1135
+ "data-kind": "fact"
1136
+ }, (0, react.createElement)("span", { className: "evo-diag-key" }, fact.label), (0, react.createElement)("span", { className: "evo-diag-value" }, fact.value), (0, react.createElement)("span", { className: "evo-diag-home" }, t("diagHomeCite", { home: fact.home }))));
1137
+ const runtimeRows = view.runtime.map((row) => (0, react.createElement)("div", {
1138
+ key: "runtime-" + row.key,
1139
+ className: "evo-diag-row",
1140
+ "data-kind": "runtime",
1141
+ "data-status": row.status
1142
+ }, (0, react.createElement)("span", { className: "evo-diag-key" }, 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)));
1143
+ return (0, react.createElement)("div", { className: "evo-diag-body" }, (0, react.createElement)("p", { className: "evolution-params-subtitle" }, t("diagSubtitle")), (0, react.createElement)("p", { className: "evo-diag-meta" }, t("diagSchema", { n: view.schemaVersion }) + " · " + t("diagGeneratedAt", { at: view.generatedAt })), (0, react.createElement)("h3", { className: "evo-diag-section" }, t("diagFactsTitle")), ...factRows, (0, react.createElement)("h3", { className: "evo-diag-section" }, t("diagRuntimeTitle")), ...runtimeRows);
1144
+ };
1145
+ return (0, react.createElement)("div", {
1146
+ className: "evolution-param-card",
1147
+ "data-card": "diagnostics"
1148
+ }, header, body());
1149
+ }
1150
+ //#endregion
1151
+ //#region src/client/diagnostics-state.ts
1152
+ /**
1153
+ * The diagnostics card's data face (W7 S7): ONE request to `HOST_ROUTES.diagnostics.view` through the
1154
+ * GENERATED route client, and the four-state answer the card renders.
1155
+ *
1156
+ * The Host route is the SAME `buildDoctorView` the CLI prints (W6/W8), so this client owns no facts
1157
+ * and no computation — it only renders.
1158
+ * @module @lmzhen/dsh-evolution-settings-ui/client/diagnostics-state
1159
+ */
1160
+ /**
1161
+ * Build the diagnostics face over one fetch implementation.
1162
+ * @param doFetch - the browser fetch, or a spec stub.
1163
+ */
1164
+ function createDiagnosticsApi(doFetch = fetch) {
1165
+ return { view: async () => await requestJson(doFetch, ROUTES.diagnostics.view, { headers: { accept: "application/json" } }) };
1166
+ }
1167
+ //#endregion
1168
+ //#region src/client/fallbacks.generated.ts
1169
+ /**
1170
+ * The command-face fallback for every Host surface, generated from evolution-commands'
1171
+ * COMMAND_FALLBACKS (the ONE home). When the platform's connection carrier is absent (E-325) the
1172
+ * panels render this hint instead of a bare HTTP failure — same use case, command face.
1173
+ */
1174
+ const FALLBACKS = {
1175
+ approval: { command: "/evolution pending … /evolution approve <id> … /evolution reject <id> … /evolution release <id>" },
1176
+ "skill-history": { command: "/evolution restore <name> [v](查看:/evolution skills history <name>)" },
1177
+ "skill-health": { command: "/evolution skills refresh(健康行:/evolution doctor)" },
1178
+ diagnostics: {
1179
+ command: "/evolution doctor",
1180
+ platformCli: "dsh --json /evolution doctor"
1181
+ }
1182
+ };
1183
+ //#endregion
970
1184
  //#region src/client/messages.ts
971
1185
  /**
972
1186
  * Copy for the evolution settings section and its parameter cards.
@@ -998,14 +1212,13 @@ window.__ModuleLoader__.load({
998
1212
  discard: "放弃修改",
999
1213
  saving: "保存中…",
1000
1214
  resetting: "恢复中…",
1001
- destHost: "保存去向:本机配置",
1215
+ writeTargetHost: "保存去向:宿主配置(对所有会话生效)",
1002
1216
  invalidNumber: "需要一个数字",
1003
1217
  invalidSelect: "不在允许的取值里",
1004
1218
  invalidAtLeast: "不能小于同组的 {n}",
1005
1219
  approvalConfirmNamed: "即将批准:{n}",
1006
1220
  approvalConfirmYes: "确认批准",
1007
1221
  approvalConfirmNo: "取消",
1008
- destMemory: "保存去向:仅本次会话",
1009
1222
  resetTo: "复位为 {value}",
1010
1223
  refused: "这个值没有被接受(界面上保持原样)。想看原因:在对话里让它说明。",
1011
1224
  resetRefused: "恢复默认值没有生效(这一行还是你的值)。想看原因:在对话里让它说明。",
@@ -1041,7 +1254,27 @@ window.__ModuleLoader__.load({
1041
1254
  approvalPreviewNone: "看不了预览:",
1042
1255
  approvalDiffAdded: "{n} 行新增",
1043
1256
  approvalDiffRemoved: "{n} 行删除",
1044
- approvalDiffCut: "改动区域已截断"
1257
+ approvalDiffCut: "改动区域已截断",
1258
+ diagTitle: "诊断",
1259
+ diagSubtitle: "安装形态、平台线与运行面的当前读数;只读,不改任何东西。",
1260
+ diagFactsTitle: "事实",
1261
+ diagRuntimeTitle: "运行时",
1262
+ diagSchema: "视图 schema v{n}",
1263
+ diagGeneratedAt: "生成于 {at}",
1264
+ diagRefresh: "刷新",
1265
+ diagRefreshing: "刷新中…",
1266
+ diagStatusOk: "正常",
1267
+ diagStatusWarn: "注意",
1268
+ diagStatusFail: "异常",
1269
+ diagStatusUnknown: "未知",
1270
+ diagRemedy: "下一步:",
1271
+ diagHomeCite: "出处:{home}",
1272
+ stateLoading: "正在读取…",
1273
+ failureUnauthorized: "需要登录:重新打开这个页面完成登录后再试。",
1274
+ failureForbidden: "平台拒绝了这个请求(身份不是 owner)。",
1275
+ failureUnmounted: "该功能没有挂载。",
1276
+ failureServer: "读取失败,可以手动重试。",
1277
+ fallbackHint: "可以改用命令:{command}"
1045
1278
  };
1046
1279
  /** English copy, key for key with {@link zh}. */
1047
1280
  const en = {
@@ -1061,14 +1294,13 @@ window.__ModuleLoader__.load({
1061
1294
  discard: "Discard",
1062
1295
  saving: "Saving…",
1063
1296
  resetting: "Resetting…",
1064
- destHost: "Saved to: this machine",
1297
+ writeTargetHost: "Saved to: host configuration (applies to every session)",
1065
1298
  invalidNumber: "Needs a number",
1066
1299
  invalidSelect: "Not one of the allowed values",
1067
1300
  invalidAtLeast: "must not be below {n}",
1068
1301
  approvalConfirmNamed: "About to approve: {n}",
1069
1302
  approvalConfirmYes: "Approve",
1070
1303
  approvalConfirmNo: "Cancel",
1071
- destMemory: "Saved to: this session only",
1072
1304
  resetTo: "Restores {value}",
1073
1305
  refused: "The value was not accepted (the page keeps what you see). For the reason, ask in the conversation.",
1074
1306
  resetRefused: "The reset did not take (this row still holds your value). For the reason, ask in the conversation.",
@@ -1104,7 +1336,27 @@ window.__ModuleLoader__.load({
1104
1336
  approvalPreviewNone: "no preview: ",
1105
1337
  approvalDiffAdded: "{n} lines added",
1106
1338
  approvalDiffRemoved: "{n} lines removed",
1107
- approvalDiffCut: "the changed region is truncated"
1339
+ approvalDiffCut: "the changed region is truncated",
1340
+ diagTitle: "Diagnostics",
1341
+ diagSubtitle: "Current readings for the install form, platform line and runtime surfaces; read-only.",
1342
+ diagFactsTitle: "Facts",
1343
+ diagRuntimeTitle: "Runtime",
1344
+ diagSchema: "view schema v{n}",
1345
+ diagGeneratedAt: "generated at {at}",
1346
+ diagRefresh: "Refresh",
1347
+ diagRefreshing: "refreshing…",
1348
+ diagStatusOk: "ok",
1349
+ diagStatusWarn: "warn",
1350
+ diagStatusFail: "fail",
1351
+ diagStatusUnknown: "unknown",
1352
+ diagRemedy: "Next: ",
1353
+ diagHomeCite: "source: {home}",
1354
+ stateLoading: "reading…",
1355
+ failureUnauthorized: "Sign-in is required: reopen this page and sign in, then retry.",
1356
+ failureForbidden: "The platform refused this request (not the owner principal).",
1357
+ failureUnmounted: "This feature is not mounted.",
1358
+ failureServer: "The read failed; you can retry manually.",
1359
+ fallbackHint: "Command fallback: {command}"
1108
1360
  };
1109
1361
  /**
1110
1362
  * Card titles per settings namespace. A namespace without an entry falls back to
@@ -1123,7 +1375,7 @@ window.__ModuleLoader__.load({
1123
1375
  /**
1124
1376
  * Resolve one message without the locale seat (tests and the fallback path).
1125
1377
  * @param key - the message key.
1126
- * @param language - which dictionary to read; defaults to Chinese.
1378
+ * @param language - which dictionary to read; defaults to English (the platform FALLBACK_LOCALE).
1127
1379
  * @returns the copy this bundle carries.
1128
1380
  */
1129
1381
  function message(key, language = "en") {
@@ -1171,6 +1423,23 @@ window.__ModuleLoader__.load({
1171
1423
  * typed from the registry (\`control\`), so booleans are switches, enums are
1172
1424
  * selects and numbers are numeric inputs with the unit shown beside the label.
1173
1425
  */
1426
+ /**
1427
+ * The NAMESPACE-RELATIVE key of one registry field.
1428
+ *
1429
+ * The E3 registry writes family ids in canonical form (`evolution-approval.pendingTtlMs`),
1430
+ * while the platform settings seat speaks a namespace's own field keys (`pendingTtlMs`) —
1431
+ * its snapshot user layer, its base and its op paths are all relative to the namespace. Sending
1432
+ * the canonical id as an op path makes the Host refuse the write
1433
+ * ("Config field evolution-approval.pendingTtlMs is not volatile", measured 2026-10-19), so the
1434
+ * card translates once, here, and every read/write goes through it.
1435
+ * @param namespace - the settings namespace (Loader row id) the card writes.
1436
+ * @param field - one generated field.
1437
+ * @returns the field's key inside that namespace.
1438
+ */
1439
+ function fieldKeyOf(namespace, field) {
1440
+ const prefix = namespace + ".";
1441
+ return field.id.startsWith(prefix) ? field.id.slice(prefix.length) : field.id;
1442
+ }
1174
1443
  /** One cell's text: absent reads as an empty control, containers as JSON. */
1175
1444
  function format(value) {
1176
1445
  if (value === void 0) return "";
@@ -1319,35 +1588,6 @@ window.__ModuleLoader__.load({
1319
1588
  const [error, setError] = (0, react.useState)("");
1320
1589
  const [pending, setPending] = (0, react.useState)(null);
1321
1590
  const [clearing, setClearing] = (0, react.useState)(null);
1322
- if (snapshot.status === "loading") return (0, react.createElement)("p", {
1323
- className: "evolution-param-note",
1324
- role: "status"
1325
- }, t("loading"));
1326
- if (snapshot.status === "unavailable") return (0, react.createElement)("p", {
1327
- className: "evolution-param-note",
1328
- role: "status"
1329
- }, t(snapshot.reason === "seat-missing" ? "seatMissing" : snapshot.reason === "projection-failed" ? "projectionFailed" : "unavailable"));
1330
- const user = isRecord(snapshot.user) ? snapshot.user : {};
1331
- const value = isRecord(snapshot.value) ? snapshot.value : {};
1332
- const disabled = !snapshot.writable;
1333
- const locked = disabled || busy || clearing !== null;
1334
- const titleKey = NAMESPACE_TITLES[namespace];
1335
- const title = titleKey === void 0 ? namespace : t(titleKey);
1336
- const changed = fields.filter((field) => Object.hasOwn(user, field.id)).length;
1337
- const textOf = (field) => draft[field.id] ?? format(value[field.id]);
1338
- const dirty = fields.filter((field) => draft[field.id] !== void 0 && draft[field.id] !== format(value[field.id]));
1339
- const peerTextOf = (field) => {
1340
- if (field.atLeast === "") return void 0;
1341
- const related = fields.find((candidate) => candidate.id === field.atLeast);
1342
- return related === void 0 ? void 0 : textOf(related);
1343
- };
1344
- const blocked = fields.some((field) => invalidReason(field, textOf(field), peerTextOf(field)) !== null);
1345
- const change = (id, next) => {
1346
- setDraft({
1347
- ...draft,
1348
- [id]: next
1349
- });
1350
- };
1351
1591
  /**
1352
1592
  * Settle a staged write against the Host's answer.
1353
1593
  *
@@ -1376,6 +1616,36 @@ window.__ModuleLoader__.load({
1376
1616
  snapshot.user,
1377
1617
  t
1378
1618
  ]);
1619
+ if (snapshot.status === "loading") return (0, react.createElement)("p", {
1620
+ className: "evolution-param-note",
1621
+ role: "status"
1622
+ }, t("loading"));
1623
+ if (snapshot.status === "unavailable") return (0, react.createElement)("p", {
1624
+ className: "evolution-param-note",
1625
+ role: "status"
1626
+ }, t(snapshot.reason === "seat-missing" ? "seatMissing" : snapshot.reason === "projection-failed" ? "projectionFailed" : "unavailable"));
1627
+ const user = isRecord(snapshot.user) ? snapshot.user : {};
1628
+ const value = isRecord(snapshot.value) ? snapshot.value : {};
1629
+ const disabled = !snapshot.writable;
1630
+ const locked = disabled || busy || clearing !== null;
1631
+ const titleKey = NAMESPACE_TITLES[namespace];
1632
+ const title = titleKey === void 0 ? namespace : t(titleKey);
1633
+ const keyOf = (field) => fieldKeyOf(namespace, field);
1634
+ const changed = fields.filter((field) => Object.hasOwn(user, keyOf(field))).length;
1635
+ const textOf = (field) => draft[field.id] ?? format(value[keyOf(field)]);
1636
+ const dirty = fields.filter((field) => draft[field.id] !== void 0 && draft[field.id] !== format(value[keyOf(field)]));
1637
+ const peerTextOf = (field) => {
1638
+ if (field.atLeast === "") return void 0;
1639
+ const related = fields.find((candidate) => candidate.id === field.atLeast);
1640
+ return related === void 0 ? void 0 : textOf(related);
1641
+ };
1642
+ const blocked = fields.some((field) => invalidReason(field, textOf(field), peerTextOf(field)) !== null);
1643
+ const change = (id, next) => {
1644
+ setDraft({
1645
+ ...draft,
1646
+ [id]: next
1647
+ });
1648
+ };
1379
1649
  /**
1380
1650
  * Restore one field's deployment default (T5-05/A60).
1381
1651
  *
@@ -1390,7 +1660,7 @@ window.__ModuleLoader__.load({
1390
1660
  (async () => {
1391
1661
  let accepted;
1392
1662
  try {
1393
- accepted = await clear(field);
1663
+ accepted = await clear(fieldKeyOf(namespace, { id: field }));
1394
1664
  } catch (caught) {
1395
1665
  setClearing(null);
1396
1666
  setError(caught instanceof Error && caught.message !== "" ? caught.message : t("resetRefused"));
@@ -1410,7 +1680,7 @@ window.__ModuleLoader__.load({
1410
1680
  setPending({
1411
1681
  accepted: await mutateOnce(dirty.map((field) => ({
1412
1682
  op: "set",
1413
- path: [field.id],
1683
+ path: [keyOf(field)],
1414
1684
  value: parseFor(field.control, textOf(field))
1415
1685
  }))),
1416
1686
  resets: false
@@ -1428,7 +1698,7 @@ window.__ModuleLoader__.load({
1428
1698
  onClick: () => {
1429
1699
  setOpen(!open);
1430
1700
  }
1431
- }, (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-dest" }, snapshot.mode === "memory" ? t("destMemory") : t("destHost")), (0, react.createElement)("span", { className: "evolution-param-chevron" }, open ? "▲" : "▼")), open ? (0, react.createElement)("div", { className: "evolution-param-body" }, disabled ? (0, react.createElement)("p", {
1701
+ }, (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-dest" }, t("writeTargetHost")), (0, react.createElement)("span", { className: "evolution-param-chevron" }, open ? "▲" : "▼")), open ? (0, react.createElement)("div", { className: "evolution-param-body" }, disabled ? (0, react.createElement)("p", {
1432
1702
  className: "evolution-param-note",
1433
1703
  role: "status"
1434
1704
  }, t("readonly")) : null, fields.length === 0 ? (0, react.createElement)("p", {
@@ -1441,10 +1711,10 @@ window.__ModuleLoader__.load({
1441
1711
  key: field.id,
1442
1712
  field,
1443
1713
  text: textOf(field),
1444
- overridden: Object.hasOwn(user, field.id),
1714
+ overridden: Object.hasOwn(user, keyOf(field)),
1445
1715
  disabled: locked,
1446
1716
  clearing: clearing === field.id,
1447
- resetTo: resetToOf(snapshot.base?.[field.id]),
1717
+ resetTo: resetToOf(snapshot.base?.[keyOf(field)]),
1448
1718
  invalid: invalidReason(field, textOf(field), peerTextOf(field)),
1449
1719
  t,
1450
1720
  onChange: change,
@@ -1494,7 +1764,6 @@ window.__ModuleLoader__.load({
1494
1764
  user: void 0,
1495
1765
  writable: false,
1496
1766
  base: void 0,
1497
- mode: "host",
1498
1767
  reason: "seat-missing"
1499
1768
  });
1500
1769
  /** What a card shows when the seat served the row but projecting it failed. A frozen constant
@@ -1506,7 +1775,6 @@ window.__ModuleLoader__.load({
1506
1775
  user: void 0,
1507
1776
  writable: false,
1508
1777
  base: void 0,
1509
- mode: "host",
1510
1778
  reason: "projection-failed"
1511
1779
  });
1512
1780
  /**
@@ -1521,7 +1789,6 @@ window.__ModuleLoader__.load({
1521
1789
  user: snapshot.user,
1522
1790
  writable: snapshot.writable,
1523
1791
  base: isRecord(snapshot.base) ? snapshot.base : void 0,
1524
- mode: snapshot.mode,
1525
1792
  ...snapshot.status === "unavailable" ? { reason: "not-served" } : {}
1526
1793
  };
1527
1794
  }
@@ -1629,14 +1896,12 @@ window.__ModuleLoader__.load({
1629
1896
  mutateOnce: async (ops) => {
1630
1897
  const seat = form();
1631
1898
  if (seat === void 0) return false;
1632
- await seat.mutate(ops);
1633
- return true;
1899
+ return await seat.mutate(ops);
1634
1900
  },
1635
1901
  clear: async (field) => {
1636
1902
  const seat = form();
1637
1903
  if (seat === void 0) return false;
1638
- await seat.unset(field);
1639
- return true;
1904
+ return await seat.unset(field);
1640
1905
  }
1641
1906
  };
1642
1907
  }
@@ -1685,9 +1950,9 @@ window.__ModuleLoader__.load({
1685
1950
  * @param fields - the row's fields.
1686
1951
  * @returns how many of them the user overrode.
1687
1952
  */
1688
- function overridden(user, fields) {
1953
+ function overridden(user, fields, namespace) {
1689
1954
  const layer = isRecord(user) ? user : {};
1690
- return fields.filter((field) => Object.hasOwn(layer, field.id)).length;
1955
+ return fields.filter((field) => Object.hasOwn(layer, fieldKeyOf(namespace, field))).length;
1691
1956
  }
1692
1957
  /**
1693
1958
  * Render one row's configuration entry.
@@ -1700,7 +1965,7 @@ window.__ModuleLoader__.load({
1700
1965
  className: "evolution-param-note",
1701
1966
  role: "status"
1702
1967
  }, t("seatMissing"));
1703
- const changed = overridden(form.state.user, fields);
1968
+ const changed = overridden(form.state.user, fields, namespace);
1704
1969
  if (view === "summary") return changed === 0 ? null : (0, react.createElement)("span", null, t("changed", { n: changed }));
1705
1970
  return ParamCardView({
1706
1971
  namespace,
@@ -1742,6 +2007,8 @@ window.__ModuleLoader__.load({
1742
2007
  * the section — one definition in the module both sides already share is what keeps that pair acyclic.
1743
2008
  */
1744
2009
  const PENDING_CARD_KEY = "approval-pending";
2010
+ /** The diagnostics card's slot key: read-only, no settings namespace (its data comes from HOST_ROUTES.diagnostics). */
2011
+ const DIAGNOSTICS_CARD_KEY = "diagnostics";
1745
2012
  //#endregion
1746
2013
  //#region src/client/SettingsSection.ts
1747
2014
  /**
@@ -1763,7 +2030,10 @@ window.__ModuleLoader__.load({
1763
2030
  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.24.0 · Web settings cards for the evolution family's parameter namespaces"), (0, react.createElement)("div", {
1764
2031
  key: PENDING_CARD_KEY,
1765
2032
  className: "evolution-params-group"
1766
- }, face.renderSlot(CARD_SLOT, {}, { entryKey: PENDING_CARD_KEY })), ...CLIENT_PARAM_SECTIONS.map((section_) => (0, react.createElement)("div", {
2033
+ }, face.renderSlot(CARD_SLOT, {}, { entryKey: PENDING_CARD_KEY })), (0, react.createElement)("div", {
2034
+ key: DIAGNOSTICS_CARD_KEY,
2035
+ className: "evolution-params-group"
2036
+ }, face.renderSlot(CARD_SLOT, {}, { entryKey: DIAGNOSTICS_CARD_KEY })), ...CLIENT_PARAM_SECTIONS.map((section_) => (0, react.createElement)("div", {
1767
2037
  key: section_.namespace,
1768
2038
  className: "evolution-params-group"
1769
2039
  }, face.renderSlot(CARD_SLOT, {}, { entryKey: section_.namespace }))));
@@ -1853,17 +2123,26 @@ window.__ModuleLoader__.load({
1853
2123
  ].join("\n");
1854
2124
  /**
1855
2125
  * Inject the stylesheet once per document.
2126
+ *
2127
+ * S5-08: it RETURNS the disposer. The tag check alone answered only half the question - "is it
2128
+ * there" - and left "who takes it away" to nobody, so a hot reload (unmount, remount) found the old
2129
+ * tag, skipped the injection, and kept serving the PREVIOUS bundle CSS. skill-history hands its
2130
+ * disposer back to the fiber; this is the same shape for the same concern.
1856
2131
  * @param host - the document to inject into; defaults to the global one.
2132
+ * @returns the disposal; a no-op when there is no document or the tag was already present.
1857
2133
  */
1858
2134
  function injectStyles(host) {
1859
2135
  const doc = host ?? globalThis.document;
1860
- if (doc === void 0) return;
1861
- if (doc.querySelector("style[data-plugin-css=\"@lmzhen/dsh-evolution-settings-ui/settings.css\"]") !== null) return;
2136
+ if (doc === void 0) return () => {};
2137
+ if (doc.querySelector("style[data-plugin-css=\"@lmzhen/dsh-evolution-settings-ui/settings.css\"]") !== null) return () => {};
1862
2138
  const tag = doc.createElement("style");
1863
2139
  tag.dataset.plugin = "@lmzhen/dsh-evolution-settings-ui";
1864
2140
  tag.dataset.pluginCss = CSS_TAG_ID;
1865
2141
  tag.textContent = CSS;
1866
2142
  doc.head.appendChild(tag);
2143
+ return () => {
2144
+ tag.remove();
2145
+ };
1867
2146
  }
1868
2147
  //#endregion
1869
2148
  //#region src/client/index.ts
@@ -1884,7 +2163,7 @@ window.__ModuleLoader__.load({
1884
2163
  * @param ctx - client cordis context.
1885
2164
  */
1886
2165
  function apply(ctx) {
1887
- injectStyles();
2166
+ ctx.effect(() => injectStyles(), "evolution-settings-ui: styles");
1888
2167
  const seam = ctx;
1889
2168
  let registered = false;
1890
2169
  ctx.effect(() => {
@@ -1959,9 +2238,20 @@ window.__ModuleLoader__.load({
1959
2238
  inject: () => ({
1960
2239
  t,
1961
2240
  api,
1962
- confirmMode
2241
+ confirmMode,
2242
+ fallback: FALLBACKS.approval
1963
2243
  })
1964
2244
  }, PendingCard);
2245
+ const diagApi = createDiagnosticsApi((input, init) => fetch(input, init));
2246
+ yield seam.slots.register({
2247
+ name: CARD_SLOT,
2248
+ key: DIAGNOSTICS_CARD_KEY,
2249
+ inject: () => ({
2250
+ t,
2251
+ api: diagApi,
2252
+ fallback: FALLBACKS.diagnostics
2253
+ })
2254
+ }, DiagnosticsCard);
1965
2255
  });
1966
2256
  seam.slots.inject(ROW_CONFIG_SLOT, function* () {
1967
2257
  for (const seat of CLIENT_ROW_SEATS) for (const rowId of seat.rows) {
@@ -0,0 +1,26 @@
1
+ /**
2
+ * The diagnostics card (W7 S7): the browser face of the SAME DiagnosticsView the CLI and the
3
+ * `/api/dsh-evolution/diagnostics` route answer.
4
+ *
5
+ * Read-only by construction: it fetches once on mount and only refetches on an explicit Refresh; a
6
+ * failed read renders the shared FailureBlock with the command-face fallback. Facts and runtime rows
7
+ * are rendered in the view's own order — the client owns no key list and no status vocabulary.
8
+ * @module @lmzhen/dsh-evolution-settings-ui/client/DiagnosticsCard
9
+ */
10
+ import { type ReactNode } from 'react';
11
+ import type { DiagnosticsApi } from './diagnostics-state.ts';
12
+ import type { FallbackHint } from './contracts.generated.ts';
13
+ import type { MessageKey } from './messages.ts';
14
+ /** Injected face: the copy seat, the one host call, and this surface's command fallback. */
15
+ export interface DiagnosticsCardFace {
16
+ t: (key: MessageKey, values?: Readonly<Record<string, string | number>>) => string;
17
+ api: DiagnosticsApi;
18
+ fallback?: FallbackHint | undefined;
19
+ }
20
+ /**
21
+ * Render the card.
22
+ * @param props - the copy seat, the host call and the command fallback.
23
+ * @returns the card.
24
+ */
25
+ export declare function DiagnosticsCard(props: DiagnosticsCardFace): ReactNode;
26
+ //# sourceMappingURL=DiagnosticsCard.d.ts.map
@@ -48,6 +48,20 @@ export type ParamCardProps = Omit<ParamCardFace, 'hooks'> & {
48
48
  };
49
49
  /** The op list a save hands to the seat — the seam's own shape, never a second vocabulary. */
50
50
  export type SettingsParamOp = SettingsPathOpLike;
51
+ /**
52
+ * The NAMESPACE-RELATIVE key of one registry field.
53
+ *
54
+ * The E3 registry writes family ids in canonical form (`evolution-approval.pendingTtlMs`),
55
+ * while the platform settings seat speaks a namespace's own field keys (`pendingTtlMs`) —
56
+ * its snapshot user layer, its base and its op paths are all relative to the namespace. Sending
57
+ * the canonical id as an op path makes the Host refuse the write
58
+ * ("Config field evolution-approval.pendingTtlMs is not volatile", measured 2026-10-19), so the
59
+ * card translates once, here, and every read/write goes through it.
60
+ * @param namespace - the settings namespace (Loader row id) the card writes.
61
+ * @param field - one generated field.
62
+ * @returns the field's key inside that namespace.
63
+ */
64
+ export declare function fieldKeyOf(namespace: string, field: ClientParamField): string;
51
65
  /**
52
66
  * U-4: whether one field's CURRENT text is a value the Host can take. The controls constrain their own
53
67
  * domains (a number input sanitizes, a select only offers listed values), so the reachable case is a
@@ -13,6 +13,7 @@
13
13
  * @module @lmzhen/dsh-evolution-settings-ui/client/PendingCard
14
14
  */
15
15
  import { type ReactNode } from 'react';
16
+ import type { FallbackHint } from './contracts.generated.ts';
16
17
  import type { ApprovalApi } from './api.ts';
17
18
  import type { MessageKey } from './messages.ts';
18
19
  /** Injected face: the copy seat and the host calls. No hook compartment — this card keeps its own state. */
@@ -24,6 +25,8 @@ export interface PendingCardFace {
24
25
  * captured once would pin the gate for the life of the fiber. Absent ⇒ the shipped default `auto`.
25
26
  */
26
27
  confirmMode?: () => 'auto' | 'ask' | 'timeout';
28
+ /** G5/W7 S4: the command-face fallback rendered with an unreachable/unauthenticated failure. */
29
+ fallback?: FallbackHint | undefined;
27
30
  }
28
31
  /**
29
32
  * U-6: the registry's gate word, read out of a section's user layer. ONE definition, exported so the
@@ -1,3 +1,4 @@
1
+ import { type RouteAnswer } from './route-client.generated.ts';
1
2
  /** One staged write, as the pending route hands it over (never the staged args). */
2
3
  export interface PendingRow {
3
4
  readonly id: string;
@@ -33,14 +34,8 @@ export type PreviewAnswer = {
33
34
  readonly available: false;
34
35
  readonly reason: string;
35
36
  };
36
- /** What one call answered: the data, or the reason it could not be carried out. */
37
- export type ApprovalAnswer<T> = {
38
- readonly ok: true;
39
- readonly data: T;
40
- } | {
41
- readonly ok: false;
42
- readonly message: string;
43
- };
37
+ /** What one call answered: the generated four-state RouteAnswer (no local failure vocabulary). */
38
+ export type ApprovalAnswer<T> = RouteAnswer<T>;
44
39
  /**
45
40
  * Which window the pending route is asked for. The host route has always taken `?status=` (the CLI
46
41
  * asks for both); this half asked for neither, so a record stuck in `executing` — an approve that
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Literal mirror of the frozen Host diagnostics contract (W1 §2.8 / core diagnostics.ts). The browser
3
+ * build keeps every @deepseek-ai/* specifier external, so the client CANNOT import the host type — the
4
+ * shape is mirrored mechanically here instead. The key unions are rendered from core diagnostics.ts by
5
+ * the generator, so a key change regenerates all three copies (a stale copy fails --check).
6
+ */
7
+ export type DiagnosticsSchemaVersion = 2;
8
+ export type DiagnosticsFactKey = 'platform-line' | 'family-version' | 'install-form' | 'preset-ids' | 'readonly-contract' | 'upgrade-order' | 'fallback-map';
9
+ export type DiagnosticsRuntimeKey = 'subagent-channel' | 'restricted-preset' | 'host-surface' | 'session-fold' | 'platform-services' | 'runs' | 'platform-pairing' | 'install-form-detected';
10
+ export type DiagnosticStatus = 'ok' | 'warn' | 'fail' | 'unknown';
11
+ export interface DocFactRow {
12
+ readonly key: DiagnosticsFactKey;
13
+ readonly label: string;
14
+ readonly value: string;
15
+ readonly home: string;
16
+ }
17
+ export interface DiagnosticRow {
18
+ readonly key: DiagnosticsRuntimeKey;
19
+ readonly label: string;
20
+ readonly status: DiagnosticStatus;
21
+ readonly detail?: string;
22
+ readonly remedy?: string;
23
+ }
24
+ export interface DiagnosticsView {
25
+ readonly schemaVersion: DiagnosticsSchemaVersion;
26
+ readonly generatedAt: string;
27
+ readonly facts: readonly DocFactRow[];
28
+ readonly runtime: readonly DiagnosticRow[];
29
+ }
30
+ /** The installation form the doctor reports (W8 install-form-detected row). */
31
+ export type InstallFormId = 'M1' | 'M2' | 'M3' | 'M4';
32
+ /** One surface's command-face path(s) — the client `fallbacks.generated.ts` table's row type. */
33
+ export interface FallbackHint {
34
+ readonly command: string;
35
+ readonly platformCli?: string;
36
+ }
37
+ //# sourceMappingURL=contracts.generated.d.ts.map
@@ -0,0 +1,28 @@
1
+ import { type RouteAnswer } from './route-client.generated.ts';
2
+ import type { DiagnosticsView } from './contracts.generated.ts';
3
+ /** What the card renders: the view, or the classified failure that kept it from arriving. */
4
+ export type DiagnosticsState = {
5
+ readonly phase: 'loading';
6
+ } | {
7
+ readonly phase: 'ready';
8
+ readonly view: DiagnosticsView;
9
+ } | {
10
+ readonly phase: 'failed';
11
+ readonly failure: import('./route-client.generated.ts').RouteFailure;
12
+ };
13
+ /** The one call the card makes. */
14
+ export interface DiagnosticsApi {
15
+ view(): Promise<RouteAnswer<DiagnosticsView>>;
16
+ }
17
+ /**
18
+ * Build the diagnostics face over one fetch implementation.
19
+ * @param doFetch - the browser fetch, or a spec stub.
20
+ */
21
+ export declare function createDiagnosticsApi(doFetch?: typeof fetch): DiagnosticsApi;
22
+ /**
23
+ * Read the view once and map it onto the card's four states (loading is the caller's initial value).
24
+ * @param doFetch - the fetch to use.
25
+ * @returns ready with the view, or failed with the classified failure.
26
+ */
27
+ export declare function loadDiagnostics(doFetch?: typeof fetch): Promise<DiagnosticsState>;
28
+ //# sourceMappingURL=diagnostics-state.d.ts.map
@@ -0,0 +1,8 @@
1
+ import type { FallbackHint } from './contracts.generated.ts';
2
+ /**
3
+ * The command-face fallback for every Host surface, generated from evolution-commands'
4
+ * COMMAND_FALLBACKS (the ONE home). When the platform's connection carrier is absent (E-325) the
5
+ * panels render this hint instead of a bare HTTP failure — same use case, command face.
6
+ */
7
+ export declare const FALLBACKS: Readonly<Record<'approval' | 'skill-history' | 'skill-health' | 'diagnostics', FallbackHint>>;
8
+ //# sourceMappingURL=fallbacks.generated.d.ts.map
@@ -28,14 +28,13 @@ export declare const zh: {
28
28
  readonly discard: "放弃修改";
29
29
  readonly saving: "保存中…";
30
30
  readonly resetting: "恢复中…";
31
- readonly destHost: "保存去向:本机配置";
31
+ readonly writeTargetHost: "保存去向:宿主配置(对所有会话生效)";
32
32
  readonly invalidNumber: "需要一个数字";
33
33
  readonly invalidSelect: "不在允许的取值里";
34
34
  readonly invalidAtLeast: "不能小于同组的 {n}";
35
35
  readonly approvalConfirmNamed: "即将批准:{n}";
36
36
  readonly approvalConfirmYes: "确认批准";
37
37
  readonly approvalConfirmNo: "取消";
38
- readonly destMemory: "保存去向:仅本次会话";
39
38
  readonly resetTo: "复位为 {value}";
40
39
  readonly refused: "这个值没有被接受(界面上保持原样)。想看原因:在对话里让它说明。";
41
40
  readonly resetRefused: "恢复默认值没有生效(这一行还是你的值)。想看原因:在对话里让它说明。";
@@ -72,6 +71,26 @@ export declare const zh: {
72
71
  readonly approvalDiffAdded: "{n} 行新增";
73
72
  readonly approvalDiffRemoved: "{n} 行删除";
74
73
  readonly approvalDiffCut: "改动区域已截断";
74
+ readonly diagTitle: "诊断";
75
+ readonly diagSubtitle: "安装形态、平台线与运行面的当前读数;只读,不改任何东西。";
76
+ readonly diagFactsTitle: "事实";
77
+ readonly diagRuntimeTitle: "运行时";
78
+ readonly diagSchema: "视图 schema v{n}";
79
+ readonly diagGeneratedAt: "生成于 {at}";
80
+ readonly diagRefresh: "刷新";
81
+ readonly diagRefreshing: "刷新中…";
82
+ readonly diagStatusOk: "正常";
83
+ readonly diagStatusWarn: "注意";
84
+ readonly diagStatusFail: "异常";
85
+ readonly diagStatusUnknown: "未知";
86
+ readonly diagRemedy: "下一步:";
87
+ readonly diagHomeCite: "出处:{home}";
88
+ readonly stateLoading: "正在读取…";
89
+ readonly failureUnauthorized: "需要登录:重新打开这个页面完成登录后再试。";
90
+ readonly failureForbidden: "平台拒绝了这个请求(身份不是 owner)。";
91
+ readonly failureUnmounted: "该功能没有挂载。";
92
+ readonly failureServer: "读取失败,可以手动重试。";
93
+ readonly fallbackHint: "可以改用命令:{command}";
75
94
  };
76
95
  /** English copy, key for key with {@link zh}. */
77
96
  export declare const en: Record<keyof typeof zh, string>;
@@ -86,7 +105,7 @@ export type MessageKey = keyof typeof zh;
86
105
  /**
87
106
  * Resolve one message without the locale seat (tests and the fallback path).
88
107
  * @param key - the message key.
89
- * @param language - which dictionary to read; defaults to Chinese.
108
+ * @param language - which dictionary to read; defaults to English (the platform FALLBACK_LOCALE).
90
109
  * @returns the copy this bundle carries.
91
110
  */
92
111
  export declare function message(key: MessageKey, language?: 'zh' | 'en'): string;
@@ -8,6 +8,7 @@
8
8
  * @module @lmzhen/dsh-evolution-settings-ui/client/pending-state
9
9
  */
10
10
  import type { PendingRow, PreviewAnswer } from './api.ts';
11
+ import type { RouteFailure } from './route-client.generated.ts';
11
12
  /** What one row's preview is showing while it is open. */
12
13
  export type PreviewView = {
13
14
  readonly kind: 'loading';
@@ -21,7 +22,7 @@ export type PreviewView = {
21
22
  readonly reason: string;
22
23
  } | {
23
24
  readonly kind: 'failed';
24
- readonly message: string;
25
+ readonly failure: RouteFailure;
25
26
  };
26
27
  /** What the card is showing right now. */
27
28
  export type PendingView = {
@@ -33,7 +34,7 @@ export type PendingView = {
33
34
  readonly kind: 'empty';
34
35
  } | {
35
36
  readonly kind: 'failed';
36
- readonly message: string;
37
+ readonly failure: RouteFailure;
37
38
  };
38
39
  /** The whole card state: the view, the ids with an action in flight, and the last refusal line. */
39
40
  export interface PendingState {
@@ -62,8 +63,8 @@ export declare function loading(state: PendingState): PendingState;
62
63
  * being shown the same line a slow answer shows.
63
64
  */
64
65
  export declare function loaded(state: PendingState, rows: readonly PendingRow[]): PendingState;
65
- /** The list could not be read: the reason stays on screen next to a retry control. */
66
- export declare function failed(state: PendingState, message: string): PendingState;
66
+ /** The list could not be read: the classified failure stays on screen next to a retry control. */
67
+ export declare function failed(state: PendingState, failure: RouteFailure): PendingState;
67
68
  /** Mark one record as having an action in flight (its buttons are disabled until it ends). */
68
69
  export declare function beginAction(state: PendingState, id: string): PendingState;
69
70
  /** The action ended, whatever it answered. */
@@ -0,0 +1,45 @@
1
+ /**
2
+ * The four-state transport classifier every family client shares (W7 §2). The platform owns
3
+ * admission (401/403/404 are its answers, not ours), so the client reads the STATUS only for those
4
+ * three and never tries to interpret a platform body. Everything the family route itself answers
5
+ * keeps its business envelope: 2xx + {ok:true,data} or 2xx + {ok:false,code,message}.
6
+ */
7
+ export type RouteFailure = {
8
+ readonly kind: 'unauthenticated';
9
+ readonly status: 401;
10
+ } | {
11
+ readonly kind: 'forbidden';
12
+ readonly status: 403;
13
+ } | {
14
+ readonly kind: 'unmounted';
15
+ readonly status: 404;
16
+ } | {
17
+ readonly kind: 'server';
18
+ readonly status: number;
19
+ readonly detail?: string;
20
+ } | {
21
+ readonly kind: 'refused';
22
+ readonly code: string;
23
+ readonly message: string;
24
+ };
25
+ /** One route answer: the data, or the classified failure. */
26
+ export type RouteAnswer<T> = {
27
+ readonly ok: true;
28
+ readonly data: T;
29
+ } | {
30
+ readonly ok: false;
31
+ readonly failure: RouteFailure;
32
+ };
33
+ /** Classify a non-2xx status: 401/403/404 keep their own kind, everything else is a server answer. */
34
+ export declare function classify(status: number): RouteFailure;
35
+ /**
36
+ * One request, no retries and no body parsing for the platform's admission answers.
37
+ * @param doFetch - the fetch in use (the browser's, or a spec's stub).
38
+ * @param path - the exact route path from the generated ROUTES table.
39
+ * @param init - standard RequestInit.
40
+ * @returns the data, or the classified failure (network/parse failures read as 'server').
41
+ */
42
+ export declare function requestJson<T>(doFetch: typeof fetch, path: string, init?: RequestInit): Promise<RouteAnswer<T>>;
43
+ /** The one-sentence rendering legacy callers attach to a classified failure. */
44
+ export declare function failureMessage(failure: RouteFailure): string;
45
+ //# sourceMappingURL=route-client.generated.d.ts.map
@@ -0,0 +1,31 @@
1
+ /** One host data-plane surface the browser half can call. */
2
+ export type SurfaceId = 'approval' | 'skill-history' | 'skill-health' | 'diagnostics';
3
+ /**
4
+ * Every family host path, generated from evolution-core's HOST_ROUTES (the ONE declaration).
5
+ * A browser half cannot import the host package at runtime (client bundles keep @deepseek-ai/*
6
+ * external), so the table is mirrored here mechanically; a rename in core regenerates all three
7
+ * client copies, and the generator's --check makes a stale copy red.
8
+ */
9
+ export declare const ROUTES: {
10
+ readonly approval: {
11
+ readonly pending: "/api/dsh-evolution/approval/pending";
12
+ readonly preview: "/api/dsh-evolution/approval/preview";
13
+ readonly approve: "/api/dsh-evolution/approval/approve";
14
+ readonly reject: "/api/dsh-evolution/approval/reject";
15
+ };
16
+ readonly 'skill-history': {
17
+ readonly skills: "/api/dsh-evolution/skill-history/skills";
18
+ readonly versions: "/api/dsh-evolution/skill-history/versions";
19
+ readonly diff: "/api/dsh-evolution/skill-history/versions/diff";
20
+ readonly body: "/api/dsh-evolution/skill-history/versions/body";
21
+ readonly undo: "/api/dsh-evolution/skill-history/undo";
22
+ readonly health: "/api/dsh-evolution/skill-history/health";
23
+ };
24
+ readonly 'skill-health': {
25
+ readonly rows: "/api/dsh-evolution/skill-health/rows";
26
+ };
27
+ readonly diagnostics: {
28
+ readonly view: "/api/dsh-evolution/diagnostics";
29
+ };
30
+ };
31
+ //# sourceMappingURL=routes.generated.d.ts.map
@@ -35,6 +35,8 @@ export declare function rowConfigKey(bundle: string, rowId: string): string;
35
35
  * the section — one definition in the module both sides already share is what keeps that pair acyclic.
36
36
  */
37
37
  export declare const PENDING_CARD_KEY = "approval-pending";
38
+ /** The diagnostics card's slot key: read-only, no settings namespace (its data comes from HOST_ROUTES.diagnostics). */
39
+ export declare const DIAGNOSTICS_CARD_KEY = "diagnostics";
38
40
  /** Snapshot of one settings row, as the client seat reports it. */
39
41
  export interface ParamSectionSnapshot {
40
42
  status: 'loading' | 'ready' | 'unavailable';
@@ -44,8 +46,6 @@ export interface ParamSectionSnapshot {
44
46
  writable: boolean;
45
47
  /** U-5: the composition layer a cleared field reverts to (platform `base`, previously dropped). */
46
48
  base: Record<string, unknown> | undefined;
47
- /** U-5: where a save lands — `memory` means this session only (platform `mode`, previously dropped). */
48
- mode: 'host' | 'memory';
49
49
  /**
50
50
  * Why an `unavailable` snapshot is unavailable: this bundle found no settings seat at
51
51
  * all, or the seat is there and serves no such row. The two read differently to the
@@ -84,7 +84,6 @@ export interface ConfigFormSnapshotLike<T = Record<string, unknown>> {
84
84
  user: unknown;
85
85
  revision: number | undefined;
86
86
  writable: boolean;
87
- mode: 'host' | 'memory';
88
87
  }
89
88
  /** One row's client write face (0.2.x `configForms`). */
90
89
  export interface ConfigFormLike<T = Record<string, unknown>> {
@@ -0,0 +1,43 @@
1
+ import { type ReactNode } from 'react';
2
+ import type { RouteAnswer, RouteFailure } from './route-client.generated.ts';
3
+ import type { FallbackHint } from './contracts.generated.ts';
4
+ /**
5
+ * The four-state base of every family card (W7 §2). Load states are read from the generated
6
+ * route-client's answer — a card owns no HTTP vocabulary and no second state machine.
7
+ */
8
+ export type LoadState<T> = {
9
+ readonly phase: 'loading';
10
+ } | {
11
+ readonly phase: 'ready';
12
+ readonly data: T;
13
+ } | {
14
+ readonly phase: 'empty';
15
+ } | {
16
+ readonly phase: 'failed';
17
+ readonly failure: RouteFailure;
18
+ };
19
+ /** Map one classified answer onto the card's four states. */
20
+ export declare function loadStateOf<T>(answer: RouteAnswer<T>, isEmpty: (data: T) => boolean): LoadState<T>;
21
+ /** The locale lookup every block calls (the platform's own `t`). */
22
+ export interface Translate {
23
+ (key: string, values?: Readonly<Record<string, string | number>>): string;
24
+ }
25
+ export type BlockTone = 'neutral' | 'warn' | 'fail';
26
+ export interface BlockProps {
27
+ readonly tone?: BlockTone;
28
+ readonly title?: string;
29
+ readonly children?: ReactNode;
30
+ }
31
+ /** A neutral/ warn/ fail block; a fail block is role=alert so assistive tech announces it. */
32
+ export declare function Block(props: BlockProps): ReactNode;
33
+ export interface FailureBlockProps {
34
+ readonly failure: RouteFailure;
35
+ readonly t: Translate;
36
+ /** The command-face fallback for this surface (rendered on unmounted/failed answers). */
37
+ readonly fallback?: FallbackHint | undefined;
38
+ }
39
+ /** One classified failure as a reader-facing sentence. */
40
+ export declare function failureText(failure: RouteFailure, t: Translate): string;
41
+ /** The failed-state block: sentence + (when the surface exists but is unreachable) the command fallback. */
42
+ export declare function FailureBlock(props: FailureBlockProps): ReactNode;
43
+ //# sourceMappingURL=states.generated.d.ts.map
@@ -5,9 +5,13 @@ export declare const CSS: string;
5
5
  /** The slice of the DOM the injection touches (this package typechecks without DOM lib). */
6
6
  interface StyleHost {
7
7
  querySelector(selector: string): unknown;
8
+ /** The element shape the injection uses. `remove()` is on the real DOM element and is what the
9
+ * S5-08 disposer calls, so the host shape names it too - otherwise the package cannot typecheck
10
+ * without the DOM lib while still using the disposal the browser offers. */
8
11
  createElement(tag: string): {
9
12
  dataset: Record<string, string>;
10
13
  textContent: string;
14
+ remove(): void;
11
15
  };
12
16
  head: {
13
17
  appendChild(node: unknown): void;
@@ -15,8 +19,14 @@ interface StyleHost {
15
19
  }
16
20
  /**
17
21
  * Inject the stylesheet once per document.
22
+ *
23
+ * S5-08: it RETURNS the disposer. The tag check alone answered only half the question - "is it
24
+ * there" - and left "who takes it away" to nobody, so a hot reload (unmount, remount) found the old
25
+ * tag, skipped the injection, and kept serving the PREVIOUS bundle CSS. skill-history hands its
26
+ * disposer back to the fiber; this is the same shape for the same concern.
18
27
  * @param host - the document to inject into; defaults to the global one.
28
+ * @returns the disposal; a no-op when there is no document or the tag was already present.
19
29
  */
20
- export declare function injectStyles(host?: StyleHost): void;
30
+ export declare function injectStyles(host?: StyleHost): () => void;
21
31
  export {};
22
32
  //# sourceMappingURL=styles.d.ts.map
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@lmzhen/dsh-evolution-settings-ui",
3
3
  "description": "Web settings cards for the evolution family's parameter namespaces (community build)",
4
- "version": "0.26.15",
4
+ "version": "0.27.0",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },
@@ -39,7 +39,10 @@
39
39
  }
40
40
  },
41
41
  "peerDependencies": {
42
- "@deepseek-ai/cordis": "^4.0.1",
42
+ "@deepseek-ai/cordis": "^4.0.5",
43
43
  "react": "^18.2.0"
44
+ },
45
+ "engines": {
46
+ "dsh": ">=0.2.1-rc.1 <0.2.2-0"
44
47
  }
45
48
  }
@@ -1,17 +0,0 @@
1
- /**
2
- * The approval paths this browser half calls.
3
- *
4
- * A browser half cannot import the host package's table at runtime (the family's client bundles treat
5
- * every `@deepseek-ai/*` specifier as external), so the literals are declared here and a spec compares
6
- * them against `APPROVAL_ROUTES` in `evolution-approval` — a rename on one side would otherwise surface
7
- * as a 404 in the GUI instead of in CI.
8
- * @module @lmzhen/dsh-evolution-settings-ui/client/approval-routes
9
- */
10
- /** One path per call the pending card makes. */
11
- export declare const APPROVAL_CLIENT_ROUTES: {
12
- readonly pending: "/api/dsh-evolution/approval/pending";
13
- readonly approve: "/api/dsh-evolution/approval/approve";
14
- readonly reject: "/api/dsh-evolution/approval/reject";
15
- readonly preview: "/api/dsh-evolution/approval/preview";
16
- };
17
- //# sourceMappingURL=approval-routes.d.ts.map