@lmzhen/dsh-evolution-settings-ui 0.26.26 → 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);
@@ -768,11 +863,11 @@ window.__ModuleLoader__.load({
768
863
  const [waiting, running] = await Promise.all([api.pending(), api.pending("executing")]);
769
864
  if (readTicket.current !== ticket) return;
770
865
  if (!waiting.ok) {
771
- apply((current) => failed(current, waiting.message));
866
+ apply((current) => failed(current, waiting.failure));
772
867
  return;
773
868
  }
774
869
  if (!running.ok) {
775
- apply((current) => failed(current, running.message));
870
+ apply((current) => failed(current, running.failure));
776
871
  return;
777
872
  }
778
873
  apply((current) => loaded(current, [...waiting.data, ...running.data]));
@@ -792,7 +887,7 @@ window.__ModuleLoader__.load({
792
887
  (async () => {
793
888
  const answer = action === "approve" ? await api.approve(id) : await api.reject(id);
794
889
  if (!answer.ok) {
795
- apply((current) => noticed(endAction(current, id), answer.message));
890
+ apply((current) => noticed(endAction(current, id), failureText(answer.failure, translate)));
796
891
  return;
797
892
  }
798
893
  apply((current) => noticed(endAction(current, id), null));
@@ -815,7 +910,7 @@ window.__ModuleLoader__.load({
815
910
  reason: answer.data.reason
816
911
  } : {
817
912
  kind: "failed",
818
- message: answer.message
913
+ failure: answer.failure
819
914
  };
820
915
  apply((current) => previewArrived(current, id, view));
821
916
  };
@@ -836,7 +931,7 @@ window.__ModuleLoader__.load({
836
931
  if (view.kind === "failed") return (0, react.createElement)("p", {
837
932
  className: "evolution-param-error",
838
933
  role: "status"
839
- }, view.message);
934
+ }, failureText(view.failure, translate));
840
935
  if (view.kind === "unavailable") return (0, react.createElement)("p", {
841
936
  className: "evolution-param-error",
842
937
  role: "status"
@@ -937,10 +1032,11 @@ window.__ModuleLoader__.load({
937
1032
  className: "evolution-param-note",
938
1033
  role: "status"
939
1034
  }, t("approvalEmpty"));
940
- if (state.view.kind === "failed") return (0, react.createElement)("div", { className: "evolution-param-actions" }, (0, react.createElement)("p", {
941
- className: "evolution-param-error",
942
- role: "status"
943
- }, 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", {
944
1040
  className: "evolution-param-button",
945
1041
  type: "button",
946
1042
  onClick: () => {
@@ -971,6 +1067,120 @@ window.__ModuleLoader__.load({
971
1067
  }, t("approvalRefresh"))), body()) : null);
972
1068
  }
973
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
974
1184
  //#region src/client/messages.ts
975
1185
  /**
976
1186
  * Copy for the evolution settings section and its parameter cards.
@@ -1002,14 +1212,13 @@ window.__ModuleLoader__.load({
1002
1212
  discard: "放弃修改",
1003
1213
  saving: "保存中…",
1004
1214
  resetting: "恢复中…",
1005
- destHost: "保存去向:本机配置",
1215
+ writeTargetHost: "保存去向:宿主配置(对所有会话生效)",
1006
1216
  invalidNumber: "需要一个数字",
1007
1217
  invalidSelect: "不在允许的取值里",
1008
1218
  invalidAtLeast: "不能小于同组的 {n}",
1009
1219
  approvalConfirmNamed: "即将批准:{n}",
1010
1220
  approvalConfirmYes: "确认批准",
1011
1221
  approvalConfirmNo: "取消",
1012
- destMemory: "保存去向:仅本次会话",
1013
1222
  resetTo: "复位为 {value}",
1014
1223
  refused: "这个值没有被接受(界面上保持原样)。想看原因:在对话里让它说明。",
1015
1224
  resetRefused: "恢复默认值没有生效(这一行还是你的值)。想看原因:在对话里让它说明。",
@@ -1045,7 +1254,27 @@ window.__ModuleLoader__.load({
1045
1254
  approvalPreviewNone: "看不了预览:",
1046
1255
  approvalDiffAdded: "{n} 行新增",
1047
1256
  approvalDiffRemoved: "{n} 行删除",
1048
- 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}"
1049
1278
  };
1050
1279
  /** English copy, key for key with {@link zh}. */
1051
1280
  const en = {
@@ -1065,14 +1294,13 @@ window.__ModuleLoader__.load({
1065
1294
  discard: "Discard",
1066
1295
  saving: "Saving…",
1067
1296
  resetting: "Resetting…",
1068
- destHost: "Saved to: this machine",
1297
+ writeTargetHost: "Saved to: host configuration (applies to every session)",
1069
1298
  invalidNumber: "Needs a number",
1070
1299
  invalidSelect: "Not one of the allowed values",
1071
1300
  invalidAtLeast: "must not be below {n}",
1072
1301
  approvalConfirmNamed: "About to approve: {n}",
1073
1302
  approvalConfirmYes: "Approve",
1074
1303
  approvalConfirmNo: "Cancel",
1075
- destMemory: "Saved to: this session only",
1076
1304
  resetTo: "Restores {value}",
1077
1305
  refused: "The value was not accepted (the page keeps what you see). For the reason, ask in the conversation.",
1078
1306
  resetRefused: "The reset did not take (this row still holds your value). For the reason, ask in the conversation.",
@@ -1108,7 +1336,27 @@ window.__ModuleLoader__.load({
1108
1336
  approvalPreviewNone: "no preview: ",
1109
1337
  approvalDiffAdded: "{n} lines added",
1110
1338
  approvalDiffRemoved: "{n} lines removed",
1111
- 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}"
1112
1360
  };
1113
1361
  /**
1114
1362
  * Card titles per settings namespace. A namespace without an entry falls back to
@@ -1175,6 +1423,23 @@ window.__ModuleLoader__.load({
1175
1423
  * typed from the registry (\`control\`), so booleans are switches, enums are
1176
1424
  * selects and numbers are numeric inputs with the unit shown beside the label.
1177
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
+ }
1178
1443
  /** One cell's text: absent reads as an empty control, containers as JSON. */
1179
1444
  function format(value) {
1180
1445
  if (value === void 0) return "";
@@ -1358,16 +1623,17 @@ window.__ModuleLoader__.load({
1358
1623
  if (snapshot.status === "unavailable") return (0, react.createElement)("p", {
1359
1624
  className: "evolution-param-note",
1360
1625
  role: "status"
1361
- }, t(snapshot.reason === "seat-missing" ? "seatMissing" : snapshot.reason === "projection-failed" ? "projectionFailed" : snapshot.reason === "session-memory" ? "destMemory" : "unavailable"));
1626
+ }, t(snapshot.reason === "seat-missing" ? "seatMissing" : snapshot.reason === "projection-failed" ? "projectionFailed" : "unavailable"));
1362
1627
  const user = isRecord(snapshot.user) ? snapshot.user : {};
1363
1628
  const value = isRecord(snapshot.value) ? snapshot.value : {};
1364
1629
  const disabled = !snapshot.writable;
1365
1630
  const locked = disabled || busy || clearing !== null;
1366
1631
  const titleKey = NAMESPACE_TITLES[namespace];
1367
1632
  const title = titleKey === void 0 ? namespace : t(titleKey);
1368
- const changed = fields.filter((field) => Object.hasOwn(user, field.id)).length;
1369
- const textOf = (field) => draft[field.id] ?? format(value[field.id]);
1370
- const dirty = fields.filter((field) => draft[field.id] !== void 0 && draft[field.id] !== format(value[field.id]));
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)]));
1371
1637
  const peerTextOf = (field) => {
1372
1638
  if (field.atLeast === "") return void 0;
1373
1639
  const related = fields.find((candidate) => candidate.id === field.atLeast);
@@ -1394,7 +1660,7 @@ window.__ModuleLoader__.load({
1394
1660
  (async () => {
1395
1661
  let accepted;
1396
1662
  try {
1397
- accepted = await clear(field);
1663
+ accepted = await clear(fieldKeyOf(namespace, { id: field }));
1398
1664
  } catch (caught) {
1399
1665
  setClearing(null);
1400
1666
  setError(caught instanceof Error && caught.message !== "" ? caught.message : t("resetRefused"));
@@ -1414,7 +1680,7 @@ window.__ModuleLoader__.load({
1414
1680
  setPending({
1415
1681
  accepted: await mutateOnce(dirty.map((field) => ({
1416
1682
  op: "set",
1417
- path: [field.id],
1683
+ path: [keyOf(field)],
1418
1684
  value: parseFor(field.control, textOf(field))
1419
1685
  }))),
1420
1686
  resets: false
@@ -1432,7 +1698,7 @@ window.__ModuleLoader__.load({
1432
1698
  onClick: () => {
1433
1699
  setOpen(!open);
1434
1700
  }
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-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", {
1436
1702
  className: "evolution-param-note",
1437
1703
  role: "status"
1438
1704
  }, t("readonly")) : null, fields.length === 0 ? (0, react.createElement)("p", {
@@ -1445,10 +1711,10 @@ window.__ModuleLoader__.load({
1445
1711
  key: field.id,
1446
1712
  field,
1447
1713
  text: textOf(field),
1448
- overridden: Object.hasOwn(user, field.id),
1714
+ overridden: Object.hasOwn(user, keyOf(field)),
1449
1715
  disabled: locked,
1450
1716
  clearing: clearing === field.id,
1451
- resetTo: resetToOf(snapshot.base?.[field.id]),
1717
+ resetTo: resetToOf(snapshot.base?.[keyOf(field)]),
1452
1718
  invalid: invalidReason(field, textOf(field), peerTextOf(field)),
1453
1719
  t,
1454
1720
  onChange: change,
@@ -1498,7 +1764,6 @@ window.__ModuleLoader__.load({
1498
1764
  user: void 0,
1499
1765
  writable: false,
1500
1766
  base: void 0,
1501
- mode: "host",
1502
1767
  reason: "seat-missing"
1503
1768
  });
1504
1769
  /** What a card shows when the seat served the row but projecting it failed. A frozen constant
@@ -1510,7 +1775,6 @@ window.__ModuleLoader__.load({
1510
1775
  user: void 0,
1511
1776
  writable: false,
1512
1777
  base: void 0,
1513
- mode: "host",
1514
1778
  reason: "projection-failed"
1515
1779
  });
1516
1780
  /**
@@ -1525,8 +1789,7 @@ window.__ModuleLoader__.load({
1525
1789
  user: snapshot.user,
1526
1790
  writable: snapshot.writable,
1527
1791
  base: isRecord(snapshot.base) ? snapshot.base : void 0,
1528
- mode: snapshot.mode,
1529
- ...snapshot.status === "unavailable" ? { reason: snapshot.mode === "memory" ? "session-memory" : "not-served" } : {}
1792
+ ...snapshot.status === "unavailable" ? { reason: "not-served" } : {}
1530
1793
  };
1531
1794
  }
1532
1795
  /**
@@ -1687,9 +1950,9 @@ window.__ModuleLoader__.load({
1687
1950
  * @param fields - the row's fields.
1688
1951
  * @returns how many of them the user overrode.
1689
1952
  */
1690
- function overridden(user, fields) {
1953
+ function overridden(user, fields, namespace) {
1691
1954
  const layer = isRecord(user) ? user : {};
1692
- return fields.filter((field) => Object.hasOwn(layer, field.id)).length;
1955
+ return fields.filter((field) => Object.hasOwn(layer, fieldKeyOf(namespace, field))).length;
1693
1956
  }
1694
1957
  /**
1695
1958
  * Render one row's configuration entry.
@@ -1702,7 +1965,7 @@ window.__ModuleLoader__.load({
1702
1965
  className: "evolution-param-note",
1703
1966
  role: "status"
1704
1967
  }, t("seatMissing"));
1705
- const changed = overridden(form.state.user, fields);
1968
+ const changed = overridden(form.state.user, fields, namespace);
1706
1969
  if (view === "summary") return changed === 0 ? null : (0, react.createElement)("span", null, t("changed", { n: changed }));
1707
1970
  return ParamCardView({
1708
1971
  namespace,
@@ -1744,6 +2007,8 @@ window.__ModuleLoader__.load({
1744
2007
  * the section — one definition in the module both sides already share is what keeps that pair acyclic.
1745
2008
  */
1746
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";
1747
2012
  //#endregion
1748
2013
  //#region src/client/SettingsSection.ts
1749
2014
  /**
@@ -1765,7 +2030,10 @@ window.__ModuleLoader__.load({
1765
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", {
1766
2031
  key: PENDING_CARD_KEY,
1767
2032
  className: "evolution-params-group"
1768
- }, 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", {
1769
2037
  key: section_.namespace,
1770
2038
  className: "evolution-params-group"
1771
2039
  }, face.renderSlot(CARD_SLOT, {}, { entryKey: section_.namespace }))));
@@ -1970,9 +2238,20 @@ window.__ModuleLoader__.load({
1970
2238
  inject: () => ({
1971
2239
  t,
1972
2240
  api,
1973
- confirmMode
2241
+ confirmMode,
2242
+ fallback: FALLBACKS.approval
1974
2243
  })
1975
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);
1976
2255
  });
1977
2256
  seam.slots.inject(ROW_CONFIG_SLOT, function* () {
1978
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>;
@@ -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,14 +46,12 @@ 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
52
52
  * operator (one is the UI's own gap, the other is a row that is not composed).
53
53
  */
54
- reason?: 'seat-missing' | 'not-served' | 'projection-failed' | 'session-memory';
54
+ reason?: 'seat-missing' | 'not-served' | 'projection-failed';
55
55
  }
56
56
  /** The reactive source the renderer binds to a `use<Name>` seat. */
57
57
  export interface ParamSectionSource {
@@ -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
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.26",
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