@lmzhen/dsh-evolution-ui 0.33.0 → 0.35.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
@@ -841,12 +841,12 @@ window.__ModuleLoader__.load({
841
841
  projectionFailed: "这一块读设置时出错,已记入日志;其它参数不受影响",
842
842
  readonly: "本次会话不允许写入设置(临时运行模式)",
843
843
  apply: "保存",
844
- reset: "恢复默认值",
844
+ reset: "复位",
845
845
  empty: "这里没有可改项",
846
846
  changed: "{n} 项已覆盖",
847
847
  discard: "放弃修改",
848
848
  saving: "保存中…",
849
- resetting: "恢复中…",
849
+ resetting: "复位中…",
850
850
  writeTargetHost: "保存去向:宿主配置(对所有会话生效)",
851
851
  invalidNumber: "需要一个数字",
852
852
  invalidSelect: "不在允许的取值里",
@@ -858,6 +858,7 @@ window.__ModuleLoader__.load({
858
858
  resetTo: "复位为 {value}",
859
859
  saved: "已保存。",
860
860
  saveReadbackFailed: "写入已被接受,但回读失败;点刷新重读后再确认。",
861
+ readbackStale: "显示的是最后一次成功读取的值;当前回读失败,刷新后可能变化。",
861
862
  saveConflict: "这个值没有被接受:设置已被其他页面修改。请重读后再保存。",
862
863
  resetDone: "已恢复默认值。",
863
864
  refused: "这个值没有被接受(界面上保持原样)。想看原因:在对话里让它说明。",
@@ -926,6 +927,8 @@ window.__ModuleLoader__.load({
926
927
  diagStatusUnknown: "未知",
927
928
  diagRemedy: "建议:",
928
929
  diagNoRows: "没有可显示的运行状态",
930
+ diagPurpose: "用途:检查插件挂载、平台服务和数据面是否正常,不修改任何状态。",
931
+ diagFooter: "诊断 schema v{version} · 生成于 {at}",
929
932
  stateLoading: "正在读取…",
930
933
  failureUnauthorized: "需要登录:重新打开这个页面完成登录后再试。",
931
934
  failureForbidden: "平台拒绝了这个请求(身份不是 owner)。",
@@ -946,7 +949,7 @@ window.__ModuleLoader__.load({
946
949
  projectionFailed: "reading this row failed and the error is in the log; the other parameters are unaffected",
947
950
  readonly: "settings cannot be written in this session (temporary mode)",
948
951
  apply: "Save",
949
- reset: "Reset to default",
952
+ reset: "Reset",
950
953
  empty: "nothing to change here",
951
954
  changed: "{n} overridden",
952
955
  discard: "Discard",
@@ -963,6 +966,7 @@ window.__ModuleLoader__.load({
963
966
  resetTo: "Restores {value}",
964
967
  saved: "Saved.",
965
968
  saveReadbackFailed: "The write was accepted, but reading it back failed; refresh and confirm.",
969
+ readbackStale: "Showing the last successfully read values; the current re-read failed and may change after refresh.",
966
970
  saveConflict: "The value was not accepted: settings changed in another page. Re-read before saving again.",
967
971
  resetDone: "Default restored.",
968
972
  refused: "The value was not accepted (the page keeps what you see). For the reason, ask in the conversation.",
@@ -1031,6 +1035,8 @@ window.__ModuleLoader__.load({
1031
1035
  diagStatusUnknown: "unknown",
1032
1036
  diagRemedy: "Suggested: ",
1033
1037
  diagNoRows: "no runtime status to show",
1038
+ diagPurpose: "Purpose: check plugin mounting, platform services and data planes; nothing is changed.",
1039
+ diagFooter: "diagnostics schema v{version} · generated {at}",
1034
1040
  stateLoading: "reading…",
1035
1041
  failureUnauthorized: "Sign-in is required: reopen this page and sign in, then retry.",
1036
1042
  failureForbidden: "The platform refused this request (not the owner principal).",
@@ -1264,10 +1270,12 @@ window.__ModuleLoader__.load({
1264
1270
  type: "button",
1265
1271
  className: "evolution-param-button",
1266
1272
  disabled: props.disabled || props.clearing,
1273
+ "data-role": "reset",
1274
+ title: props.resetTo === null ? void 0 : t("resetTo", { value: props.resetTo }),
1267
1275
  onClick: () => {
1268
1276
  props.onClear(field.id);
1269
1277
  }
1270
- }, props.clearing ? t("resetting") : t("reset")), props.resetTo === null ? null : (0, react.createElement)("span", { className: "evolution-param-hint" }, t("resetTo", { value: props.resetTo }))) : null);
1278
+ }, props.clearing ? t("resetting") : t("reset"))) : null);
1271
1279
  }
1272
1280
  /**
1273
1281
  * Render one row's card body from a supplied snapshot.
@@ -1426,7 +1434,11 @@ window.__ModuleLoader__.load({
1426
1434
  onClick: () => {
1427
1435
  setOpen(!open);
1428
1436
  }
1429
- }, (0, react.createElement)("span", { className: "evolution-param-card-title" }, title), changed === 0 ? null : (0, react.createElement)("span", { className: "evolution-param-count" }, t("changed", { n: changed })), (0, react.createElement)("span", { className: "evolution-param-chevron" }, open ? "▲" : "▼")), open ? (0, react.createElement)("div", { className: "evolution-param-body" }, hintKey === void 0 ? null : (0, react.createElement)("p", { className: "evolution-param-note" }, t(hintKey)), disabled ? (0, react.createElement)("p", {
1437
+ }, (0, react.createElement)("span", { className: "evolution-param-card-title" }, title), changed === 0 ? null : (0, react.createElement)("span", { className: "evolution-param-count" }, t("changed", { n: changed })), (0, react.createElement)("span", { className: "evolution-param-chevron" }, open ? "▲" : "▼")), open ? (0, react.createElement)("div", { className: "evolution-param-body" }, hintKey === void 0 ? null : (0, react.createElement)("p", { className: "evolution-param-note" }, t(hintKey)), snapshot.stale === true ? (0, react.createElement)("p", {
1438
+ className: "evolution-param-note",
1439
+ role: "status",
1440
+ "data-role": "stale-note"
1441
+ }, t("readbackStale")) : null, disabled ? (0, react.createElement)("p", {
1430
1442
  className: "evolution-param-note",
1431
1443
  role: "status"
1432
1444
  }, t("readonly")) : null, fields.length === 0 ? (0, react.createElement)("p", {
@@ -1677,34 +1689,51 @@ window.__ModuleLoader__.load({
1677
1689
  ...EMPTY,
1678
1690
  reason: "seat-missing"
1679
1691
  };
1692
+ let lastReady;
1693
+ let generation = 0;
1680
1694
  const listeners = /* @__PURE__ */ new Set();
1681
- let started = false;
1682
- let listening = false;
1695
+ let remoteOff;
1683
1696
  const notify = () => {
1684
1697
  for (const listener of listeners) listener();
1685
1698
  };
1686
- const refresh = async () => {
1687
- snapshot = await describe(namespace);
1699
+ const absorb = (next) => {
1700
+ if (next.status === "ready") {
1701
+ lastReady = next;
1702
+ snapshot = next;
1703
+ } else if (lastReady !== void 0) snapshot = {
1704
+ ...lastReady,
1705
+ stale: true,
1706
+ reason: next.reason ?? "readback-failed"
1707
+ };
1708
+ else snapshot = next;
1688
1709
  notify();
1689
1710
  };
1690
- const start = () => {
1691
- if (started) return;
1692
- started = true;
1693
- if (!listening) {
1694
- listening = true;
1695
- (ctx.get?.("remote"))?.$on?.("settings/document-updated", (changed) => {
1696
- if (changed === namespace) refresh();
1697
- });
1698
- }
1699
- refresh();
1711
+ const refresh = async () => {
1712
+ const ticket = ++generation;
1713
+ const next = await describe(namespace);
1714
+ if (ticket !== generation) return;
1715
+ absorb(next);
1716
+ };
1717
+ const remote = () => ctx.get?.("remote");
1718
+ const stopRemote = () => {
1719
+ remoteOff?.();
1720
+ remoteOff = void 0;
1721
+ };
1722
+ const startRemote = () => {
1723
+ if (remoteOff !== void 0) return;
1724
+ remoteOff = remote()?.$on?.("settings/document-updated", (changed) => {
1725
+ if (changed === namespace) refresh();
1726
+ });
1700
1727
  };
1701
1728
  const form = {
1702
1729
  getSnapshot: () => snapshot,
1703
1730
  subscribe: (listener) => {
1704
1731
  listeners.add(listener);
1705
- start();
1732
+ startRemote();
1733
+ refresh();
1706
1734
  return () => {
1707
1735
  listeners.delete(listener);
1736
+ if (listeners.size === 0) stopRemote();
1708
1737
  };
1709
1738
  },
1710
1739
  set: async (field, value) => await form.mutate([{
@@ -1718,37 +1747,63 @@ window.__ModuleLoader__.load({
1718
1747
  }]),
1719
1748
  mutate: async (ops, expectedRevision) => {
1720
1749
  const outcome = await write(namespace, ops, expectedRevision ?? snapshot.revision);
1721
- if (outcome.snapshot !== void 0) {
1722
- snapshot = outcome.snapshot;
1723
- notify();
1724
- } else await refresh();
1750
+ if (outcome.snapshot !== void 0) absorb(outcome.snapshot);
1751
+ else await refresh();
1725
1752
  return outcome.ok;
1753
+ },
1754
+ refresh,
1755
+ dispose: () => {
1756
+ stopRemote();
1757
+ listeners.clear();
1726
1758
  }
1727
1759
  };
1728
1760
  return form;
1729
1761
  }
1730
1762
  /**
1731
- * Build one cached form per family namespace over the family settings routes.
1732
- * @param ctx - the client plugin context (remote subscription is optional).
1733
- * @returns the seat; each namespace is served by /api/dsh-evolution/settings/*.
1763
+ * Build one seat per client registration; every form's remote listener is released by
1764
+ * the caller's effect. No module-level WeakMap: a replaced registration must not share
1765
+ * a dead seat with the previous one.
1734
1766
  */
1735
- const seats = /* @__PURE__ */ new WeakMap();
1736
1767
  function createSettingsSeat(ctx) {
1737
- const cached = seats.get(ctx);
1738
- if (cached !== void 0) return cached;
1739
1768
  const forms = /* @__PURE__ */ new Map();
1740
- const seat = { get(namespace) {
1741
- let form = forms.get(namespace);
1742
- if (form === void 0) {
1743
- form = createForm(namespace, ctx);
1744
- forms.set(namespace, form);
1769
+ return {
1770
+ get(namespace) {
1771
+ let form = forms.get(namespace);
1772
+ if (form === void 0) {
1773
+ form = createForm(namespace, ctx);
1774
+ forms.set(namespace, form);
1775
+ }
1776
+ return form;
1777
+ },
1778
+ dispose: () => {
1779
+ for (const form of forms.values()) form.dispose?.();
1780
+ forms.clear();
1745
1781
  }
1746
- return form;
1747
- } };
1748
- seats.set(ctx, seat);
1749
- return seat;
1782
+ };
1750
1783
  }
1751
1784
  //#endregion
1785
+ //#region ../evolution-client-kit/src/format.ts
1786
+ /**
1787
+ * Format an ISO string, epoch milliseconds, or null as local wall time.
1788
+ * Invalid values return null; callers decide how to say "unknown".
1789
+ * @param value - ISO string, epoch ms, or null.
1790
+ * @param options - seconds on/off; defaults to on.
1791
+ * @returns the formatted local time, or null when the value is absent/invalid.
1792
+ */
1793
+ function formatLocalDateTime(value, options = {}) {
1794
+ if (value === null || value === void 0) return null;
1795
+ const ms = typeof value === "number" ? value : Date.parse(value);
1796
+ if (!Number.isFinite(ms)) return null;
1797
+ const date = new Date(ms);
1798
+ if (Number.isNaN(date.getTime())) return null;
1799
+ const pad = (part) => String(part).padStart(2, "0");
1800
+ const head = String(date.getFullYear()) + "-" + pad(date.getMonth() + 1) + "-" + pad(date.getDate()) + " " + pad(date.getHours()) + ":" + pad(date.getMinutes());
1801
+ return options.seconds === false ? head : head + ":" + pad(date.getSeconds());
1802
+ }
1803
+ //#endregion
1804
+ //#region ../evolution-client-kit/src/notice.ts
1805
+ /** One inline state notice: semantic tone plus text, no surface-specific styling. */
1806
+ //#endregion
1752
1807
  //#region ../evolution-client-kit/src/action.ts
1753
1808
  /**
1754
1809
  * Command-only actions. Approval decisions are a separate host API and must never
@@ -1848,7 +1903,8 @@ window.__ModuleLoader__.load({
1848
1903
  user: snapshot.user,
1849
1904
  writable: snapshot.writable,
1850
1905
  base: isRecord(snapshot.base) ? snapshot.base : void 0,
1851
- ...snapshot.status === "unavailable" ? { reason: snapshot.reason ?? "not-served" } : {}
1906
+ ...snapshot.status === "unavailable" ? { reason: snapshot.reason ?? "not-served" } : {},
1907
+ ...snapshot.stale === true ? { stale: true } : {}
1852
1908
  };
1853
1909
  }
1854
1910
  /**
@@ -1958,7 +2014,6 @@ window.__ModuleLoader__.load({
1958
2014
  readback: "failed",
1959
2015
  kind: "transport"
1960
2016
  };
1961
- const before = seat.getSnapshot().revision;
1962
2017
  let written = false;
1963
2018
  try {
1964
2019
  written = await seat.mutate(ops);
@@ -1970,12 +2025,15 @@ window.__ModuleLoader__.load({
1970
2025
  failure
1971
2026
  };
1972
2027
  }
1973
- const snapshot = seat.getSnapshot();
1974
2028
  if (!written) return {
1975
2029
  written: false,
1976
2030
  readback: "failed",
1977
2031
  kind: "conflict"
1978
2032
  };
2033
+ try {
2034
+ await seat.refresh?.();
2035
+ } catch {}
2036
+ const snapshot = seat.getSnapshot();
1979
2037
  if (snapshot.status !== "ready") return {
1980
2038
  written: true,
1981
2039
  readback: "failed",
@@ -1983,7 +2041,8 @@ window.__ModuleLoader__.load({
1983
2041
  };
1984
2042
  return {
1985
2043
  written: true,
1986
- readback: snapshot.revision !== before ? "ok" : "failed"
2044
+ readback: snapshot.stale === true ? "failed" : "ok",
2045
+ ...snapshot.stale === true ? { kind: "transport" } : {}
1987
2046
  };
1988
2047
  };
1989
2048
  return {
@@ -2586,6 +2645,8 @@ window.__ModuleLoader__.load({
2586
2645
  * read, because a half-list would read as a complete window.
2587
2646
  */
2588
2647
  const readTicket = (0, react.useRef)(0);
2648
+ const previewTicket = (0, react.useRef)(0);
2649
+ const actionTickets = (0, react.useRef)(/* @__PURE__ */ new Map());
2589
2650
  const load = async () => {
2590
2651
  apply(loading);
2591
2652
  const ticket = readTicket.current + 1;
@@ -2613,9 +2674,13 @@ window.__ModuleLoader__.load({
2613
2674
  };
2614
2675
  }, []);
2615
2676
  const decide = (id, action) => {
2677
+ if (latest.current.busy.includes(id)) return;
2678
+ const ticket = (actionTickets.current.get(id) ?? 0) + 1;
2679
+ actionTickets.current.set(id, ticket);
2616
2680
  apply((current) => beginAction(current, id));
2617
2681
  (async () => {
2618
2682
  const answer = action === "approve" ? await api.approve(id) : action === "reject" ? await api.reject(id) : await api.release(id);
2683
+ if (actionTickets.current.get(id) !== ticket) return;
2619
2684
  if (!answer.ok) {
2620
2685
  apply((current) => noticed(endAction(current, id), failureText(answer.failure, translate)));
2621
2686
  return;
@@ -2631,7 +2696,9 @@ window.__ModuleLoader__.load({
2631
2696
  };
2632
2697
  /** Ask the host what this record would store; the answer lands only if the row is still open. */
2633
2698
  const loadPreview = async (id) => {
2699
+ const ticket = ++previewTicket.current;
2634
2700
  const answer = await api.preview(id);
2701
+ if (ticket !== previewTicket.current || latest.current.open !== id) return;
2635
2702
  const view = answer.ok ? answer.data.available ? {
2636
2703
  kind: "ready",
2637
2704
  answer: answer.data
@@ -2685,7 +2752,13 @@ window.__ModuleLoader__.load({
2685
2752
  }, t("approvalDiffCut"))] : []
2686
2753
  ];
2687
2754
  const lines = [];
2755
+ const multiHunk = view.answer.hunks.length > 1 || view.answer.hunks.some((hunk) => hunk.path !== view.answer.path);
2688
2756
  for (const [hunkIndex, hunk] of view.answer.hunks.entries()) {
2757
+ if (multiHunk) lines.push((0, react.createElement)("div", {
2758
+ key: "hunk-" + String(hunkIndex),
2759
+ className: "evolution-param-hunk-path",
2760
+ "data-role": "hunk-path"
2761
+ }, hunk.path));
2689
2762
  for (const [index, line] of hunk.oldText.split("\n").entries()) {
2690
2763
  if (hunk.oldText === "" && line === "") continue;
2691
2764
  lines.push((0, react.createElement)("div", {
@@ -2854,6 +2927,7 @@ window.__ModuleLoader__.load({
2854
2927
  const zh = {
2855
2928
  "entry.label": "技能历史",
2856
2929
  title: "技能历史",
2930
+ bodyTitle: "正文版本",
2857
2931
  "workbench.history": "技能历史",
2858
2932
  "workbench.pending": "待批",
2859
2933
  "workbench.merge": "合并",
@@ -2883,6 +2957,7 @@ window.__ModuleLoader__.load({
2883
2957
  "failure.server": "读取失败,可以重试。",
2884
2958
  "fallback.hint": "可以改用命令:{command}",
2885
2959
  "undo.confirm": "确认回退(会写入一版,可再回退)",
2960
+ "undo.confirmUnit": "确认回退整个跨技能写入单元(会写入一版,可再回退)",
2886
2961
  "undo.no": "取消",
2887
2962
  current: "当前",
2888
2963
  "versions.count": "{n} 个版本",
@@ -2923,6 +2998,7 @@ window.__ModuleLoader__.load({
2923
2998
  "action.archive": "归档",
2924
2999
  "action.archive.note": "这一版存的是被归档的正文",
2925
3000
  "action.consolidate": "合并",
3001
+ "action.merge": "合并",
2926
3002
  "action.restructure": "结构调整",
2927
3003
  "action.extract": "跨技能提取",
2928
3004
  "action.support-write": "写入附带文件",
@@ -2946,6 +3022,7 @@ window.__ModuleLoader__.load({
2946
3022
  const en = {
2947
3023
  "entry.label": "Skill history",
2948
3024
  title: "Skill history",
3025
+ bodyTitle: "Body versions",
2949
3026
  "workbench.history": "Skill history",
2950
3027
  "workbench.pending": "Pending",
2951
3028
  "workbench.merge": "Merge",
@@ -2975,6 +3052,7 @@ window.__ModuleLoader__.load({
2975
3052
  "failure.server": "The read failed; you can retry.",
2976
3053
  "fallback.hint": "Command fallback: {command}",
2977
3054
  "undo.confirm": "Confirm restore (writes a new version; can be undone)",
3055
+ "undo.confirmUnit": "Confirm restoring the whole cross-skill unit (writes a new version; can be undone)",
2978
3056
  "undo.no": "Cancel",
2979
3057
  current: "Current",
2980
3058
  "versions.count": "{n} versions",
@@ -3015,6 +3093,7 @@ window.__ModuleLoader__.load({
3015
3093
  "action.archive": "archived",
3016
3094
  "action.archive.note": "this version holds the archived body",
3017
3095
  "action.consolidate": "merged",
3096
+ "action.merge": "merged",
3018
3097
  "action.restructure": "restructured",
3019
3098
  "action.extract": "extracted across skills",
3020
3099
  "action.support-write": "support file written",
@@ -3081,6 +3160,7 @@ window.__ModuleLoader__.load({
3081
3160
  ".evo-hist-skill[aria-current=\"true\"]{background:var(--dsw-alias-interactive-bg-active);color:var(--dsw-alias-label-primary);font-weight:500}",
3082
3161
  ".evo-hist-cell-line{display:flex;align-items:baseline;gap:var(--evo-space-6);min-width:0}",
3083
3162
  ".evo-hist-cell-meta{margin-top:var(--evo-space-2)}",
3163
+ ".evo-hist-skill-description{display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;margin-top:var(--evo-space-2);color:var(--dsw-alias-label-tertiary)}",
3084
3164
  ".evo-hist-cell{min-width:0;font-variant-numeric:tabular-nums}",
3085
3165
  ".evo-hist-slot-lead{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}",
3086
3166
  ".evo-hist-slot-meta{flex:none;white-space:nowrap}",
@@ -3124,7 +3204,7 @@ window.__ModuleLoader__.load({
3124
3204
  ".evo-doc-tag{position:absolute;left:var(--evo-space-8);top:var(--evo-space-8);color:var(--dsw-alias-label-tertiary)}",
3125
3205
  ".evo-doc-line-add{color:var(--evo-tone-success)}",
3126
3206
  ".evo-doc-line-del{color:var(--evo-tone-danger)}",
3127
- "@media (max-width:720px){.evo-hist-root{flex-direction:column}.evo-hist-aside{flex:0 0 auto;width:100%;max-height:42%;border-right:0;border-bottom:var(--evo-hairline-width) solid var(--dsw-alias-border-l2)}.evo-hist-row{max-width:none}}"
3207
+ "@media (max-width:900px){.evo-hist-root{flex-direction:column}.evo-hist-aside{flex:0 0 auto;width:100%;max-height:42%;border-right:0;border-bottom:var(--evo-hairline-width) solid var(--dsw-alias-border-l2)}.evo-hist-row{max-width:none}}"
3128
3208
  ].join("\n");
3129
3209
  //#endregion
3130
3210
  //#region src/panels/history/client/atoms.ts
@@ -3232,11 +3312,7 @@ window.__ModuleLoader__.load({
3232
3312
  */
3233
3313
  /** The local time an ISO timestamp names, as the operator's own clock shows it. */
3234
3314
  function localTime(at) {
3235
- const parsed = Date.parse(at);
3236
- if (Number.isNaN(parsed)) return at;
3237
- const date = new Date(parsed);
3238
- const pad = (value) => String(value).padStart(2, "0");
3239
- return String(date.getFullYear()) + "-" + pad(date.getMonth() + 1) + "-" + pad(date.getDate()) + " " + pad(date.getHours()) + ":" + pad(date.getMinutes());
3315
+ return formatLocalDateTime(at, { seconds: false }) ?? at;
3240
3316
  }
3241
3317
  /** The text one cell shows, resolved through the seat. */
3242
3318
  function cellText(copy, cell) {
@@ -3285,7 +3361,11 @@ window.__ModuleLoader__.load({
3285
3361
  className: "evo-hist-skill",
3286
3362
  "aria-current": selected ? "true" : void 0,
3287
3363
  onClick: onOpen
3288
- }, cellLine(row.cells.title, copy, "evo-hist-cell-line"), cellLine(row.cells.meta, copy, "evo-hist-cell-line evo-hist-cell-meta"));
3364
+ }, cellLine(row.cells.title, copy, "evo-hist-cell-line"), (() => {
3365
+ const description = row.cells.meta.filter((cell) => cell.key === "description");
3366
+ const facts = row.cells.meta.filter((cell) => cell.key !== "description");
3367
+ return [description.length === 0 ? null : cellLine(description, copy, "evo-hist-cell-line evo-hist-skill-description"), facts.length === 0 ? null : cellLine(facts, copy, "evo-hist-cell-line evo-hist-cell-meta")];
3368
+ })());
3289
3369
  }
3290
3370
  /**
3291
3371
  * One version row: what happened, when, how big — then the controls, then whatever is expanded.
@@ -3313,7 +3393,7 @@ window.__ModuleLoader__.load({
3313
3393
  key: "undo",
3314
3394
  tone: confirming ? "primary" : "quiet",
3315
3395
  onClick: onRestore,
3316
- children: confirming ? copy.t("undo.confirm") : copy.t("undo")
3396
+ children: confirming ? copy.t(row.unitId === void 0 ? "undo.confirm" : "undo.confirmUnit") : copy.t("undo")
3317
3397
  }));
3318
3398
  }
3319
3399
  if (row.undoable === true || row.charsDelta !== void 0) actions.push(button({
@@ -3373,8 +3453,8 @@ window.__ModuleLoader__.load({
3373
3453
  });
3374
3454
  }
3375
3455
  /** The pane's heading and its one control. */
3376
- function panelHead(copy, onRefresh) {
3377
- return (0, react.createElement)("div", { className: "evo-hist-head" }, (0, react.createElement)("h2", { className: "evo-hist-title" }, copy.t("title")), button({
3456
+ function panelHead(copy, onRefresh, title) {
3457
+ return (0, react.createElement)("div", { className: "evo-hist-head" }, (0, react.createElement)("h2", { className: "evo-hist-title" }, title ?? copy.t("title")), button({
3378
3458
  key: "refresh",
3379
3459
  title: copy.t("refresh"),
3380
3460
  onClick: onRefresh,
@@ -3900,7 +3980,7 @@ window.__ModuleLoader__.load({
3900
3980
  };
3901
3981
  return (0, react.createElement)("div", { className: "evo-hist-root" }, (0, react.createElement)("aside", { className: "evo-hist-aside" }, searchField(face, query, setQuery), (0, react.createElement)("div", { className: "evo-hist-list" }, skills.length === 0 ? emptyState(face.t(skillsLoaded ? "empty.skills" : "loading")) : visible.length === 0 ? emptyState(face.t("search.none")) : visible.map((skill) => skillLine(face, skill, skill.name === selected, () => {
3902
3982
  open(skill.name);
3903
- })))), (0, react.createElement)("div", { className: "evo-hist-main" }, panelHead(face, refresh), notice(noteText, noteError), failure === void 0 ? null : (0, react.createElement)("div", { className: "evo-hist-failure" }, failureBlock(face.t, failure)), selected === void 0 ? [emptyState(face.t(skills.length === 0 ? skillsLoaded ? "empty.skills" : "loading" : "empty.pick"), "empty"), (0, react.createElement)("p", {
3983
+ })))), (0, react.createElement)("div", { className: "evo-hist-main" }, panelHead(face, refresh, selected ?? face.t("bodyTitle")), notice(noteText, noteError), failure === void 0 ? null : (0, react.createElement)("div", { className: "evo-hist-failure" }, failureBlock(face.t, failure)), selected === void 0 ? [emptyState(face.t(skills.length === 0 ? skillsLoaded ? "empty.skills" : "loading" : "empty.pick"), "empty"), (0, react.createElement)("p", {
3904
3984
  key: "hint",
3905
3985
  className: "evo-hist-hint"
3906
3986
  }, face.t("hint"))] : loaded ? null : emptyState(versionsFailed === void 0 ? face.t("loading") : `${face.t("empty.versions.failed")} — ${versionsFailed}`), loaded ? (0, react.createElement)("div", null, group(face.t("group.content"), payload.content, void 0), group(face.t("group.support"), payload.support, face.t("group.support.note")), payload.content.length === 0 && payload.support.length === 0 ? (0, react.createElement)("p", { className: "evo-hist-hint" }, face.t("empty.versions")) : null) : null));
@@ -3980,19 +4060,26 @@ window.__ModuleLoader__.load({
3980
4060
  const translate = (key, values) => t(key, values);
3981
4061
  const [state, setState] = (0, react.useState)({ phase: "loading" });
3982
4062
  const [refreshing, setRefreshing] = (0, react.useState)(false);
4063
+ const [refreshFailure, setRefreshFailure] = (0, react.useState)(void 0);
3983
4064
  const [open, setOpen] = (0, react.useState)(false);
3984
4065
  const ticket = (0, react.useRef)(0);
3985
4066
  const read = (keepView) => {
3986
4067
  const current = ticket.current + 1;
3987
4068
  ticket.current = current;
3988
- if (keepView) setRefreshing(true);
3989
- else setState({ phase: "loading" });
4069
+ if (keepView) {
4070
+ setRefreshing(true);
4071
+ setRefreshFailure(void 0);
4072
+ } else setState({ phase: "loading" });
3990
4073
  api.view().then((answer) => {
3991
4074
  if (ticket.current !== current) return;
3992
- setState(answer.ok ? {
3993
- phase: "ready",
3994
- view: answer.data
3995
- } : {
4075
+ if (answer.ok) {
4076
+ setState({
4077
+ phase: "ready",
4078
+ view: answer.data
4079
+ });
4080
+ setRefreshFailure(void 0);
4081
+ } else if (keepView) setRefreshFailure(answer.failure);
4082
+ else setState({
3996
4083
  phase: "failed",
3997
4084
  failure: answer.failure
3998
4085
  });
@@ -4007,6 +4094,7 @@ window.__ModuleLoader__.load({
4007
4094
  read(true);
4008
4095
  };
4009
4096
  const summary = state.phase === "ready" ? summaryOf(state.view.runtime) : void 0;
4097
+ const focusRow = summary === void 0 || summary.status === "ok" || state.phase !== "ready" ? void 0 : state.view.runtime.find((row) => row.status === summary.status);
4010
4098
  const header = (0, react.createElement)("button", {
4011
4099
  type: "button",
4012
4100
  className: "evolution-param-head",
@@ -4017,7 +4105,7 @@ window.__ModuleLoader__.load({
4017
4105
  }, (0, react.createElement)("span", { className: "evolution-param-card-title" }, t("diagTitle")), summary === void 0 ? null : (0, react.createElement)("span", {
4018
4106
  className: "evo-diag-badge",
4019
4107
  "data-status": summary.status
4020
- }, t(SUMMARY_KEYS[summary.status], summary.count === 0 ? void 0 : { n: summary.count })), (0, react.createElement)("span", { className: "evolution-param-chevron" }, open ? "▲" : "▼"));
4108
+ }, t(SUMMARY_KEYS[summary.status], summary.count === 0 ? void 0 : { n: summary.count })), focusRow === void 0 ? null : (0, react.createElement)("span", { "data-role": "diag-focus" }, focusRow.label), (0, react.createElement)("span", { className: "evolution-param-chevron" }, open ? "▲" : "▼"));
4021
4109
  const runtime = (view) => view.runtime.length === 0 ? null : (0, react.createElement)("div", {}, (0, react.createElement)("h3", { className: "evolution-param-group" }, t("diagRuntimeTitle")), (0, react.createElement)("div", { className: "evo-diag-list" }, ...view.runtime.map((row) => (0, react.createElement)("div", {
4022
4110
  key: "runtime-" + row.key,
4023
4111
  className: "evo-diag-item",
@@ -4032,13 +4120,26 @@ window.__ModuleLoader__.load({
4032
4120
  fallback: props.fallback
4033
4121
  });
4034
4122
  const view = state.view;
4035
- return (0, react.createElement)("div", {}, (0, react.createElement)("p", { className: "evolution-param-note" }, t("diagSubtitle")), (0, react.createElement)("div", { className: "evolution-param-actions" }, (0, react.createElement)("button", {
4123
+ return (0, react.createElement)("div", {}, (0, react.createElement)("p", { className: "evolution-param-note" }, t("diagSubtitle")), refreshFailure === void 0 ? null : FailureBlock({
4124
+ failure: refreshFailure,
4125
+ t: translate,
4126
+ fallback: props.fallback
4127
+ }), (0, react.createElement)("div", { className: "evolution-param-actions" }, (0, react.createElement)("button", {
4036
4128
  type: "button",
4037
4129
  className: "evolution-param-button",
4038
4130
  onClick: refresh,
4039
4131
  disabled: refreshing,
4040
4132
  "data-action": "diag-refresh"
4041
- }, t(refreshing ? "diagRefreshing" : "diagRefresh"))), runtime(view), view.runtime.length === 0 ? (0, react.createElement)("p", { className: "evolution-param-note" }, t("diagNoRows")) : null);
4133
+ }, t(refreshing ? "diagRefreshing" : "diagRefresh"))), runtime(view), view.runtime.length === 0 ? (0, react.createElement)("p", { className: "evolution-param-note" }, t("diagNoRows")) : null, (0, react.createElement)("p", {
4134
+ className: "evolution-param-note",
4135
+ "data-role": "diag-purpose"
4136
+ }, t("diagPurpose")), (0, react.createElement)("p", {
4137
+ className: "evolution-param-hint",
4138
+ "data-role": "diag-footer"
4139
+ }, t("diagFooter", {
4140
+ version: String(view.schemaVersion),
4141
+ at: new Date(view.generatedAt).toLocaleString()
4142
+ })));
4042
4143
  };
4043
4144
  return (0, react.createElement)("section", {
4044
4145
  className: "evolution-param-card",
@@ -4061,16 +4162,23 @@ window.__ModuleLoader__.load({
4061
4162
  * @returns the page surface.
4062
4163
  */
4063
4164
  function WorkbenchPage(props) {
4064
- const [selectedSkill, setSelectedSkill] = (0, react.useState)(void 0);
4065
- const [view, setView] = (0, react.useState)("history");
4066
- const [focusRevision, setFocusRevision] = (0, react.useState)(0);
4165
+ const state = (0, react.useSyncExternalStore)(props.store.subscribe, props.store.getState, props.store.getState);
4166
+ const view = state.view;
4167
+ const selectedSkill = state.selectedSkill;
4168
+ const focusRevision = state.focusRevision;
4067
4169
  const openHistory = (skill) => {
4068
- setSelectedSkill(skill);
4069
- setView("history");
4070
- setFocusRevision((value) => value + 1);
4170
+ props.store.openHistory(skill);
4071
4171
  };
4072
4172
  const hasMerge = props.hasView?.("merge") ?? true;
4073
4173
  const hasHealth = props.hasView?.("health") ?? true;
4174
+ (0, react.useEffect)(() => {
4175
+ if (view === "merge" && !hasMerge || view === "health" && !hasHealth) props.store.setView("history");
4176
+ }, [
4177
+ view,
4178
+ hasMerge,
4179
+ hasHealth,
4180
+ props.store
4181
+ ]);
4074
4182
  const viewOrder = [
4075
4183
  "history",
4076
4184
  "pending",
@@ -4084,7 +4192,7 @@ window.__ModuleLoader__.load({
4084
4192
  event.preventDefault();
4085
4193
  const index = viewOrder.indexOf(current);
4086
4194
  const next = event.key === "Home" ? viewOrder[0] : event.key === "End" ? viewOrder[viewOrder.length - 1] : viewOrder[(index + delta + viewOrder.length) % viewOrder.length];
4087
- setView(next);
4195
+ props.store.setView(next);
4088
4196
  requestAnimationFrame(() => {
4089
4197
  document.getElementById("evo-workbench-tab-" + next)?.focus();
4090
4198
  });
@@ -4105,7 +4213,7 @@ window.__ModuleLoader__.load({
4105
4213
  "aria-controls": "evo-workbench-panel",
4106
4214
  "data-active": view === "history" ? "true" : void 0,
4107
4215
  onClick: () => {
4108
- setView("history");
4216
+ props.store.setView("history");
4109
4217
  },
4110
4218
  onKeyDown: (event) => {
4111
4219
  onTabKey(event, "history");
@@ -4119,7 +4227,7 @@ window.__ModuleLoader__.load({
4119
4227
  "aria-controls": "evo-workbench-panel",
4120
4228
  "data-active": view === "pending" ? "true" : void 0,
4121
4229
  onClick: () => {
4122
- setView("pending");
4230
+ props.store.setView("pending");
4123
4231
  },
4124
4232
  onKeyDown: (event) => {
4125
4233
  onTabKey(event, "pending");
@@ -4133,7 +4241,7 @@ window.__ModuleLoader__.load({
4133
4241
  "aria-controls": "evo-workbench-panel",
4134
4242
  "data-active": view === "merge" ? "true" : void 0,
4135
4243
  onClick: () => {
4136
- setView("merge");
4244
+ props.store.setView("merge");
4137
4245
  },
4138
4246
  onKeyDown: (event) => {
4139
4247
  onTabKey(event, "merge");
@@ -4147,7 +4255,7 @@ window.__ModuleLoader__.load({
4147
4255
  "aria-controls": "evo-workbench-panel",
4148
4256
  "data-active": view === "health" ? "true" : void 0,
4149
4257
  onClick: () => {
4150
- setView("health");
4258
+ props.store.setView("health");
4151
4259
  },
4152
4260
  onKeyDown: (event) => {
4153
4261
  onTabKey(event, "health");
@@ -4161,7 +4269,7 @@ window.__ModuleLoader__.load({
4161
4269
  "aria-controls": "evo-workbench-panel",
4162
4270
  "data-active": view === "diagnostics" ? "true" : void 0,
4163
4271
  onClick: () => {
4164
- setView("diagnostics");
4272
+ props.store.setView("diagnostics");
4165
4273
  },
4166
4274
  onKeyDown: (event) => {
4167
4275
  onTabKey(event, "diagnostics");
@@ -4211,12 +4319,51 @@ window.__ModuleLoader__.load({
4211
4319
  ".evo-workbench-title{margin:0;font-size:var(--evo-type-lg);font-weight:600;line-height:var(--evo-leading-base)}",
4212
4320
  ".evo-workbench-body{display:flex;flex:1;min-height:0;min-width:0;overflow:hidden}",
4213
4321
  ".evo-workbench-body>.evolution-param-card{flex:1;min-width:0;overflow:auto}",
4214
- ".evo-workbench-nav{display:flex;width:100%;align-items:center;gap:var(--evo-space-8);padding:var(--evo-space-6) var(--evo-space-12);border:0;border-radius:var(--evo-radius-control);background:transparent;color:var(--dsw-alias-label-secondary);font:inherit;font-size:var(--evo-type-sm);text-align:left;cursor:pointer}",
4215
- ".evo-workbench-nav:hover,.evo-workbench-nav[data-active=\"true\"]{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}",
4216
- ".evo-workbench-nav:focus-visible{outline:var(--evo-focus-ring);outline-offset:var(--evo-focus-offset)}",
4217
4322
  "@media (max-width:720px){.evo-workbench-head{flex-wrap:wrap}.evo-workbench-tabs{flex-wrap:wrap}}"
4218
4323
  ].join("");
4219
4324
  //#endregion
4325
+ //#region src/panels/history/client/WorkbenchStore.ts
4326
+ function createWorkbenchStore(initial = {
4327
+ view: "history",
4328
+ focusRevision: 0
4329
+ }) {
4330
+ let state = initial;
4331
+ const listeners = /* @__PURE__ */ new Set();
4332
+ const emit = (next) => {
4333
+ state = next;
4334
+ for (const listener of listeners) listener();
4335
+ };
4336
+ return {
4337
+ getState: () => state,
4338
+ subscribe(listener) {
4339
+ listeners.add(listener);
4340
+ return () => {
4341
+ listeners.delete(listener);
4342
+ };
4343
+ },
4344
+ setView(view) {
4345
+ if (state.view !== view) emit({
4346
+ ...state,
4347
+ view
4348
+ });
4349
+ },
4350
+ openHistory(skill) {
4351
+ emit({
4352
+ view: "history",
4353
+ ...skill === void 0 ? {} : { selectedSkill: skill },
4354
+ focusRevision: state.focusRevision + 1
4355
+ });
4356
+ },
4357
+ clearSelection() {
4358
+ const { selectedSkill: _selected, ...rest } = state;
4359
+ emit({
4360
+ ...rest,
4361
+ focusRevision: state.focusRevision + 1
4362
+ });
4363
+ }
4364
+ };
4365
+ }
4366
+ //#endregion
4220
4367
  //#region src/panels/history/client/index.ts
4221
4368
  /**
4222
4369
  * Register the locale namespace, the workbench main occupant and its sidebar row.
@@ -4225,6 +4372,9 @@ window.__ModuleLoader__.load({
4225
4372
  function apply$1(ctx) {
4226
4373
  const api = createSkillHistoryApi();
4227
4374
  const settingsSeat = createSettingsSeat(ctx);
4375
+ ctx.effect(() => () => {
4376
+ settingsSeat.dispose?.();
4377
+ }, "evolution-ui: settings seat");
4228
4378
  let approvalConfirmMode = "ask";
4229
4379
  const approvalForm = settingsSeat.get("evolution-approval");
4230
4380
  const stopApprovalWatch = approvalForm?.subscribe(() => {
@@ -4273,6 +4423,7 @@ window.__ModuleLoader__.load({
4273
4423
  api: createDiagnosticsApi((input, init) => fetch(input, init)),
4274
4424
  fallback: FALLBACKS.diagnostics
4275
4425
  });
4426
+ const workbenchStore = createWorkbenchStore();
4276
4427
  registerSidebarPanel(ctx, {
4277
4428
  id: WORKBENCH_ID,
4278
4429
  order: 35,
@@ -4289,6 +4440,7 @@ window.__ModuleLoader__.load({
4289
4440
  component: WorkbenchPage,
4290
4441
  body: (t) => ({
4291
4442
  t,
4443
+ store: workbenchStore,
4292
4444
  history: historyFace(t),
4293
4445
  pending: pendingFace(),
4294
4446
  diagnostics: diagnosticsFace(),
@@ -7,23 +7,20 @@ import { type ReactNode } from 'react';
7
7
  import { type PanelFace } from './Panel.ts';
8
8
  import { type PendingCardFace } from '../../settings/PendingCard.ts';
9
9
  import { type DiagnosticsCardFace } from '../../settings/DiagnosticsCard.ts';
10
+ import type { WorkbenchStore } from './WorkbenchStore.ts';
10
11
  /** Stable page id used by the sidebar row, the page slot key, and navigation params. */
11
12
  export declare const WORKBENCH_ID = "evolution.workbench";
12
13
  /** Injected face: copy plus the history view's data callbacks. */
13
14
  export interface WorkbenchFace {
14
15
  readonly t: (key: string) => string;
16
+ readonly store: WorkbenchStore;
15
17
  readonly history: PanelFace;
16
18
  readonly pending: PendingCardFace;
17
19
  readonly diagnostics: DiagnosticsCardFace;
18
20
  /** Whether a feature view is actually mounted (health is disabled by default). */
19
21
  readonly hasView?: (view: 'merge' | 'health') => boolean;
20
22
  }
21
- /** The two views the workbench owns today. */
22
- export type WorkbenchView = 'history' | 'pending' | 'merge' | 'health' | 'diagnostics';
23
- /** Sidebar row injected face. */
24
- export interface WorkbenchNavFace {
25
- readonly t: (key: string) => string;
26
- }
23
+ export type { WorkbenchView } from './WorkbenchStore.ts';
27
24
  /**
28
25
  * Render the workbench page. The platform keeps it mounted; this component only
29
26
  * consumes a new explicit revision exactly once.
@@ -0,0 +1,16 @@
1
+ /** Registration-owned workbench navigation state. No module-level singleton. */
2
+ export type WorkbenchView = 'history' | 'pending' | 'merge' | 'health' | 'diagnostics';
3
+ export interface WorkbenchState {
4
+ readonly view: WorkbenchView;
5
+ readonly selectedSkill?: string;
6
+ readonly focusRevision: number;
7
+ }
8
+ export interface WorkbenchStore {
9
+ getState(): WorkbenchState;
10
+ subscribe(listener: () => void): () => void;
11
+ setView(view: WorkbenchView): void;
12
+ openHistory(skill?: string): void;
13
+ clearSelection(): void;
14
+ }
15
+ export declare function createWorkbenchStore(initial?: WorkbenchState): WorkbenchStore;
16
+ //# sourceMappingURL=WorkbenchStore.d.ts.map
@@ -72,5 +72,5 @@ export declare function emptyState(text: string, key?: string): ReactNode;
72
72
  /** One notice about what just happened. */
73
73
  export declare function notice(text: string | undefined, error: boolean): ReactNode;
74
74
  /** The pane's heading and its one control. */
75
- export declare function panelHead(copy: Copy, onRefresh: () => void): ReactNode;
75
+ export declare function panelHead(copy: Copy, onRefresh: () => void, title?: string): ReactNode;
76
76
  //# sourceMappingURL=chrome.d.ts.map
@@ -5,7 +5,7 @@
5
5
  * description, so the settings surface can state both without a hand-written copy.
6
6
  */
7
7
  /** The family version this surface belongs to. */
8
- export declare const FAMILY_VERSION = "0.33.0";
8
+ export declare const FAMILY_VERSION = "0.35.0";
9
9
  /** What this surface is for, in one line. */
10
10
  export declare const PACKAGE_PURPOSE = "Web UI umbrella for the evolution family: settings and skill-history panels";
11
11
  //# sourceMappingURL=generated-about.d.ts.map
@@ -23,12 +23,12 @@ export declare const zh: {
23
23
  readonly projectionFailed: "这一块读设置时出错,已记入日志;其它参数不受影响";
24
24
  readonly readonly: "本次会话不允许写入设置(临时运行模式)";
25
25
  readonly apply: "保存";
26
- readonly reset: "恢复默认值";
26
+ readonly reset: "复位";
27
27
  readonly empty: "这里没有可改项";
28
28
  readonly changed: "{n} 项已覆盖";
29
29
  readonly discard: "放弃修改";
30
30
  readonly saving: "保存中…";
31
- readonly resetting: "恢复中…";
31
+ readonly resetting: "复位中…";
32
32
  readonly writeTargetHost: "保存去向:宿主配置(对所有会话生效)";
33
33
  readonly invalidNumber: "需要一个数字";
34
34
  readonly invalidSelect: "不在允许的取值里";
@@ -40,6 +40,7 @@ export declare const zh: {
40
40
  readonly resetTo: "复位为 {value}";
41
41
  readonly saved: "已保存。";
42
42
  readonly saveReadbackFailed: "写入已被接受,但回读失败;点刷新重读后再确认。";
43
+ readonly readbackStale: "显示的是最后一次成功读取的值;当前回读失败,刷新后可能变化。";
43
44
  readonly saveConflict: "这个值没有被接受:设置已被其他页面修改。请重读后再保存。";
44
45
  readonly resetDone: "已恢复默认值。";
45
46
  readonly refused: "这个值没有被接受(界面上保持原样)。想看原因:在对话里让它说明。";
@@ -108,6 +109,8 @@ export declare const zh: {
108
109
  readonly diagStatusUnknown: "未知";
109
110
  readonly diagRemedy: "建议:";
110
111
  readonly diagNoRows: "没有可显示的运行状态";
112
+ readonly diagPurpose: "用途:检查插件挂载、平台服务和数据面是否正常,不修改任何状态。";
113
+ readonly diagFooter: "诊断 schema v{version} · 生成于 {at}";
111
114
  readonly stateLoading: "正在读取…";
112
115
  readonly failureUnauthorized: "需要登录:重新打开这个页面完成登录后再试。";
113
116
  readonly failureForbidden: "平台拒绝了这个请求(身份不是 owner)。";
@@ -45,7 +45,9 @@ export interface ParamSectionSnapshot {
45
45
  * all, or the seat is there and serves no such row. The two read differently to the
46
46
  * operator (one is the UI's own gap, the other is a row that is not composed).
47
47
  */
48
- reason?: 'seat-missing' | 'not-served' | 'projection-failed';
48
+ reason?: 'seat-missing' | 'not-served' | 'projection-failed' | 'readback-failed';
49
+ /** Last-known values are still shown; the row could not be re-read. */
50
+ stale?: boolean;
49
51
  }
50
52
  /** The reactive source the renderer binds to a `use<Name>` seat. */
51
53
  export interface ParamSectionSource {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@lmzhen/dsh-evolution-ui",
3
3
  "description": "Web UI umbrella for the evolution family: settings and skill-history panels (community build)",
4
- "version": "0.33.0",
4
+ "version": "0.35.0",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },
@@ -45,9 +45,9 @@
45
45
  "react": "^18.2.0"
46
46
  },
47
47
  "dependencies": {
48
- "@lmzhen/dsh-evolution-client-kit": "^0.33.0",
49
- "@lmzhen/dsh-evolution-core": "^0.33.0",
50
- "@lmzhen/dsh-evolution-curator": "^0.33.0"
48
+ "@lmzhen/dsh-evolution-client-kit": "^0.35.0",
49
+ "@lmzhen/dsh-evolution-core": "^0.35.0",
50
+ "@lmzhen/dsh-evolution-curator": "^0.35.0"
51
51
  },
52
52
  "devDependencies": {
53
53
  "@types/react": "~18.3.1",