@lmzhen/dsh-evolution-merge-ui 0.32.5 → 0.33.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,19 +1,7 @@
1
1
  # @lmzhen/dsh-evolution-merge-ui
2
2
 
3
- > 合并面板:看待批队列、A/B 正文和历史。
3
+ Skill-merge view inside the shared evolution workbench. It ships only in the `all` installation form.
4
4
 
5
- ## 你会看到
5
+ The view is read-only: action buttons copy the appropriate `/evolution merge` or `/evolution doctor` command; they do not approve, execute, or undo a merge in the browser. Internal run fields (run id, stage, parent, result ref) live under the view's run-details disclosure so the queue and decision facts come first.
6
6
 
7
- 左侧栏「技能合并」;动作按钮复制命令。
8
-
9
- ## 什么时候会用到
10
-
11
- 你想看合并提议/历史时。
12
-
13
- ## 默认 / 前提
14
-
15
- 只有 all 安装形态带这个面板。
16
-
17
- ## 边界
18
-
19
- 面板只读;实际合并用命令。
7
+ The host entry intentionally registers nothing. The Loader discovers the `dsh.client` manifest and serves the browser bundle, which registers itself into `evolution.workbench.view` and uses the shared client-kit lifecycle and the four merge read routes.
package/lib/client.js CHANGED
@@ -5,28 +5,24 @@ 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 ../evolution-client-kit/src/panel.ts
9
- function interpolate(text, values) {
10
- if (values === void 0) return text;
11
- return text.replace(/\{(\w+)\}/g, (_match, key) => String(values[key] ?? ""));
12
- }
8
+ //#region ../evolution-client-kit/src/workbench-view.ts
13
9
  function injectStyles(doc, css, tagId) {
14
10
  if (doc === void 0) return () => {};
15
11
  doc.querySelector("style[data-plugin-css=\"" + tagId + "\"]")?.remove();
16
12
  const tag = doc.createElement("style");
17
13
  tag.setAttribute("data-plugin-css", tagId);
18
14
  tag.textContent = css;
19
- doc.head.append(tag);
15
+ doc.head.appendChild(tag);
20
16
  return () => {
21
17
  tag.remove();
22
18
  };
23
19
  }
24
20
  /**
25
- * Register one sidebar row and its main body under one lifecycle.
26
- * @param ctx - the client context carrying the slots and locale seats.
27
- * @param spec - the feature-specific row/body descriptor.
21
+ * Register one view into `evolution.workbench.view`.
22
+ * @param ctx - client cordis context.
23
+ * @param spec - view identity, copy, styles, component and face factory.
28
24
  */
29
- function registerSidebarPanel(ctx, spec) {
25
+ function registerWorkbenchView(ctx, spec) {
30
26
  const seam = ctx;
31
27
  let registered = false;
32
28
  ctx.effect(() => {
@@ -35,12 +31,12 @@ window.__ModuleLoader__.load({
35
31
  registered = true;
36
32
  return dispose;
37
33
  } catch (error) {
38
- ctx.logger.warn(spec.namespace + ": the locale seat refused the dictionary (" + String(error) + "); the panel falls back to the bundle copy");
34
+ ctx.logger.warn(spec.namespace + ": locale refused (" + String(error) + "); the view falls back to the bundle copy");
39
35
  return () => {};
40
36
  }
41
37
  }, spec.namespace + ": locale");
42
38
  ctx.effect(() => injectStyles(typeof document === "undefined" ? void 0 : document, spec.styles.css, spec.styles.tagId), spec.namespace + ": styles");
43
- const fallback = spec.dictionaries[spec.fallback ?? "zh"];
39
+ const fallback = spec.dictionaries.zh;
44
40
  const lookup = (key) => {
45
41
  const own = fallback[key] ?? key;
46
42
  if (!registered) return own;
@@ -50,47 +46,36 @@ window.__ModuleLoader__.load({
50
46
  return own;
51
47
  }
52
48
  };
53
- const t = (key, values) => interpolate(lookup(key), values);
54
- const label = () => lookup("entry.label");
55
- seam.slots.inject("sidebar.panellist", function* () {
49
+ const t = (key, values) => {
50
+ const template = lookup(key);
51
+ if (values === void 0) return template;
52
+ return template.replace(/\{(\w+)\}/g, (match, slot) => values[slot] === void 0 ? match : String(values[slot]));
53
+ };
54
+ seam.slots.inject("evolution.workbench.view", function* () {
56
55
  yield seam.slots.register({
57
- name: "sidebar.panellist",
56
+ name: "evolution.workbench.view",
58
57
  id: spec.id,
59
58
  order: spec.order,
60
- label
61
- }, spec.icon);
62
- yield seam.slots.register({
63
- name: "main",
64
- key: spec.id,
65
- inject: () => spec.body(t)
59
+ label: () => t("entry.label"),
60
+ inject: () => spec.face(t)
66
61
  }, spec.component);
67
62
  });
68
63
  }
69
64
  //#endregion
70
- //#region ../evolution-client-kit/src/workspace.ts
65
+ //#region ../evolution-client-kit/src/generated/routes.generated.ts
71
66
  /**
72
- * Best-effort workspace id; a seat that is absent or refuses leaves the panel in local scope.
73
- * @param ctx - the client context.
74
- * @returns the current workspace id, or undefined.
67
+ * Every family host path, generated from evolution-core's HOST_ROUTES (the ONE declaration).
68
+ * A browser half cannot import the host package at runtime (client bundles keep @deepseek-ai/*
69
+ * external), so the table is mirrored here mechanically; a rename in core regenerates all three
70
+ * client copies, and the generator's --check makes a stale copy red.
75
71
  */
76
- function panelWorkspaceId(ctx) {
77
- const get = ctx.get;
78
- if (typeof get !== "function") return void 0;
79
- for (const name of ["workspace", "uiWorkspace"]) try {
80
- const seat = get.call(ctx, name);
81
- const id = seat?.workspaceId ?? seat?.currentWorkspaceId;
82
- if (typeof id === "string" && id.trim() !== "") return id;
83
- } catch {}
84
- }
85
- //#endregion
86
- //#region src/client/api.ts
87
- /** Read-only merge wire client. Classifies every failure and never writes. */
88
- const HOST_ROUTES = {
72
+ const ROUTES = {
89
73
  approval: {
90
74
  pending: "/api/dsh-evolution/approval/pending",
91
75
  preview: "/api/dsh-evolution/approval/preview",
92
76
  approve: "/api/dsh-evolution/approval/approve",
93
- reject: "/api/dsh-evolution/approval/reject"
77
+ reject: "/api/dsh-evolution/approval/reject",
78
+ release: "/api/dsh-evolution/approval/release"
94
79
  },
95
80
  "skill-history": {
96
81
  skills: "/api/dsh-evolution/skill-history/skills",
@@ -109,8 +94,166 @@ window.__ModuleLoader__.load({
109
94
  body: "/api/dsh-evolution/merge/body",
110
95
  history: "/api/dsh-evolution/merge/history"
111
96
  },
112
- diagnostics: { view: "/api/dsh-evolution/diagnostics" }
113
- }.merge;
97
+ diagnostics: { view: "/api/dsh-evolution/diagnostics" },
98
+ settings: {
99
+ describe: "/api/dsh-evolution/settings/describe",
100
+ mutate: "/api/dsh-evolution/settings/mutate"
101
+ }
102
+ };
103
+ //#endregion
104
+ //#region ../evolution-client-kit/src/settings-seat.ts
105
+ const SETTINGS_DESCRIBE = ROUTES.settings.describe;
106
+ const SETTINGS_MUTATE = ROUTES.settings.mutate;
107
+ const EMPTY = Object.freeze({
108
+ status: "unavailable",
109
+ value: void 0,
110
+ user: void 0,
111
+ writable: false,
112
+ base: void 0,
113
+ revision: void 0,
114
+ reason: "transport"
115
+ });
116
+ function snapshotFrom(value, ns) {
117
+ if (typeof value !== "object" || value === null) return EMPTY;
118
+ const list = value.namespaces;
119
+ if (!Array.isArray(list)) return EMPTY;
120
+ const item = list.find((candidate) => typeof candidate === "object" && candidate !== null && candidate.ns === ns);
121
+ return item === void 0 ? {
122
+ ...EMPTY,
123
+ reason: "not-served"
124
+ } : item;
125
+ }
126
+ async function describe(namespace) {
127
+ try {
128
+ const response = await fetch(SETTINGS_DESCRIBE, { headers: { accept: "application/json" } });
129
+ if (!response.ok) return EMPTY;
130
+ const body = await response.json();
131
+ if (body.ok !== true) return EMPTY;
132
+ return snapshotFrom(body.data, namespace);
133
+ } catch {
134
+ return EMPTY;
135
+ }
136
+ }
137
+ async function write(namespace, ops, expectedRevision) {
138
+ try {
139
+ const response = await fetch(SETTINGS_MUTATE, {
140
+ method: "POST",
141
+ headers: {
142
+ "content-type": "application/json",
143
+ accept: "application/json"
144
+ },
145
+ body: JSON.stringify({
146
+ ns: namespace,
147
+ ops,
148
+ ...expectedRevision === void 0 ? {} : { expectedRevision }
149
+ })
150
+ });
151
+ if (!response.ok) return { ok: false };
152
+ const body = await response.json();
153
+ return {
154
+ ok: body.ok === true,
155
+ snapshot: snapshotFrom(body.data, namespace)
156
+ };
157
+ } catch {
158
+ return { ok: false };
159
+ }
160
+ }
161
+ function createForm(namespace, ctx) {
162
+ let snapshot = {
163
+ ...EMPTY,
164
+ reason: "seat-missing"
165
+ };
166
+ const listeners = /* @__PURE__ */ new Set();
167
+ let started = false;
168
+ let listening = false;
169
+ const notify = () => {
170
+ for (const listener of listeners) listener();
171
+ };
172
+ const refresh = async () => {
173
+ snapshot = await describe(namespace);
174
+ notify();
175
+ };
176
+ const start = () => {
177
+ if (started) return;
178
+ started = true;
179
+ if (!listening) {
180
+ listening = true;
181
+ (ctx.get?.("remote"))?.$on?.("settings/document-updated", (changed) => {
182
+ if (changed === namespace) refresh();
183
+ });
184
+ }
185
+ refresh();
186
+ };
187
+ const form = {
188
+ getSnapshot: () => snapshot,
189
+ subscribe: (listener) => {
190
+ listeners.add(listener);
191
+ start();
192
+ return () => {
193
+ listeners.delete(listener);
194
+ };
195
+ },
196
+ set: async (field, value) => await form.mutate([{
197
+ op: "set",
198
+ path: [field],
199
+ value
200
+ }]),
201
+ unset: async (field) => await form.mutate([{
202
+ op: "unset",
203
+ path: [field]
204
+ }]),
205
+ mutate: async (ops, expectedRevision) => {
206
+ const outcome = await write(namespace, ops, expectedRevision ?? snapshot.revision);
207
+ if (outcome.snapshot !== void 0) {
208
+ snapshot = outcome.snapshot;
209
+ notify();
210
+ } else await refresh();
211
+ return outcome.ok;
212
+ }
213
+ };
214
+ return form;
215
+ }
216
+ /**
217
+ * Build one cached form per family namespace over the family settings routes.
218
+ * @param ctx - the client plugin context (remote subscription is optional).
219
+ * @returns the seat; each namespace is served by /api/dsh-evolution/settings/*.
220
+ */
221
+ const seats = /* @__PURE__ */ new WeakMap();
222
+ function createSettingsSeat(ctx) {
223
+ const cached = seats.get(ctx);
224
+ if (cached !== void 0) return cached;
225
+ const forms = /* @__PURE__ */ new Map();
226
+ const seat = { get(namespace) {
227
+ let form = forms.get(namespace);
228
+ if (form === void 0) {
229
+ form = createForm(namespace, ctx);
230
+ forms.set(namespace, form);
231
+ }
232
+ return form;
233
+ } };
234
+ seats.set(ctx, seat);
235
+ return seat;
236
+ }
237
+ //#endregion
238
+ //#region ../evolution-client-kit/src/workspace.ts
239
+ /**
240
+ * Best-effort workspace id; a seat that is absent or refuses leaves the panel in local scope.
241
+ * @param ctx - the client context.
242
+ * @returns the current workspace id, or undefined.
243
+ */
244
+ function panelWorkspaceId(ctx) {
245
+ const get = ctx.get;
246
+ if (typeof get !== "function") return void 0;
247
+ for (const name of ["workspace", "uiWorkspace"]) try {
248
+ const seat = get.call(ctx, name);
249
+ const id = seat?.workspaceId ?? seat?.currentWorkspaceId;
250
+ if (typeof id === "string" && id.trim() !== "") return id;
251
+ } catch {}
252
+ }
253
+ //#endregion
254
+ //#region src/client/api.ts
255
+ /** Read-only merge wire client. Classifies every failure and never writes. */
256
+ const HOST_ROUTES = ROUTES.merge;
114
257
  const isBlocked = (failure) => failure.kind === "blocked";
115
258
  const asReadiness = (value) => {
116
259
  const raw = typeof value === "object" && value !== null ? value : {};
@@ -348,7 +491,7 @@ window.__ModuleLoader__.load({
348
491
  startCommands: facts.startCommands,
349
492
  notes: [...facts.notes],
350
493
  degraded: facts.degraded,
351
- ...facts.last === null && facts.active === null ? {} : { latestRun: toRun(facts.last ?? facts.active) },
494
+ ...facts.active === null && facts.last === null ? {} : { latestRun: toRun(facts.active ?? facts.last) },
352
495
  approval: {
353
496
  mergeApproval: facts.approval.mergeApproval,
354
497
  ...platformWrite === void 0 ? {} : { platformWrite }
@@ -426,6 +569,7 @@ window.__ModuleLoader__.load({
426
569
  "start.pair": "复制配对命令模板",
427
570
  "degraded": "读模型降级:",
428
571
  "copied": "已复制:",
572
+ "copyFailed": "复制失败:浏览器没有接受剪贴板写入。",
429
573
  "row.noSummary": "无摘要",
430
574
  "overlay.body": "正文",
431
575
  "hint": "手动触发;后台逐对判定。这里只读,不写技能。",
@@ -441,6 +585,7 @@ window.__ModuleLoader__.load({
441
585
  "empty.fuse": "没有待合并的对。",
442
586
  "empty.skipped": "没有标记为无需合并的对。",
443
587
  "loading": "正在读取…",
588
+ "refreshing": "刷新中,下面仍是上次结果…",
444
589
  "refresh": "刷新",
445
590
  "loadMore": "加载更多",
446
591
  "approval.on": "审批开:合并前需人工批准。",
@@ -489,13 +634,13 @@ window.__ModuleLoader__.load({
489
634
  "skipKind.forced": "强制",
490
635
  "action.approve": "复制批准",
491
636
  "action.reject": "复制拒绝",
492
- "action.retry-candidates": "重试候选",
637
+ "action.retry-candidates": "复制重试候选",
493
638
  "action.retry-resume": "重试续跑",
494
639
  "action.discard": "复制丢弃",
495
- "action.restore": "恢复",
640
+ "action.restore": "复制恢复命令",
496
641
  "action.pair": "复制配对",
497
- "action.status": "状态",
498
- "action.report": "报告",
642
+ "action.status": "复制状态命令",
643
+ "action.report": "复制报告命令",
499
644
  "action.cancel": "取消",
500
645
  "action.confirm": "确认复制",
501
646
  "disabled.expired": "已过期,不可用",
@@ -505,6 +650,7 @@ window.__ModuleLoader__.load({
505
650
  "disabled.owned": "已有运行",
506
651
  "disabled.unavailable": "暂不可用",
507
652
  "history.empty": "没有历史。",
653
+ "history.archivedAt": "归档于 {date}",
508
654
  "failureServer": "读取失败",
509
655
  "failureUnauthorized": "未登录",
510
656
  "failureForbidden": "无权访问",
@@ -540,6 +686,7 @@ window.__ModuleLoader__.load({
540
686
  "stage.state.absent": "无暂存",
541
687
  "origin.system": "系统",
542
688
  "row.id": "编号",
689
+ "row.details": "详情",
543
690
  "row.requestedTarget": "请求目标",
544
691
  "row.stage": "合并暂存",
545
692
  "row.summaryA": "摘要 A",
@@ -582,6 +729,7 @@ window.__ModuleLoader__.load({
582
729
  "settings.capabilityMissing": "宿主未提供该能力",
583
730
  "settings.capabilityDisabled": "当前部署不可用",
584
731
  "settings.readonly": "只读显示;修改请到平台设置页。",
732
+ "settings.mergeApprovalPath": "设置 → 插件 → 自进化 → 技能自动合并",
585
733
  "run.title": "运行",
586
734
  "run.id": "运行编号",
587
735
  "run.state": "状态",
@@ -592,6 +740,7 @@ window.__ModuleLoader__.load({
592
740
  "run.currentPair": "当前对",
593
741
  "run.parent": "父运行",
594
742
  "run.report": "报告",
743
+ "run.details": "运行详情",
595
744
  "run.noReport": "暂无报告",
596
745
  "run.resultRef": "报告位置",
597
746
  "run.actions": "运行操作",
@@ -609,7 +758,7 @@ window.__ModuleLoader__.load({
609
758
  "error.title": "读取失败",
610
759
  "error.retry": "重试",
611
760
  "error.raw": "原始信息",
612
- "doctor.open": "打开全局诊断",
761
+ "doctor.open": "复制诊断命令",
613
762
  "sort.updatedAt": "按更新时间",
614
763
  "sort.score": "按分数",
615
764
  "pin": "置顶",
@@ -622,6 +771,7 @@ window.__ModuleLoader__.load({
622
771
  "start.pair": "Copy pair command template",
623
772
  "degraded": "Degraded read model:",
624
773
  "copied": "Copied:",
774
+ "copyFailed": "Copy failed: the browser did not accept the clipboard write.",
625
775
  "row.noSummary": "No summary",
626
776
  "overlay.body": "Body",
627
777
  "hint": "Manual only; pairs are judged in the background. Read-only here.",
@@ -637,6 +787,7 @@ window.__ModuleLoader__.load({
637
787
  "empty.fuse": "No pairs ready to merge.",
638
788
  "empty.skipped": "No pairs marked as not worth merging.",
639
789
  "loading": "Loading…",
790
+ "refreshing": "Refreshing; the rows below are the previous result…",
640
791
  "refresh": "Refresh",
641
792
  "loadMore": "Load more",
642
793
  "approval.on": "Approval on: merges wait for a human.",
@@ -685,13 +836,13 @@ window.__ModuleLoader__.load({
685
836
  "skipKind.forced": "forced",
686
837
  "action.approve": "Copy approve",
687
838
  "action.reject": "Copy reject",
688
- "action.retry-candidates": "Retry candidates",
839
+ "action.retry-candidates": "Copy retry candidates",
689
840
  "action.retry-resume": "Retry resume",
690
841
  "action.discard": "Copy discard",
691
- "action.restore": "Restore",
842
+ "action.restore": "Copy restore command",
692
843
  "action.pair": "Copy pair",
693
- "action.status": "Status",
694
- "action.report": "Report",
844
+ "action.status": "Copy status command",
845
+ "action.report": "Copy report command",
695
846
  "action.cancel": "Cancel",
696
847
  "action.confirm": "Copy confirmation",
697
848
  "disabled.expired": "expired",
@@ -701,6 +852,7 @@ window.__ModuleLoader__.load({
701
852
  "disabled.owned": "already running",
702
853
  "disabled.unavailable": "unavailable",
703
854
  "history.empty": "No history.",
855
+ "history.archivedAt": "Archived {date}",
704
856
  "failureServer": "Read failed",
705
857
  "failureUnauthorized": "Not signed in",
706
858
  "failureForbidden": "Forbidden",
@@ -736,6 +888,7 @@ window.__ModuleLoader__.load({
736
888
  "stage.state.absent": "Not staged",
737
889
  "origin.system": "System",
738
890
  "row.id": "ID",
891
+ "row.details": "Details",
739
892
  "row.requestedTarget": "Requested target",
740
893
  "row.stage": "Merge staging",
741
894
  "row.summaryA": "Summary A",
@@ -778,6 +931,7 @@ window.__ModuleLoader__.load({
778
931
  "settings.capabilityMissing": "Host did not provide this capability",
779
932
  "settings.capabilityDisabled": "Not available in this deployment",
780
933
  "settings.readonly": "Read-only here; edit in platform settings.",
934
+ "settings.mergeApprovalPath": "Settings → Plugins → Evolution → Skill auto-merge",
781
935
  "run.title": "Run",
782
936
  "run.id": "Run ID",
783
937
  "run.state": "State",
@@ -788,6 +942,7 @@ window.__ModuleLoader__.load({
788
942
  "run.currentPair": "Current pair",
789
943
  "run.parent": "Parent run",
790
944
  "run.report": "Report",
945
+ "run.details": "Run details",
791
946
  "run.noReport": "No report yet",
792
947
  "run.resultRef": "Report location",
793
948
  "run.actions": "Run actions",
@@ -805,7 +960,7 @@ window.__ModuleLoader__.load({
805
960
  "error.title": "Read failed",
806
961
  "error.retry": "Retry",
807
962
  "error.raw": "Raw message",
808
- "doctor.open": "Open global diagnostics",
963
+ "doctor.open": "Copy diagnostics command",
809
964
  "sort.updatedAt": "Last updated",
810
965
  "sort.score": "Score",
811
966
  "pin": "Pin",
@@ -815,12 +970,14 @@ window.__ModuleLoader__.load({
815
970
  "settings.saved": "已保存",
816
971
  "settings.saveFailed": "保存失败",
817
972
  "settings.openPlatform": "打开平台设置",
973
+ "settings.openPlatformPath": "设置 → 内置插件 → 插件配置 → 技能自动合并",
818
974
  "settings.live": "可直接在此保存;platformWrite 能力来自宿主。"
819
975
  });
820
976
  Object.assign(en, {
821
977
  "settings.saved": "Saved",
822
978
  "settings.saveFailed": "Save failed",
823
979
  "settings.openPlatform": "Open platform settings",
980
+ "settings.openPlatformPath": "Settings → Built-in plugins → Plugin configuration → Skill auto-merge",
824
981
  "settings.live": "Save directly here; platformWrite capability comes from the host."
825
982
  });
826
983
  Object.assign(zh, { "settings.platformWriteLinkHint": "此开关属于平台设置,这里只读;请到平台设置修改。" });
@@ -850,7 +1007,7 @@ window.__ModuleLoader__.load({
850
1007
  ".evolution-merge-panel [data-role=\"stat\"] { border:var(--evo-hairline-width) solid var(--dsw-alias-border-l2); border-radius:var(--evo-radius-control); padding:var(--evo-space-6) var(--evo-space-10); min-width:calc(var(--evo-space-16) * 4); }",
851
1008
  ".evolution-merge-panel [data-role=\"stat-value\"] { font-size:var(--evo-type-lg); font-weight:600; }",
852
1009
  ".evolution-merge-panel [data-role=\"stat-label\"] { font-size:var(--evo-type-xs); color:var(--dsw-alias-label-tertiary); }",
853
- ".evolution-merge-panel [data-role=\"tabs\"] { display:flex; gap:var(--evo-space-6); border-bottom:var(--evo-hairline-width) solid var(--dsw-alias-border-l2); }",
1010
+ ".evolution-merge-panel [data-role=\"tabs\"] { display:flex; flex-wrap:wrap; gap:var(--evo-space-6); border-bottom:var(--evo-hairline-width) solid var(--dsw-alias-border-l2); }",
854
1011
  ".evolution-merge-panel [data-role=\"tab\"] { background:none; border:0; border-bottom:var(--evo-hairline-width) solid transparent; padding:var(--evo-space-6) var(--evo-space-8); cursor:pointer; color:inherit; }",
855
1012
  ".evolution-merge-panel [data-role=\"tab\"][data-active=\"true\"] { border-bottom-color:var(--dsw-alias-brand-primary); }",
856
1013
  ".evolution-merge-panel button:focus-visible { outline:2px solid currentColor !important; outline-offset:2px; box-shadow:0 0 0 2px currentColor !important; }",
@@ -862,6 +1019,13 @@ window.__ModuleLoader__.load({
862
1019
  ".evolution-merge-panel [data-role=\"failure\"] [data-tone=\"fail\"] { color:var(--dsw-alias-state-error-primary); }",
863
1020
  ".evolution-merge-panel [data-role=\"fallback\"] { display:flex; gap:var(--evo-space-6); flex-wrap:wrap; }",
864
1021
  ".evolution-merge-panel [data-role=\"overlay\"] { position:fixed; z-index:60; top:8vh; left:50%; transform:translateX(-50%); width:min(820px, calc(100vw - 64px)); max-height:78vh; overflow:auto; display:flex; flex-direction:column; gap:var(--evo-space-8); padding:var(--evo-space-12); border:var(--evo-hairline-width) solid var(--dsw-alias-border-l2); border-radius:var(--evo-radius-control); background:var(--dsw-alias-bg-layer-1); box-shadow:0 0 var(--evo-space-8) var(--dsw-alias-border-l2); }",
1022
+ ".evo-merge-modal { width:min(820px, calc(100vw - 48px)); max-width:calc(100vw - 48px); max-height:min(78vh, 720px); }",
1023
+ ".evo-merge-modal [data-role=\"overlay\"][data-platform-modal] { position:static; z-index:auto; top:auto; left:auto; transform:none; width:auto; box-sizing:border-box; max-height:none; overflow:auto; padding:var(--evo-space-20); border:0; border-radius:0; background:transparent; box-shadow:none; gap:var(--evo-space-8); }",
1024
+ ".evo-merge-modal [data-role=\"overlay-head\"] { display:flex; align-items:center; justify-content:space-between; gap:var(--evo-space-8); }",
1025
+ ".evo-merge-modal [data-role=\"overlay-title\"] { margin:0; font-size:var(--evo-type-lg); }",
1026
+ ".evo-merge-modal [data-role=\"body-text\"] { margin:0; max-height:52vh; overflow:auto; white-space:pre-wrap; }",
1027
+ ".evo-merge-modal [data-role=\"history-list\"] { margin:0; padding-left:var(--evo-space-20); }",
1028
+ ".evo-merge-modal [data-role=\"history-item\"] { padding:var(--evo-space-4) 0; }",
865
1029
  ".evolution-merge-panel [data-role=\"overlay-head\"] { display:flex; align-items:center; justify-content:space-between; gap:var(--evo-space-8); }",
866
1030
  ".evolution-merge-panel [data-role=\"overlay-title\"] { margin:0; font-size:var(--evo-type-lg); }",
867
1031
  ".evolution-merge-panel [data-role=\"rows\"] { display:flex; flex-direction:column; gap:var(--evo-space-6); }",
@@ -902,6 +1066,25 @@ window.__ModuleLoader__.load({
902
1066
  ".evolution-merge-panel [data-role=\"refresh\"] { align-self:flex-start; }"
903
1067
  ].join("\n");
904
1068
  //#endregion
1069
+ //#region src/client/platform-modal.ts
1070
+ const PLATFORM_UI_MODULE$1 = "@deepseek-ai/dsh-client-ui-primitives";
1071
+ let cached;
1072
+ /** The platform Modal, or undefined in a non-browser/SSR render or a deployment without the row. */
1073
+ function platformModal() {
1074
+ if (cached !== void 0) return cached ?? void 0;
1075
+ if (typeof window === "undefined") {
1076
+ cached = null;
1077
+ return;
1078
+ }
1079
+ try {
1080
+ const row = require(PLATFORM_UI_MODULE$1);
1081
+ cached = typeof row.Modal === "function" ? row.Modal : null;
1082
+ } catch {
1083
+ cached = null;
1084
+ }
1085
+ return cached ?? void 0;
1086
+ }
1087
+ //#endregion
905
1088
  //#region src/client/platform-menu.ts
906
1089
  /**
907
1090
  * The one seam onto the platform's menu material and rows.
@@ -1061,13 +1244,6 @@ window.__ModuleLoader__.load({
1061
1244
  //#endregion
1062
1245
  //#region src/client/Panel.ts
1063
1246
  /** Batch 4 merge panel: keyed read store, four tabs, local overlays, localized errors. */
1064
- async function saveSetting(form, field, value) {
1065
- try {
1066
- return await form.set(field, value);
1067
- } catch {
1068
- return false;
1069
- }
1070
- }
1071
1247
  const QUEUES = [
1072
1248
  "verify",
1073
1249
  "pending",
@@ -1171,19 +1347,43 @@ window.__ModuleLoader__.load({
1171
1347
  if (run === void 0) return null;
1172
1348
  const report = run.actions.find((action) => action.id === "report");
1173
1349
  const reportEnabled = report?.enabled === true;
1350
+ const internals = [
1351
+ (0, react.createElement)("span", {
1352
+ key: "id",
1353
+ "data-role": "run-id",
1354
+ title: run.id
1355
+ }, t("run.id") + " " + run.id),
1356
+ run.stage === void 0 ? null : (0, react.createElement)("span", {
1357
+ key: "stage",
1358
+ "data-role": "run-stage"
1359
+ }, t("stage." + run.stage)),
1360
+ run.endedAt === void 0 ? null : (0, react.createElement)("span", {
1361
+ key: "ended",
1362
+ "data-role": "run-ended"
1363
+ }, t("run.endedAt") + " " + new Date(run.endedAt).toLocaleString()),
1364
+ run.failure === void 0 ? null : (0, react.createElement)("span", {
1365
+ key: "failure",
1366
+ "data-role": "run-failure",
1367
+ title: run.failure.detail
1368
+ }, t("run.failure") + " " + run.failure.code),
1369
+ run.currentPairKey === void 0 ? null : (0, react.createElement)("span", {
1370
+ key: "pair",
1371
+ "data-role": "run-current-pair"
1372
+ }, t("run.currentPair") + " " + run.currentPairKey),
1373
+ run.parentRunId === void 0 ? null : (0, react.createElement)("span", {
1374
+ key: "parent",
1375
+ "data-role": "run-parent"
1376
+ }, t("run.parent") + " " + run.parentRunId),
1377
+ run.resultRef === void 0 ? null : (0, react.createElement)("span", {
1378
+ key: "ref",
1379
+ "data-role": "run-result-ref",
1380
+ title: run.resultRef
1381
+ }, t("run.resultRef"))
1382
+ ];
1174
1383
  return (0, react.createElement)("div", {
1175
1384
  "data-role": "run-strip",
1176
1385
  "data-run-state": run.state
1177
- }, (0, react.createElement)("span", {
1178
- "data-role": "run-id",
1179
- title: run.id
1180
- }, t("run.id") + " " + run.id), (0, react.createElement)("span", { "data-role": "run-state" }, t("run.state") + " " + t("run." + run.state)), run.stage === void 0 ? null : (0, react.createElement)("span", { "data-role": "run-stage" }, t("stage." + run.stage)), run.progress === null || run.progress === void 0 ? null : (0, react.createElement)("span", { "data-role": "run-progress" }, t("run.progress") + " " + String(run.progress.done) + "/" + String(run.progress.total)), run.endedAt === void 0 ? null : (0, react.createElement)("span", { "data-role": "run-ended" }, t("run.endedAt") + " " + new Date(run.endedAt).toLocaleString()), run.failure === void 0 ? null : (0, react.createElement)("span", {
1181
- "data-role": "run-failure",
1182
- title: run.failure.detail
1183
- }, t("run.failure") + " " + run.failure.code), run.currentPairKey === void 0 ? null : (0, react.createElement)("span", { "data-role": "run-current-pair" }, t("run.currentPair") + " " + run.currentPairKey), run.parentRunId === void 0 ? null : (0, react.createElement)("span", { "data-role": "run-parent" }, t("run.parent") + " " + run.parentRunId), run.resultRef === void 0 ? null : (0, react.createElement)("span", {
1184
- "data-role": "run-result-ref",
1185
- title: run.resultRef
1186
- }, t("run.resultRef")), (0, react.createElement)("button", {
1386
+ }, (0, react.createElement)("span", { "data-role": "run-state" }, t("run.state") + " " + t("run." + run.state)), run.progress === null || run.progress === void 0 ? null : (0, react.createElement)("span", { "data-role": "run-progress" }, t("run.progress") + " " + String(run.progress.done) + "/" + String(run.progress.total)), (0, react.createElement)("button", {
1187
1387
  type: "button",
1188
1388
  "data-role": "run-report",
1189
1389
  "data-action": "report",
@@ -1202,7 +1402,7 @@ window.__ModuleLoader__.load({
1202
1402
  onClick: () => {
1203
1403
  if (action.enabled) copy(action.command);
1204
1404
  }
1205
- }, t("action." + action.id))));
1405
+ }, t("action." + action.id))), (0, react.createElement)("details", { "data-role": "run-details" }, (0, react.createElement)("summary", null, t("run.details")), ...internals));
1206
1406
  }
1207
1407
  function ReadOnlySetting({ section, label, value, disabled, missing, note, link, t }) {
1208
1408
  const text = missing ? t("settings.capabilityMissing") : disabled ? t("settings.capabilityDisabled") : value === true ? t("settings.on") : t("settings.off");
@@ -1213,86 +1413,21 @@ window.__ModuleLoader__.load({
1213
1413
  }, (0, react.createElement)("span", { "data-role": "settings-label" }, label), (0, react.createElement)("span", {
1214
1414
  "data-role": "settings-value",
1215
1415
  "data-value": missing ? "missing" : disabled ? "unavailable" : value === true ? "on" : "off"
1216
- }, text), note === void 0 ? null : (0, react.createElement)("span", { "data-role": "settings-note" }, note), link === true ? (0, react.createElement)("a", {
1217
- "data-role": "settings-link",
1218
- href: "#/settings",
1219
- onClick: () => {
1220
- window.dispatchEvent(new KeyboardEvent("keydown", {
1221
- key: ",",
1222
- code: "Comma",
1223
- ctrlKey: true,
1224
- altKey: true,
1225
- bubbles: true
1226
- }));
1227
- }
1228
- }, t("settings.openPlatform")) : null);
1229
- }
1230
- function EditableSetting({ form, field, label, fallback, capability, t }) {
1231
- const subscribe = (0, react.useCallback)((listener) => form.subscribe(listener), [form]);
1232
- const readSnapshot = (0, react.useCallback)(() => form["getSnapshot"](), [form]);
1233
- const snapshot = (0, react.useSyncExternalStore)(subscribe, readSnapshot, readSnapshot);
1234
- const fieldPresent = snapshot.status !== "ready" || snapshot.value === void 0 || Object.prototype.hasOwnProperty.call(snapshot.value, field);
1235
- const [busy, setBusy] = (0, react.useState)(false);
1236
- const [saved, setSaved] = (0, react.useState)(void 0);
1237
- const value = snapshot.status === "ready" && typeof snapshot.value?.[field] === "boolean" ? snapshot.value[field] : fallback;
1238
- const disabled = !capability || busy || snapshot.status !== "ready" || !snapshot.writable;
1239
- const toggle = async () => {
1240
- if (disabled) return;
1241
- setBusy(true);
1242
- setSaved(void 0);
1243
- const ok = await saveSetting(form, field, !value);
1244
- setBusy(false);
1245
- setSaved(ok ? "saved" : "failed");
1246
- };
1247
- if (!fieldPresent) return (0, react.createElement)(ReadOnlySetting, {
1248
- section: "platform-write",
1249
- label,
1250
- value: fallback,
1251
- disabled: !capability,
1252
- missing: false,
1253
- note: t("settings.platformWriteLinkHint"),
1254
- link: true,
1255
- t
1256
- });
1257
- return (0, react.createElement)("div", {
1258
- "data-role": "settings-section",
1259
- "data-section": field === "mergeApproval" ? "merge-approval" : "platform-write",
1260
- ...disabled ? { "data-disabled": "true" } : {}
1261
- }, (0, react.createElement)("label", { "data-role": "settings-label" }, label), (0, react.createElement)("input", {
1262
- type: "checkbox",
1263
- role: "switch",
1264
- "data-role": "settings-toggle",
1265
- "data-field": field,
1266
- "data-value": value ? "on" : "off",
1267
- checked: value,
1268
- disabled,
1269
- onChange: () => {
1270
- toggle();
1271
- }
1272
- }), (0, react.createElement)("span", { "data-role": "settings-value" }, value ? t("settings.on") : t("settings.off")), ...saved === void 0 ? [] : [(0, react.createElement)("span", {
1273
- "data-role": "settings-save-state",
1274
- "data-save": saved
1275
- }, t(saved === "saved" ? "settings.saved" : "settings.saveFailed"))]);
1416
+ }, text), note === void 0 ? null : (0, react.createElement)("span", { "data-role": "settings-note" }, note), link === true ? (0, react.createElement)("span", { "data-role": "settings-link" }, t("settings.openPlatformPath")) : null);
1276
1417
  }
1277
- function SettingsCard({ view, settings, t }) {
1418
+ function SettingsCard({ view, t }) {
1278
1419
  const platform = view.approval.platformWrite;
1279
- const platformCapability = platform?.capability ?? settings?.platform !== void 0;
1420
+ const platformCapability = platform?.capability === true;
1280
1421
  const platformFallback = platform?.enabled ?? false;
1281
- const mergeCapability = settings?.merge !== void 0;
1282
1422
  const mergeFallback = view.approval.mergeApproval;
1283
- return (0, react.createElement)("div", { "data-role": "settings-card" }, (0, react.createElement)("h3", { "data-role": "settings-title" }, t("settings.title")), (0, react.createElement)("p", { "data-role": "settings-hint" }, t("settings.hint")), settings?.merge === void 0 ? (0, react.createElement)(ReadOnlySetting, {
1423
+ return (0, react.createElement)("div", { "data-role": "settings-card" }, (0, react.createElement)("h3", { "data-role": "settings-title" }, t("settings.title")), (0, react.createElement)("p", { "data-role": "settings-hint" }, t("settings.hint")), (0, react.createElement)(ReadOnlySetting, {
1284
1424
  section: "merge-approval",
1285
1425
  label: t("settings.mergeApproval"),
1286
1426
  value: mergeFallback,
1287
1427
  disabled: false,
1288
1428
  missing: false,
1289
- t
1290
- }) : (0, react.createElement)(EditableSetting, {
1291
- form: settings.merge,
1292
- field: "mergeApproval",
1293
- label: t("settings.mergeApproval"),
1294
- fallback: mergeFallback,
1295
- capability: mergeCapability,
1429
+ note: t("settings.mergeApprovalPath"),
1430
+ link: true,
1296
1431
  t
1297
1432
  }), (0, react.createElement)(ReadOnlySetting, {
1298
1433
  section: "platform-write",
@@ -1303,22 +1438,22 @@ window.__ModuleLoader__.load({
1303
1438
  note: t("settings.platformWriteLinkHint"),
1304
1439
  link: true,
1305
1440
  t
1306
- }), settings?.merge === void 0 ? (0, react.createElement)("a", {
1307
- "data-role": "settings-link",
1308
- href: "#/settings"
1309
- }, t("settings.openPlatform")) : null, (0, react.createElement)("p", { "data-role": "settings-readonly" }, settings?.merge === void 0 ? t("settings.readonly") : t("settings.live")));
1441
+ }), (0, react.createElement)("p", { "data-role": "settings-readonly" }, t("settings.readonly")));
1310
1442
  }
1311
- function rowNode(row, store, t, copy, sort) {
1312
- const facts = [
1443
+ function rowNode(row, t, copy, sort, openBody) {
1444
+ const first = [
1313
1445
  row.target === void 0 ? void 0 : t("row.target") + " " + row.target,
1446
+ row.failure === void 0 ? void 0 : t("row.failure") + " " + row.failure.code + (row.failure.retryable ? "" : " · " + t("row.noRetry")),
1447
+ row.expired === void 0 ? void 0 : t("row.expired") + " " + t("expired." + row.expired),
1448
+ row.reasonCode === void 0 && row.reason === void 0 ? void 0 : t("row.reason") + " " + reasonText(t, row.reasonCode, row.reason ?? "")
1449
+ ].filter((value) => value !== void 0).join(" · ");
1450
+ const details = [
1451
+ t("row.id") + " " + row.id,
1314
1452
  row.targetActual === void 0 ? void 0 : t("row.actualTarget") + " " + row.targetActual,
1315
1453
  row.verifySubState === void 0 ? void 0 : t("row.verify") + " " + t("verify." + row.verifySubState),
1316
1454
  row.origin === void 0 ? void 0 : t("row.origin") + " " + t("origin." + row.origin),
1317
1455
  row.attempts === void 0 ? void 0 : t("row.attempts") + " " + String(row.attempts),
1318
- row.confidence === void 0 ? void 0 : t("row.confidence") + " " + row.confidence.toFixed(2),
1319
- row.failure === void 0 ? void 0 : t("row.failure") + " " + row.failure.code + (row.failure.retryable ? "" : " · " + t("row.noRetry")),
1320
- row.expired === void 0 ? void 0 : t("row.expired") + " " + t("expired." + row.expired),
1321
- row.reasonCode === void 0 && row.reason === void 0 ? void 0 : t("row.reason") + " " + reasonText(t, row.reasonCode, row.reason ?? "")
1456
+ row.confidence === void 0 ? void 0 : t("row.confidence") + " " + row.confidence.toFixed(2)
1322
1457
  ].filter((value) => value !== void 0).join(" · ");
1323
1458
  const actions = row.actions.map((action) => (0, react.createElement)("button", {
1324
1459
  key: action.id,
@@ -1330,7 +1465,6 @@ window.__ModuleLoader__.load({
1330
1465
  disabled: !action.enabled,
1331
1466
  onClick: () => {
1332
1467
  if (!action.enabled) return;
1333
- if (action.confirm && globalThis.confirm !== void 0 && !globalThis.confirm(t("action.confirm") + " " + (t("action." + action.id) || action.id))) return;
1334
1468
  copy(action.command);
1335
1469
  }
1336
1470
  }, t("action." + action.id)));
@@ -1341,7 +1475,15 @@ window.__ModuleLoader__.load({
1341
1475
  "data-row-state": row.state,
1342
1476
  "data-row-queue": row.queue,
1343
1477
  draggable: true,
1478
+ tabIndex: 0,
1344
1479
  title: t("sort.drag"),
1480
+ "aria-haspopup": "menu",
1481
+ onKeyDown: (event) => {
1482
+ if (event.key !== "ContextMenu" && !(event.shiftKey && event.key === "F10")) return;
1483
+ event.preventDefault();
1484
+ const box = event.currentTarget.getBoundingClientRect();
1485
+ sort.menu(row.id, box.left + 12, box.top + 12);
1486
+ },
1345
1487
  onDragStart: (event) => {
1346
1488
  if (event.dataTransfer != null) {
1347
1489
  event.dataTransfer.setData("text/plain", row.id);
@@ -1365,52 +1507,99 @@ window.__ModuleLoader__.load({
1365
1507
  event.preventDefault();
1366
1508
  sort.menu(row.id, event.clientX, event.clientY);
1367
1509
  }
1368
- }, (0, react.createElement)("div", null, (0, react.createElement)("div", { "data-role": "row-label" }, row.label), (0, react.createElement)("div", {
1369
- "data-role": "row-id",
1370
- title: row.id
1371
- }, t("row.id") + " " + row.id), (0, react.createElement)("div", { "data-role": "row-meta" }, t("state." + row.state) + (row.stage === null ? "" : " · " + t("stage." + row.stage))), stageNode(row, t), summaryNode(row, t), facts === "" ? null : (0, react.createElement)("div", { "data-role": "row-facts" }, facts)), (0, react.createElement)("div", null, (0, react.createElement)("div", { "data-role": "row-meta" }, new Date(row.updatedAt).toLocaleString()), (0, react.createElement)("div", { "data-role": "row-actions" }, (() => {
1510
+ }, (0, react.createElement)("div", null, (0, react.createElement)("div", { "data-role": "row-label" }, row.label), (0, react.createElement)("div", { "data-role": "row-meta" }, t("state." + row.state) + (row.stage === null ? "" : " · " + t("stage." + row.stage))), stageNode(row, t), summaryNode(row, t), first === "" ? null : (0, react.createElement)("div", { "data-role": "row-facts" }, first), (0, react.createElement)("details", { "data-role": "row-details" }, (0, react.createElement)("summary", null, t("row.details")), (0, react.createElement)("p", null, details))), (0, react.createElement)("div", null, (0, react.createElement)("div", { "data-role": "row-meta" }, new Date(row.updatedAt).toLocaleString()), (0, react.createElement)("div", { "data-role": "row-actions" }, (() => {
1372
1511
  const bodyA = row.bodies?.find((body) => body.name === row.a);
1373
1512
  const bodyB = row.bodies?.find((body) => body.name === row.b);
1374
1513
  return [bodyA === void 0 ? null : (0, react.createElement)("button", {
1375
1514
  key: "a",
1376
1515
  type: "button",
1377
1516
  "data-role": "body-a",
1517
+ "aria-label": t("body." + bodyA.side) + " " + bodyA.name,
1518
+ title: t("body." + bodyA.side) + " " + bodyA.name,
1378
1519
  onClick: () => {
1379
- store.openBody(bodyA.name, bodyA.side, bodyA.rowId);
1520
+ openBody(bodyA.name, bodyA.side, bodyA.rowId);
1380
1521
  }
1381
- }, "A"), bodyB === void 0 ? null : (0, react.createElement)("button", {
1522
+ }, "A · " + bodyA.name), bodyB === void 0 ? null : (0, react.createElement)("button", {
1382
1523
  key: "b",
1383
1524
  type: "button",
1384
1525
  "data-role": "body-b",
1526
+ "aria-label": t("body." + bodyB.side) + " " + bodyB.name,
1527
+ title: t("body." + bodyB.side) + " " + bodyB.name,
1385
1528
  onClick: () => {
1386
- store.openBody(bodyB.name, bodyB.side, bodyB.rowId);
1529
+ openBody(bodyB.name, bodyB.side, bodyB.rowId);
1387
1530
  }
1388
- }, "B")];
1531
+ }, "B · " + bodyB.name)];
1389
1532
  })(), actions)));
1390
1533
  }
1534
+ const overlayKey = (event, onClose) => {
1535
+ if (event.key === "Escape") {
1536
+ event.preventDefault();
1537
+ onClose();
1538
+ return;
1539
+ }
1540
+ if (event.key !== "Tab") return;
1541
+ const nodes = [...event.currentTarget.querySelectorAll("button, [href], input, select, textarea, [tabindex]:not([tabindex=\"-1\"])")].filter((node) => !node.hasAttribute("disabled"));
1542
+ if (nodes.length === 0) return;
1543
+ const first = nodes[0];
1544
+ const last = nodes[nodes.length - 1];
1545
+ if (event.shiftKey && document.activeElement === first) {
1546
+ event.preventDefault();
1547
+ last.focus();
1548
+ } else if (!event.shiftKey && document.activeElement === last) {
1549
+ event.preventDefault();
1550
+ first.focus();
1551
+ }
1552
+ };
1391
1553
  function bodyNode(overlay, t, onClose) {
1392
- return (0, react.createElement)("section", {
1554
+ const Modal = platformModal();
1555
+ const title = t("overlay.body") + " · " + overlay.name;
1556
+ const inner = (0, react.createElement)("section", {
1393
1557
  "data-role": "overlay",
1394
1558
  "data-overlay": "body",
1395
- role: "dialog",
1396
- "aria-modal": "true",
1397
- "aria-label": t("overlay.body")
1398
- }, (0, react.createElement)("div", { "data-role": "overlay-head" }, (0, react.createElement)("h3", { "data-role": "overlay-title" }, t("overlay.body") + " · " + overlay.name), (0, react.createElement)("button", {
1559
+ "aria-label": title,
1560
+ ...Modal === void 0 ? {
1561
+ role: "dialog",
1562
+ "aria-modal": "true",
1563
+ onKeyDown: (event) => {
1564
+ overlayKey(event, onClose);
1565
+ }
1566
+ } : { "data-platform-modal": "true" }
1567
+ }, (0, react.createElement)("div", { "data-role": "overlay-head" }, (0, react.createElement)("h3", { "data-role": "overlay-title" }, title), (0, react.createElement)("button", {
1399
1568
  type: "button",
1400
1569
  "data-role": "overlay-close",
1570
+ ...Modal === void 0 ? { autoFocus: true } : { "data-modal-autofocus": "true" },
1401
1571
  onClick: onClose
1402
1572
  }, t("close"))), (0, react.createElement)("p", { "data-role": "overlay-meta" }, t("body." + overlay.side) + " · " + (overlay.status === "ready" ? t("body.totalBytes") + " " + String(overlay.body?.totalBytes ?? 0) : "")), overlay.status === "loading" ? (0, react.createElement)("p", { "data-role": "overlay-loading" }, t("loading")) : null, overlay.status === "error" && overlay.failure !== void 0 ? failureNode(t, overlay.failure, [], () => {}) : null, overlay.status === "ready" && overlay.body !== void 0 ? (0, react.createElement)("div", null, overlay.body.truncated ? (0, react.createElement)("p", { "data-role": "body-truncated" }, t("body.truncated")) : null, overlay.body.text === "" ? (0, react.createElement)("p", { "data-role": "body-empty" }, t("body.empty")) : (0, react.createElement)("pre", { "data-role": "body-text" }, overlay.body.text)) : null);
1573
+ return Modal === void 0 ? inner : (0, react.createElement)(Modal, {
1574
+ open: true,
1575
+ onClose,
1576
+ title,
1577
+ closeLabel: t("close"),
1578
+ headless: true,
1579
+ className: "evo-merge-modal"
1580
+ }, inner);
1403
1581
  }
1404
1582
  function historyNode(overlay, t, onClose) {
1405
- return (0, react.createElement)("section", {
1583
+ const Modal = platformModal();
1584
+ const title = t("history");
1585
+ const inner = (0, react.createElement)("section", {
1406
1586
  "data-role": "overlay",
1407
1587
  "data-overlay": "history",
1408
- role: "dialog",
1409
- "aria-modal": "true",
1410
- "aria-label": t("history")
1411
- }, (0, react.createElement)("div", { "data-role": "overlay-head" }, (0, react.createElement)("h3", { "data-role": "overlay-title" }, t("history")), (0, react.createElement)("button", {
1588
+ "aria-label": title,
1589
+ ...Modal === void 0 ? {
1590
+ role: "dialog",
1591
+ "aria-modal": "true",
1592
+ onKeyDown: (event) => {
1593
+ if (event.key === "Escape") {
1594
+ event.preventDefault();
1595
+ onClose();
1596
+ }
1597
+ }
1598
+ } : { "data-platform-modal": "true" }
1599
+ }, (0, react.createElement)("div", { "data-role": "overlay-head" }, (0, react.createElement)("h3", { "data-role": "overlay-title" }, title), (0, react.createElement)("button", {
1412
1600
  type: "button",
1413
1601
  "data-role": "overlay-close",
1602
+ ...Modal === void 0 ? { autoFocus: true } : { "data-modal-autofocus": "true" },
1414
1603
  onClick: onClose
1415
1604
  }, t("close"))), overlay.status === "loading" ? (0, react.createElement)("p", { "data-role": "overlay-loading" }, t("loading")) : null, overlay.status === "error" && overlay.failure !== void 0 ? failureNode(t, overlay.failure, [], () => {}) : null, overlay.status === "ready" ? overlay.items === void 0 || overlay.items.length === 0 ? (0, react.createElement)("p", { "data-role": "history-empty" }, t("history.empty")) : (0, react.createElement)("ul", { "data-role": "history-list" }, overlay.items.map((item) => (0, react.createElement)("li", {
1416
1605
  key: item.name + ":" + item.archivedAt,
@@ -1419,20 +1608,59 @@ window.__ModuleLoader__.load({
1419
1608
  const kind = t("history.kind." + item.kind);
1420
1609
  const reason = reasonText(t, item.reasonCode, item.reason);
1421
1610
  return kind === reason ? kind : kind + " · " + reason;
1422
- })() + " · v" + String(item.lastVersion)))) : null);
1611
+ })() + " · v" + String(item.lastVersion) + " · " + t("history.archivedAt", { date: new Date(item.archivedAt).toLocaleString() })))) : null);
1612
+ return Modal === void 0 ? inner : (0, react.createElement)(Modal, {
1613
+ open: true,
1614
+ onClose,
1615
+ title,
1616
+ closeLabel: t("close"),
1617
+ headless: true,
1618
+ className: "evo-merge-modal"
1619
+ }, inner);
1423
1620
  }
1424
1621
  function MergePanel(face) {
1425
1622
  const { store, t } = face;
1426
1623
  const snapshot = (0, react.useSyncExternalStore)(store.subscribe, store.read, store.read);
1427
- const [copied, setCopied] = (0, react.useState)(void 0);
1624
+ const [copyState, setCopyState] = (0, react.useState)({ kind: "idle" });
1428
1625
  const [orders, setOrders] = (0, react.useState)({});
1429
1626
  const [dragId, setDragId] = (0, react.useState)(void 0);
1430
1627
  const [menu, setMenu] = (0, react.useState)(void 0);
1628
+ const returnFocus = (0, react.useRef)(null);
1629
+ const captureFocus = () => {
1630
+ returnFocus.current = document.activeElement instanceof HTMLElement ? document.activeElement : null;
1631
+ };
1632
+ const openBody = (name, side, rowId) => {
1633
+ captureFocus();
1634
+ store.openBody(name, side, rowId);
1635
+ };
1636
+ const openHistory = () => {
1637
+ captureFocus();
1638
+ store.openHistory();
1639
+ };
1640
+ const closeOverlay = () => {
1641
+ store.closeOverlay();
1642
+ requestAnimationFrame(() => {
1643
+ returnFocus.current?.focus();
1644
+ });
1645
+ };
1431
1646
  const workspaceId = resolveWorkspaceId(face.workspaceId);
1432
1647
  const orderStore = (0, react.useMemo)(() => createRowOrderStore(), []);
1433
1648
  const copy = (command) => {
1434
- navigator.clipboard.writeText(command);
1435
- setCopied(command);
1649
+ (async () => {
1650
+ try {
1651
+ if (navigator.clipboard?.writeText === void 0) throw new Error("clipboard unavailable");
1652
+ await navigator.clipboard.writeText(command);
1653
+ setCopyState({
1654
+ kind: "copied",
1655
+ text: command
1656
+ });
1657
+ } catch {
1658
+ setCopyState({
1659
+ kind: "failed",
1660
+ text: command
1661
+ });
1662
+ }
1663
+ })();
1436
1664
  };
1437
1665
  const doctorCommand = face.doctorCommand ?? DOCTOR_COMMAND;
1438
1666
  (0, react.useEffect)(() => {
@@ -1503,19 +1731,37 @@ window.__ModuleLoader__.load({
1503
1731
  saveOrder(pinRow(visibleRows.map((row) => row.id), menu.id, edge));
1504
1732
  setMenu(void 0);
1505
1733
  };
1734
+ const onTabKey = (event, current) => {
1735
+ const delta = event.key === "ArrowRight" ? 1 : event.key === "ArrowLeft" ? -1 : 0;
1736
+ const isEdge = event.key === "Home" || event.key === "End";
1737
+ if (delta === 0 && !isEdge) return;
1738
+ event.preventDefault();
1739
+ const index = QUEUES.indexOf(current);
1740
+ const next = event.key === "Home" ? QUEUES[0] : event.key === "End" ? QUEUES[QUEUES.length - 1] : QUEUES[(index + delta + QUEUES.length) % QUEUES.length];
1741
+ store.select(next);
1742
+ requestAnimationFrame(() => {
1743
+ document.getElementById("merge-tab-" + next)?.focus();
1744
+ });
1745
+ };
1506
1746
  const tabs = (0, react.createElement)("div", {
1507
1747
  "data-role": "tabs",
1508
1748
  role: "tablist"
1509
1749
  }, QUEUES.map((item) => (0, react.createElement)("button", {
1510
1750
  key: item,
1751
+ id: "merge-tab-" + item,
1511
1752
  type: "button",
1512
1753
  role: "tab",
1513
1754
  "aria-selected": snapshot.queue === item ? "true" : "false",
1755
+ "aria-controls": "merge-rows",
1756
+ tabIndex: snapshot.queue === item ? 0 : -1,
1514
1757
  "data-role": "tab",
1515
1758
  "data-active": snapshot.queue === item ? "true" : "false",
1516
1759
  onClick: () => {
1517
1760
  setMenu(void 0);
1518
1761
  store.select(item);
1762
+ },
1763
+ onKeyDown: (event) => {
1764
+ onTabKey(event, item);
1519
1765
  }
1520
1766
  }, t("tab." + item) + " " + String(view.counts[item]))));
1521
1767
  const expiredToggle = snapshot.queue === "pending" ? (0, react.createElement)("button", {
@@ -1528,14 +1774,20 @@ window.__ModuleLoader__.load({
1528
1774
  store.toggleExpired();
1529
1775
  }
1530
1776
  }, t("tab.expired") + " " + String(view.expired)) : null;
1531
- const list = uiState === "ready" && queue.status === "ready" ? (0, react.createElement)("div", { "data-role": "rows" }, visibleRows.map((row) => rowNode(row, store, t, copy, sortHandlers))) : null;
1777
+ const list = uiState === "ready" && queue.status === "ready" ? (0, react.createElement)("div", {
1778
+ "data-role": "rows",
1779
+ id: "merge-rows",
1780
+ role: "tabpanel"
1781
+ }, visibleRows.map((row) => rowNode(row, t, copy, sortHandlers, openBody))) : null;
1532
1782
  const empty = uiState === "empty" ? (0, react.createElement)("div", null, (0, react.createElement)("p", { "data-role": "note" }, t("empty." + snapshot.queue)), (0, react.createElement)("p", { "data-role": "empty-scope" }, t("empty.scope"))) : null;
1533
- const activeFailure = viewFailure !== void 0 ? viewFailure : queue.status === "error" ? queue.failure : void 0;
1783
+ const staleViewFailure = snapshot.viewState === "ready" ? snapshot.viewFailure : void 0;
1784
+ const activeFailure = viewFailure !== void 0 ? viewFailure : staleViewFailure !== void 0 ? staleViewFailure : queue.status === "error" ? queue.failure : void 0;
1534
1785
  const overlay = snapshot.overlay;
1535
1786
  const menuStyle = menu === void 0 ? void 0 : {
1536
1787
  position: "fixed",
1537
1788
  zIndex: 70,
1538
- minWidth: "var(--evo-measure-read)",
1789
+ minWidth: "160px",
1790
+ maxWidth: "min(320px, calc(100vw - 16px))",
1539
1791
  left: String(Math.min(menu.x, Math.max(0, (globalThis.innerWidth || 1440) - 180))) + "px",
1540
1792
  top: String(Math.min(menu.y, Math.max(0, (globalThis.innerHeight || 800) - 110))) + "px"
1541
1793
  };
@@ -1606,12 +1858,17 @@ window.__ModuleLoader__.load({
1606
1858
  }
1607
1859
  }, t("start.pair"))), view.degraded ? (0, react.createElement)("p", { "data-role": "degraded" }, t("degraded") + " " + view.notes.join(",")) : null, (0, react.createElement)(SettingsCard, {
1608
1860
  view,
1609
- settings: face.settings,
1610
1861
  t
1611
- }), runNode(view, t, copy), tabs, expiredToggle, snapshot.viewState === "loading" || queue.status === "loading" ? (0, react.createElement)("p", { "data-role": "note" }, t("loading")) : null, activeFailure === void 0 ? null : failureNode(t, activeFailure, fallback, copy), copied === void 0 ? null : (0, react.createElement)("p", {
1862
+ }), runNode(view, t, copy), tabs, expiredToggle, snapshot.viewState === "loading" || queue.status === "loading" ? (0, react.createElement)("p", { "data-role": "note" }, t("loading")) : queue.refreshing === true ? (0, react.createElement)("p", {
1863
+ "data-role": "note",
1864
+ role: "status"
1865
+ }, t("refreshing")) : null, activeFailure === void 0 ? null : failureNode(t, activeFailure, fallback, copy), copyState.kind === "idle" ? null : copyState.kind === "copied" ? (0, react.createElement)("p", {
1612
1866
  "data-role": "copied",
1613
1867
  role: "status"
1614
- }, t("copied") + " " + copied), list, empty, queue.nextCursor === void 0 || uiState !== "ready" ? null : (0, react.createElement)("button", {
1868
+ }, t("copied") + " " + (copyState.text ?? "")) : (0, react.createElement)("p", {
1869
+ "data-role": "copy-failed",
1870
+ role: "alert"
1871
+ }, t("copyFailed")), list, empty, queue.nextCursor === void 0 || uiState !== "ready" ? null : (0, react.createElement)("button", {
1615
1872
  type: "button",
1616
1873
  "data-role": "load-more",
1617
1874
  disabled: queue.status === "loading",
@@ -1622,7 +1879,7 @@ window.__ModuleLoader__.load({
1622
1879
  type: "button",
1623
1880
  "data-role": "history",
1624
1881
  onClick: () => {
1625
- store.openHistory();
1882
+ openHistory();
1626
1883
  }
1627
1884
  }, t("history")), (0, react.createElement)("button", {
1628
1885
  type: "button",
@@ -1630,11 +1887,7 @@ window.__ModuleLoader__.load({
1630
1887
  onClick: () => {
1631
1888
  copy(doctorCommand);
1632
1889
  }
1633
- }, t("doctor.open")), overlay === void 0 ? null : overlay.kind === "body" ? bodyNode(overlay, t, () => {
1634
- store.closeOverlay();
1635
- }) : historyNode(overlay, t, () => {
1636
- store.closeOverlay();
1637
- }), (0, react.createElement)("button", {
1890
+ }, t("doctor.open")), overlay === void 0 ? null : overlay.kind === "body" ? bodyNode(overlay, t, closeOverlay) : historyNode(overlay, t, closeOverlay), (0, react.createElement)("button", {
1638
1891
  type: "button",
1639
1892
  "data-role": "refresh",
1640
1893
  onClick: () => {
@@ -1693,6 +1946,7 @@ window.__ModuleLoader__.load({
1693
1946
  overlayToken = 0;
1694
1947
  snapshot;
1695
1948
  started = false;
1949
+ loadedView = false;
1696
1950
  disposed = false;
1697
1951
  constructor(source, initial = {}) {
1698
1952
  this.source = source;
@@ -1738,7 +1992,11 @@ window.__ModuleLoader__.load({
1738
1992
  }
1739
1993
  async loadView(showLoading = true) {
1740
1994
  const token = ++this.viewToken;
1741
- if (showLoading) this.set({
1995
+ if (showLoading) if (this.snapshot.viewState === "ready") this.set({
1996
+ ...this.snapshot,
1997
+ viewRefreshing: true
1998
+ });
1999
+ else this.set({
1742
2000
  ...this.snapshot,
1743
2001
  viewState: "loading",
1744
2002
  viewFailure: void 0
@@ -1762,19 +2020,23 @@ window.__ModuleLoader__.load({
1762
2020
  });
1763
2021
  return;
1764
2022
  }
2023
+ const keeping = this.loadedView;
1765
2024
  this.set({
1766
2025
  ...this.snapshot,
1767
- viewState: "error",
1768
- viewFailure: outcome.failure
2026
+ viewState: keeping ? "ready" : "error",
2027
+ viewFailure: outcome.failure,
2028
+ viewRefreshing: false
1769
2029
  });
1770
2030
  return;
1771
2031
  }
2032
+ this.loadedView = true;
1772
2033
  const wasBlocked = this.snapshot.viewState === "blocked";
1773
2034
  this.set({
1774
2035
  ...this.snapshot,
1775
2036
  view: outcome.payload,
1776
2037
  viewState: "ready",
1777
- viewFailure: void 0
2038
+ viewFailure: void 0,
2039
+ viewRefreshing: false
1778
2040
  });
1779
2041
  if (wasBlocked && this.snapshot.current.status === "idle") this.loadQueue(this.snapshot.queue, this.snapshot.expiredOnly);
1780
2042
  }
@@ -1783,7 +2045,13 @@ window.__ModuleLoader__.load({
1783
2045
  const token = (this.tokens.get(key) ?? 0) + 1;
1784
2046
  this.tokens.set(key, token);
1785
2047
  const previous = this.cache.get(key);
1786
- const next = cursor === void 0 ? {
2048
+ const keepRows = cursor === void 0 && (previous?.rows.length ?? 0) > 0;
2049
+ const next = cursor === void 0 ? keepRows ? {
2050
+ status: "ready",
2051
+ refreshing: true,
2052
+ rows: previous.rows,
2053
+ ...previous.nextCursor === void 0 ? {} : { nextCursor: previous.nextCursor }
2054
+ } : {
1787
2055
  status: "loading",
1788
2056
  rows: []
1789
2057
  } : {
@@ -1815,7 +2083,12 @@ window.__ModuleLoader__.load({
1815
2083
  });
1816
2084
  return;
1817
2085
  }
1818
- const failed = {
2086
+ const failed = keepRows ? {
2087
+ status: "ready",
2088
+ refreshing: false,
2089
+ rows: previous.rows,
2090
+ failure: outcome.failure
2091
+ } : {
1819
2092
  status: "error",
1820
2093
  rows: [],
1821
2094
  failure: outcome.failure
@@ -1830,6 +2103,7 @@ window.__ModuleLoader__.load({
1830
2103
  const items = cursor === void 0 ? outcome.payload.items : [...previous?.rows ?? [], ...outcome.payload.items];
1831
2104
  const ready = {
1832
2105
  status: items.length === 0 ? "empty" : "ready",
2106
+ refreshing: false,
1833
2107
  rows: items,
1834
2108
  ...outcome.payload.nextCursor === void 0 ? {} : { nextCursor: outcome.payload.nextCursor }
1835
2109
  };
@@ -1981,26 +2255,16 @@ window.__ModuleLoader__.load({
1981
2255
  //#endregion
1982
2256
  //#region src/client/index.ts
1983
2257
  const inject = ["slots", "locale"];
1984
- function PanelIcon(props = {}) {
1985
- const size = typeof props.size === "number" && props.size > 0 ? props.size : 16;
1986
- return (0, react.createElement)("svg", {
1987
- "aria-hidden": "true",
1988
- width: String(size),
1989
- height: String(size),
1990
- viewBox: "0 0 16 16",
1991
- fill: "none",
1992
- stroke: "currentColor",
1993
- strokeWidth: "1.5",
1994
- strokeLinecap: "round",
1995
- strokeLinejoin: "round"
1996
- }, (0, react.createElement)("path", { d: "M3 4h10M3 8h6M3 12h8" }));
2258
+ /** The view component receives the workbench's activeView marker alongside its face. */
2259
+ function MergeView(props) {
2260
+ return props.activeView === "merge" ? MergePanel(props) : null;
1997
2261
  }
1998
2262
  /**
1999
- * Register the merge row and its keyed main body through the shared panel lifecycle.
2263
+ * Register the merge view on the workbench page.
2000
2264
  * @param ctx - the client context.
2001
2265
  */
2002
2266
  function apply(ctx) {
2003
- registerSidebarPanel(ctx, {
2267
+ registerWorkbenchView(ctx, {
2004
2268
  id: PANEL_ID,
2005
2269
  order: 45,
2006
2270
  namespace: NS,
@@ -2012,30 +2276,24 @@ window.__ModuleLoader__.load({
2012
2276
  css: CSS,
2013
2277
  tagId: CSS_TAG_ID
2014
2278
  },
2015
- icon: PanelIcon,
2016
- component: MergePanel,
2017
- body: (t) => {
2279
+ component: MergeView,
2280
+ face: (t) => {
2018
2281
  const source = {
2019
2282
  view,
2020
2283
  rows,
2021
2284
  body,
2022
2285
  history
2023
2286
  };
2024
- let seat;
2025
- try {
2026
- seat = ctx.get?.("configForms");
2027
- } catch {
2028
- seat = void 0;
2029
- }
2287
+ const seat = createSettingsSeat(ctx);
2030
2288
  const workspaceId = panelWorkspaceId(ctx);
2031
2289
  return {
2032
2290
  store: new MergeStore(source),
2033
2291
  t,
2034
2292
  doctorCommand: "/evolution doctor",
2035
- settings: { ...seat === void 0 ? {} : {
2036
- merge: seat.get("evolution-merge"),
2037
- platform: seat.get("evolution-approval")
2038
- } },
2293
+ settings: {
2294
+ ...seat.get("evolution-merge") === void 0 ? {} : { merge: seat.get("evolution-merge") },
2295
+ ...seat.get("evolution-approval") === void 0 ? {} : { platform: seat.get("evolution-approval") }
2296
+ },
2039
2297
  ...workspaceId === void 0 ? {} : { workspaceId }
2040
2298
  };
2041
2299
  }
@@ -11,6 +11,8 @@ export interface MergeSource {
11
11
  export type QueueStatus = 'idle' | 'loading' | 'ready' | 'empty' | 'error';
12
12
  export interface QueueState {
13
13
  status: QueueStatus;
14
+ /** A background refresh is in flight; the old rows remain visible. */
15
+ refreshing?: boolean | undefined;
14
16
  rows: MergeRowView[];
15
17
  nextCursor?: string | undefined;
16
18
  failure?: MergeFailure | undefined;
@@ -31,6 +33,7 @@ export type OverlayState = {
31
33
  export interface StoreSnapshot {
32
34
  view: MergeViewPayload;
33
35
  viewState: 'loading' | 'ready' | 'error' | 'blocked';
36
+ viewRefreshing?: boolean | undefined;
34
37
  viewFailure?: MergeFailure | undefined;
35
38
  queue: MergeQueue;
36
39
  expiredOnly: boolean;
@@ -52,6 +55,7 @@ export declare class MergeStore {
52
55
  private overlayToken;
53
56
  private snapshot;
54
57
  private started;
58
+ private loadedView;
55
59
  private disposed;
56
60
  constructor(source: MergeSource, initial?: MergeStoreInitial);
57
61
  readonly read: () => StoreSnapshot;
@@ -1,7 +1,7 @@
1
1
  /** Batch 4 merge panel: keyed read store, four tabs, local overlays, localized errors. */
2
2
  import { type ReactNode } from 'react';
3
3
  import type { MergeStore } from './MergeStore.ts';
4
- import type { ConfigFormLike, SettingsFace } from '@lmzhen/dsh-evolution-client-kit';
4
+ import type { SettingsFace } from '@lmzhen/dsh-evolution-client-kit';
5
5
  export type Translate = (key: string, values?: Readonly<Record<string, string | number>>) => string;
6
6
  export interface MergeFace {
7
7
  store: MergeStore;
@@ -10,6 +10,5 @@ export interface MergeFace {
10
10
  settings?: SettingsFace;
11
11
  workspaceId?: string;
12
12
  }
13
- export declare function saveSetting(form: ConfigFormLike, field: string, value: boolean): Promise<boolean>;
14
13
  export declare function MergePanel(face: MergeFace): ReactNode;
15
14
  //# sourceMappingURL=Panel.d.ts.map
@@ -1,8 +1,8 @@
1
- /** Browser half: one sidebar row and the read-only Batch 4 panel. */
1
+ /** Browser half: one read-only merge view inside the shared evolution workbench. */
2
2
  import type { Context as ClientContext } from '@deepseek-ai/cordis';
3
3
  export declare const inject: string[];
4
4
  /**
5
- * Register the merge row and its keyed main body through the shared panel lifecycle.
5
+ * Register the merge view on the workbench page.
6
6
  * @param ctx - the client context.
7
7
  */
8
8
  export declare function apply(ctx: ClientContext): void;
@@ -0,0 +1,15 @@
1
+ /** Platform Modal reached through the rc.3 client module table. */
2
+ import type { ReactNode } from 'react';
3
+ export interface PlatformModalProps {
4
+ readonly open: boolean;
5
+ readonly onClose: () => void;
6
+ readonly title: string;
7
+ readonly closeLabel: string;
8
+ readonly children?: ReactNode;
9
+ readonly headless?: boolean;
10
+ readonly className?: string;
11
+ }
12
+ export type PlatformModalComponent = (props: PlatformModalProps) => ReactNode;
13
+ /** The platform Modal, or undefined in a non-browser/SSR render or a deployment without the row. */
14
+ export declare function platformModal(): PlatformModalComponent | undefined;
15
+ //# sourceMappingURL=platform-modal.d.ts.map
package/package.json CHANGED
@@ -11,7 +11,7 @@
11
11
  ]
12
12
  }
13
13
  },
14
- "version": "0.32.5",
14
+ "version": "0.33.0",
15
15
  "publishConfig": {
16
16
  "access": "public"
17
17
  },
@@ -51,7 +51,7 @@
51
51
  "react-dom": "^18.2.0"
52
52
  },
53
53
  "dependencies": {
54
- "@lmzhen/dsh-evolution-client-kit": "^0.32.5"
54
+ "@lmzhen/dsh-evolution-client-kit": "^0.33.0"
55
55
  },
56
56
  "engines": {
57
57
  "dsh": ">=0.2.1-rc.2 <0.2.2-0"