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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/client.js CHANGED
@@ -163,34 +163,51 @@ window.__ModuleLoader__.load({
163
163
  ...EMPTY,
164
164
  reason: "seat-missing"
165
165
  };
166
+ let lastReady;
167
+ let generation = 0;
166
168
  const listeners = /* @__PURE__ */ new Set();
167
- let started = false;
168
- let listening = false;
169
+ let remoteOff;
169
170
  const notify = () => {
170
171
  for (const listener of listeners) listener();
171
172
  };
172
- const refresh = async () => {
173
- snapshot = await describe(namespace);
173
+ const absorb = (next) => {
174
+ if (next.status === "ready") {
175
+ lastReady = next;
176
+ snapshot = next;
177
+ } else if (lastReady !== void 0) snapshot = {
178
+ ...lastReady,
179
+ stale: true,
180
+ reason: next.reason ?? "readback-failed"
181
+ };
182
+ else snapshot = next;
174
183
  notify();
175
184
  };
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();
185
+ const refresh = async () => {
186
+ const ticket = ++generation;
187
+ const next = await describe(namespace);
188
+ if (ticket !== generation) return;
189
+ absorb(next);
190
+ };
191
+ const remote = () => ctx.get?.("remote");
192
+ const stopRemote = () => {
193
+ remoteOff?.();
194
+ remoteOff = void 0;
195
+ };
196
+ const startRemote = () => {
197
+ if (remoteOff !== void 0) return;
198
+ remoteOff = remote()?.$on?.("settings/document-updated", (changed) => {
199
+ if (changed === namespace) refresh();
200
+ });
186
201
  };
187
202
  const form = {
188
203
  getSnapshot: () => snapshot,
189
204
  subscribe: (listener) => {
190
205
  listeners.add(listener);
191
- start();
206
+ startRemote();
207
+ refresh();
192
208
  return () => {
193
209
  listeners.delete(listener);
210
+ if (listeners.size === 0) stopRemote();
194
211
  };
195
212
  },
196
213
  set: async (field, value) => await form.mutate([{
@@ -204,35 +221,39 @@ window.__ModuleLoader__.load({
204
221
  }]),
205
222
  mutate: async (ops, expectedRevision) => {
206
223
  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();
224
+ if (outcome.snapshot !== void 0) absorb(outcome.snapshot);
225
+ else await refresh();
211
226
  return outcome.ok;
227
+ },
228
+ refresh,
229
+ dispose: () => {
230
+ stopRemote();
231
+ listeners.clear();
212
232
  }
213
233
  };
214
234
  return form;
215
235
  }
216
236
  /**
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/*.
237
+ * Build one seat per client registration; every form's remote listener is released by
238
+ * the caller's effect. No module-level WeakMap: a replaced registration must not share
239
+ * a dead seat with the previous one.
220
240
  */
221
- const seats = /* @__PURE__ */ new WeakMap();
222
241
  function createSettingsSeat(ctx) {
223
- const cached = seats.get(ctx);
224
- if (cached !== void 0) return cached;
225
242
  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);
243
+ return {
244
+ get(namespace) {
245
+ let form = forms.get(namespace);
246
+ if (form === void 0) {
247
+ form = createForm(namespace, ctx);
248
+ forms.set(namespace, form);
249
+ }
250
+ return form;
251
+ },
252
+ dispose: () => {
253
+ for (const form of forms.values()) form.dispose?.();
254
+ forms.clear();
231
255
  }
232
- return form;
233
- } };
234
- seats.set(ctx, seat);
235
- return seat;
256
+ };
236
257
  }
237
258
  //#endregion
238
259
  //#region ../evolution-client-kit/src/workspace.ts
@@ -491,7 +512,8 @@ window.__ModuleLoader__.load({
491
512
  startCommands: facts.startCommands,
492
513
  notes: [...facts.notes],
493
514
  degraded: facts.degraded,
494
- ...facts.active === null && facts.last === null ? {} : { latestRun: toRun(facts.active ?? facts.last) },
515
+ ...facts.active === null ? {} : { activeRun: toRun(facts.active) },
516
+ ...facts.last === null ? {} : { lastRun: toRun(facts.last) },
495
517
  approval: {
496
518
  mergeApproval: facts.approval.mergeApproval,
497
519
  ...platformWrite === void 0 ? {} : { platformWrite }
@@ -518,9 +540,14 @@ window.__ModuleLoader__.load({
518
540
  };
519
541
  }
520
542
  async function body(name, side, rowId) {
521
- const answer = await requestWire(HOST_ROUTES.body + (rowId === void 0 ? "?name=" + encodeURIComponent(name) : "?id=" + encodeURIComponent(rowId)) + "&side=" + side);
543
+ const query = new URLSearchParams({
544
+ name,
545
+ side
546
+ });
547
+ if (rowId !== void 0) query.set("id", rowId);
548
+ const answer = await requestWire(HOST_ROUTES.body + "?" + query.toString());
522
549
  if (!answer.ok) {
523
- if (answer.failure.kind === "problem" && answer.failure.code === "bad-query" && (answer.failure.message ?? "").includes("multiple merge rows")) return {
550
+ if (answer.failure.kind === "problem" && answer.failure.code === "bad-query" && (answer.failure.message ?? "").includes("ambiguous")) return {
524
551
  ok: false,
525
552
  failure: {
526
553
  ...answer.failure,
@@ -541,6 +568,56 @@ window.__ModuleLoader__.load({
541
568
  }
542
569
  };
543
570
  }
571
+ async function decide(rowId, verb, sessionId) {
572
+ const path = HOST_ROUTES.body.replace(/body$/, "decision");
573
+ let response;
574
+ try {
575
+ response = await fetch(path, {
576
+ method: "POST",
577
+ headers: {
578
+ accept: "application/json",
579
+ "content-type": "application/json"
580
+ },
581
+ body: JSON.stringify({
582
+ rowId,
583
+ verb,
584
+ sessionId
585
+ })
586
+ });
587
+ } catch {
588
+ return {
589
+ ok: false,
590
+ failure: problem("server", 0)
591
+ };
592
+ }
593
+ let body;
594
+ try {
595
+ body = await response.json();
596
+ } catch {
597
+ return {
598
+ ok: false,
599
+ failure: problem("server", response.status)
600
+ };
601
+ }
602
+ if (typeof body !== "object" || body === null) return {
603
+ ok: false,
604
+ failure: problem("server", response.status)
605
+ };
606
+ const record = body;
607
+ if (record.wireVersion !== 1 || typeof record.ok !== "boolean" || typeof record.message !== "string") return {
608
+ ok: false,
609
+ failure: problem("wire-version", response.status)
610
+ };
611
+ return {
612
+ ok: true,
613
+ payload: {
614
+ ok: record.ok,
615
+ message: record.message,
616
+ ...typeof record.code === "string" ? { code: record.code } : {},
617
+ ...typeof record.runId === "string" ? { runId: record.runId } : {}
618
+ }
619
+ };
620
+ }
544
621
  async function history() {
545
622
  const answer = await requestWire(HOST_ROUTES.history);
546
623
  return answer.ok ? {
@@ -634,6 +711,8 @@ window.__ModuleLoader__.load({
634
711
  "skipKind.forced": "强制",
635
712
  "action.approve": "复制批准",
636
713
  "action.reject": "复制拒绝",
714
+ "decision.approve": "批准",
715
+ "decision.reject": "拒绝",
637
716
  "action.retry-candidates": "复制重试候选",
638
717
  "action.retry-resume": "重试续跑",
639
718
  "action.discard": "复制丢弃",
@@ -708,7 +787,6 @@ window.__ModuleLoader__.load({
708
787
  "history.reason.archived": "已归档",
709
788
  "history.reason.unknown": "原因未提供",
710
789
  "history.lastVersion": "最后版本",
711
- "history.archivedAt": "归档时间",
712
790
  "body.truncated": "正文过长,已截断。",
713
791
  "body.empty": "正文为空。",
714
792
  "body.totalBytes": "总字节",
@@ -729,7 +807,6 @@ window.__ModuleLoader__.load({
729
807
  "settings.capabilityMissing": "宿主未提供该能力",
730
808
  "settings.capabilityDisabled": "当前部署不可用",
731
809
  "settings.readonly": "只读显示;修改请到平台设置页。",
732
- "settings.mergeApprovalPath": "设置 → 插件 → 自进化 → 技能自动合并",
733
810
  "run.title": "运行",
734
811
  "run.id": "运行编号",
735
812
  "run.state": "状态",
@@ -836,6 +913,8 @@ window.__ModuleLoader__.load({
836
913
  "skipKind.forced": "forced",
837
914
  "action.approve": "Copy approve",
838
915
  "action.reject": "Copy reject",
916
+ "decision.approve": "Approve",
917
+ "decision.reject": "Reject",
839
918
  "action.retry-candidates": "Copy retry candidates",
840
919
  "action.retry-resume": "Retry resume",
841
920
  "action.discard": "Copy discard",
@@ -910,7 +989,6 @@ window.__ModuleLoader__.load({
910
989
  "history.reason.archived": "archived",
911
990
  "history.reason.unknown": "reason not provided",
912
991
  "history.lastVersion": "Last version",
913
- "history.archivedAt": "Archived at",
914
992
  "body.truncated": "Body is truncated.",
915
993
  "body.empty": "Body is empty.",
916
994
  "body.totalBytes": "Total bytes",
@@ -931,7 +1009,6 @@ window.__ModuleLoader__.load({
931
1009
  "settings.capabilityMissing": "Host did not provide this capability",
932
1010
  "settings.capabilityDisabled": "Not available in this deployment",
933
1011
  "settings.readonly": "Read-only here; edit in platform settings.",
934
- "settings.mergeApprovalPath": "Settings → Plugins → Evolution → Skill auto-merge",
935
1012
  "run.title": "Run",
936
1013
  "run.id": "Run ID",
937
1014
  "run.state": "State",
@@ -970,14 +1047,14 @@ window.__ModuleLoader__.load({
970
1047
  "settings.saved": "已保存",
971
1048
  "settings.saveFailed": "保存失败",
972
1049
  "settings.openPlatform": "打开平台设置",
973
- "settings.openPlatformPath": "设置 → 内置插件 → 插件配置 → 技能自动合并",
1050
+ "settings.openPlatformPath": "设置 → 内置插件 → 自进化 → 技能自动合并",
974
1051
  "settings.live": "可直接在此保存;platformWrite 能力来自宿主。"
975
1052
  });
976
1053
  Object.assign(en, {
977
1054
  "settings.saved": "Saved",
978
1055
  "settings.saveFailed": "Save failed",
979
1056
  "settings.openPlatform": "Open platform settings",
980
- "settings.openPlatformPath": "Settings → Built-in plugins → Plugin configuration → Skill auto-merge",
1057
+ "settings.openPlatformPath": "Settings → Built-in plugins → Evolution → Skill auto-merge",
981
1058
  "settings.live": "Save directly here; platformWrite capability comes from the host."
982
1059
  });
983
1060
  Object.assign(zh, { "settings.platformWriteLinkHint": "此开关属于平台设置,这里只读;请到平台设置修改。" });
@@ -1146,9 +1223,9 @@ window.__ModuleLoader__.load({
1146
1223
  const value = workspaceId?.trim() ?? "";
1147
1224
  return value === "" ? "local" : value;
1148
1225
  }
1149
- /** Stable localStorage key for one workspace and queue. */
1150
- function rowOrderKey(workspaceId, queue) {
1151
- return "dsh-evolution-merge.row-order.v1:" + encodeURIComponent(resolveWorkspaceId(workspaceId)) + ":" + queue;
1226
+ /** Stable localStorage key for one workspace, queue and expired filter. */
1227
+ function rowOrderKey(workspaceId, queue, expiredOnly = false) {
1228
+ return "dsh-evolution-merge.row-order.v1:" + encodeURIComponent(resolveWorkspaceId(workspaceId)) + ":" + queue + (expiredOnly ? ":expired" : ":live");
1152
1229
  }
1153
1230
  function defaultStorage() {
1154
1231
  try {
@@ -1177,18 +1254,18 @@ window.__ModuleLoader__.load({
1177
1254
  /** One tab's persisted order; failures degrade to an empty (wire) order. */
1178
1255
  function createRowOrderStore(storage = defaultStorage()) {
1179
1256
  return {
1180
- read(workspaceId, queue) {
1257
+ read(workspaceId, queue, expiredOnly = false) {
1181
1258
  if (storage === void 0) return [];
1182
1259
  try {
1183
- return normalize(storage.getItem(rowOrderKey(workspaceId, queue)));
1260
+ return normalize(storage.getItem(rowOrderKey(workspaceId, queue, expiredOnly)));
1184
1261
  } catch {
1185
1262
  return [];
1186
1263
  }
1187
1264
  },
1188
- write(workspaceId, queue, ids) {
1265
+ write(workspaceId, queue, ids, expiredOnly = false) {
1189
1266
  if (storage === void 0) return;
1190
1267
  try {
1191
- storage.setItem(rowOrderKey(workspaceId, queue), JSON.stringify(ids));
1268
+ storage.setItem(rowOrderKey(workspaceId, queue, expiredOnly), JSON.stringify(ids));
1192
1269
  } catch {}
1193
1270
  }
1194
1271
  };
@@ -1343,8 +1420,12 @@ window.__ModuleLoader__.load({
1343
1420
  }, t("stage.state." + stage.state) + expiry);
1344
1421
  }
1345
1422
  function runNode(view, t, copy) {
1346
- const run = view.latestRun;
1347
- if (run === void 0) return null;
1423
+ const active = view.activeRun;
1424
+ const last = view.lastRun;
1425
+ if (active === void 0 && last === void 0) return null;
1426
+ return (0, react.createElement)("div", { "data-role": "run-strips" }, active === void 0 ? null : runStrip(active, t, copy, "active"), last === void 0 ? null : runStrip(last, t, copy, "last"));
1427
+ }
1428
+ function runStrip(run, t, copy, role) {
1348
1429
  const report = run.actions.find((action) => action.id === "report");
1349
1430
  const reportEnabled = report?.enabled === true;
1350
1431
  const internals = [
@@ -1382,6 +1463,7 @@ window.__ModuleLoader__.load({
1382
1463
  ];
1383
1464
  return (0, react.createElement)("div", {
1384
1465
  "data-role": "run-strip",
1466
+ "data-run-role": role,
1385
1467
  "data-run-state": run.state
1386
1468
  }, (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", {
1387
1469
  type: "button",
@@ -1420,14 +1502,12 @@ window.__ModuleLoader__.load({
1420
1502
  const platformCapability = platform?.capability === true;
1421
1503
  const platformFallback = platform?.enabled ?? false;
1422
1504
  const mergeFallback = view.approval.mergeApproval;
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, {
1505
+ 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)("p", { "data-role": "settings-path" }, t("settings.openPlatformPath")), (0, react.createElement)(ReadOnlySetting, {
1424
1506
  section: "merge-approval",
1425
1507
  label: t("settings.mergeApproval"),
1426
1508
  value: mergeFallback,
1427
1509
  disabled: false,
1428
1510
  missing: false,
1429
- note: t("settings.mergeApprovalPath"),
1430
- link: true,
1431
1511
  t
1432
1512
  }), (0, react.createElement)(ReadOnlySetting, {
1433
1513
  section: "platform-write",
@@ -1436,11 +1516,10 @@ window.__ModuleLoader__.load({
1436
1516
  disabled: platformCapability === false,
1437
1517
  missing: platform === void 0,
1438
1518
  note: t("settings.platformWriteLinkHint"),
1439
- link: true,
1440
1519
  t
1441
1520
  }), (0, react.createElement)("p", { "data-role": "settings-readonly" }, t("settings.readonly")));
1442
1521
  }
1443
- function rowNode(row, t, copy, sort, openBody) {
1522
+ function rowNode(row, t, copy, sort, openBody, onDecide, busy) {
1444
1523
  const first = [
1445
1524
  row.target === void 0 ? void 0 : t("row.target") + " " + row.target,
1446
1525
  row.failure === void 0 ? void 0 : t("row.failure") + " " + row.failure.code + (row.failure.retryable ? "" : " · " + t("row.noRetry")),
@@ -1455,19 +1534,35 @@ window.__ModuleLoader__.load({
1455
1534
  row.attempts === void 0 ? void 0 : t("row.attempts") + " " + String(row.attempts),
1456
1535
  row.confidence === void 0 ? void 0 : t("row.confidence") + " " + row.confidence.toFixed(2)
1457
1536
  ].filter((value) => value !== void 0).join(" · ");
1458
- const actions = row.actions.map((action) => (0, react.createElement)("button", {
1459
- key: action.id,
1460
- type: "button",
1461
- "data-role": "row-action",
1462
- "data-action": action.id,
1463
- ...action.disabledReason === void 0 ? {} : { "data-disabled-reason": action.disabledReason },
1464
- title: action.disabledReason === void 0 ? action.command : t("disabled." + action.disabledReason),
1465
- disabled: !action.enabled,
1466
- onClick: () => {
1467
- if (!action.enabled) return;
1468
- copy(action.command);
1469
- }
1470
- }, t("action." + action.id)));
1537
+ const actions = row.actions.flatMap((action) => {
1538
+ const decision = (action.id === "approve" || action.id === "reject") && onDecide !== void 0;
1539
+ const primary = (0, react.createElement)("button", {
1540
+ key: action.id,
1541
+ type: "button",
1542
+ "data-role": decision ? "row-decision" : "row-action",
1543
+ "data-action": action.id,
1544
+ ...action.disabledReason === void 0 ? {} : { "data-disabled-reason": action.disabledReason },
1545
+ title: action.command,
1546
+ disabled: !action.enabled || busy,
1547
+ onClick: () => {
1548
+ if (!action.enabled || busy) return;
1549
+ if (decision) onDecide(row.id, action.id);
1550
+ else copy(action.command);
1551
+ }
1552
+ }, decision ? t("decision." + action.id) : t("action." + action.id));
1553
+ if (!decision) return [primary];
1554
+ return [primary, (0, react.createElement)("button", {
1555
+ key: action.id + "-copy",
1556
+ type: "button",
1557
+ "data-role": "row-copy",
1558
+ "data-action": action.id + "-copy",
1559
+ title: action.command,
1560
+ disabled: busy,
1561
+ onClick: () => {
1562
+ if (!busy) copy(action.command);
1563
+ }
1564
+ }, t("row.copy"))];
1565
+ });
1471
1566
  return (0, react.createElement)("div", {
1472
1567
  key: row.id,
1473
1568
  "data-role": "row",
@@ -1507,7 +1602,16 @@ window.__ModuleLoader__.load({
1507
1602
  event.preventDefault();
1508
1603
  sort.menu(row.id, event.clientX, event.clientY);
1509
1604
  }
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" }, (() => {
1605
+ }, (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" }, (0, react.createElement)("button", {
1606
+ type: "button",
1607
+ "data-role": "row-sort",
1608
+ "aria-haspopup": "menu",
1609
+ title: t("sort.menu"),
1610
+ onClick: (event) => {
1611
+ const rect = event.currentTarget.getBoundingClientRect();
1612
+ sort.menu(row.id, rect.left, rect.bottom);
1613
+ }
1614
+ }, t("sort.menu")), (() => {
1511
1615
  const bodyA = row.bodies?.find((body) => body.name === row.a);
1512
1616
  const bodyB = row.bodies?.find((body) => body.name === row.b);
1513
1617
  return [bodyA === void 0 ? null : (0, react.createElement)("button", {
@@ -1624,6 +1728,8 @@ window.__ModuleLoader__.load({
1624
1728
  const [copyState, setCopyState] = (0, react.useState)({ kind: "idle" });
1625
1729
  const [orders, setOrders] = (0, react.useState)({});
1626
1730
  const [dragId, setDragId] = (0, react.useState)(void 0);
1731
+ const [deciding, setDeciding] = (0, react.useState)(void 0);
1732
+ const [decisionNotice, setDecisionNotice] = (0, react.useState)(void 0);
1627
1733
  const [menu, setMenu] = (0, react.useState)(void 0);
1628
1734
  const returnFocus = (0, react.useRef)(null);
1629
1735
  const captureFocus = () => {
@@ -1645,6 +1751,27 @@ window.__ModuleLoader__.load({
1645
1751
  };
1646
1752
  const workspaceId = resolveWorkspaceId(face.workspaceId);
1647
1753
  const orderStore = (0, react.useMemo)(() => createRowOrderStore(), []);
1754
+ const decideRow = (rowId, verb) => {
1755
+ if (face.decide === void 0) return;
1756
+ setDeciding(rowId);
1757
+ (async () => {
1758
+ try {
1759
+ setDecisionNotice({
1760
+ rowId,
1761
+ ...await face.decide(rowId, verb)
1762
+ });
1763
+ store.refresh();
1764
+ } catch (error) {
1765
+ setDecisionNotice({
1766
+ rowId,
1767
+ ok: false,
1768
+ message: error instanceof Error ? error.message : String(error)
1769
+ });
1770
+ } finally {
1771
+ setDeciding(void 0);
1772
+ }
1773
+ })();
1774
+ };
1648
1775
  const copy = (command) => {
1649
1776
  (async () => {
1650
1777
  try {
@@ -1694,15 +1821,15 @@ window.__ModuleLoader__.load({
1694
1821
  const viewFailure = snapshot.viewState === "error" ? snapshot.viewFailure : void 0;
1695
1822
  const queue = snapshot.current;
1696
1823
  const uiState = blocked !== void 0 ? "blocked" : viewFailure !== void 0 ? "error" : snapshot.viewState === "loading" ? "loading" : queue.status === "error" ? "error" : queue.status === "loading" ? "loading" : queue.status === "empty" ? "empty" : "ready";
1697
- const orderKey = rowOrderKey(workspaceId, snapshot.queue);
1698
- const visibleOrder = orders[orderKey] ?? orderStore.read(workspaceId, snapshot.queue);
1824
+ const orderKey = rowOrderKey(workspaceId, snapshot.queue, snapshot.expiredOnly);
1825
+ const visibleOrder = orders[orderKey] ?? orderStore.read(workspaceId, snapshot.queue, snapshot.expiredOnly);
1699
1826
  const visibleRows = orderRows(queue.rows, visibleOrder);
1700
1827
  const saveOrder = (ids) => {
1701
1828
  setOrders((previous) => ({
1702
1829
  ...previous,
1703
1830
  [orderKey]: ids
1704
1831
  }));
1705
- orderStore.write(workspaceId, snapshot.queue, ids);
1832
+ orderStore.write(workspaceId, snapshot.queue, ids, snapshot.expiredOnly);
1706
1833
  };
1707
1834
  const sortHandlers = {
1708
1835
  start: (id) => {
@@ -1774,11 +1901,15 @@ window.__ModuleLoader__.load({
1774
1901
  store.toggleExpired();
1775
1902
  }
1776
1903
  }, t("tab.expired") + " " + String(view.expired)) : null;
1904
+ const notice = decisionNotice === void 0 ? null : (0, react.createElement)("p", {
1905
+ "data-role": "decision-notice",
1906
+ "data-ok": decisionNotice.ok ? "true" : "false"
1907
+ }, decisionNotice.message);
1777
1908
  const list = uiState === "ready" && queue.status === "ready" ? (0, react.createElement)("div", {
1778
1909
  "data-role": "rows",
1779
1910
  id: "merge-rows",
1780
1911
  role: "tabpanel"
1781
- }, visibleRows.map((row) => rowNode(row, t, copy, sortHandlers, openBody))) : null;
1912
+ }, visibleRows.map((row) => rowNode(row, t, copy, sortHandlers, openBody, face.decide === void 0 ? void 0 : decideRow, deciding === row.id))) : null;
1782
1913
  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;
1783
1914
  const staleViewFailure = snapshot.viewState === "ready" ? snapshot.viewFailure : void 0;
1784
1915
  const activeFailure = viewFailure !== void 0 ? viewFailure : staleViewFailure !== void 0 ? staleViewFailure : queue.status === "error" ? queue.failure : void 0;
@@ -1868,7 +1999,7 @@ window.__ModuleLoader__.load({
1868
1999
  }, t("copied") + " " + (copyState.text ?? "")) : (0, react.createElement)("p", {
1869
2000
  "data-role": "copy-failed",
1870
2001
  role: "alert"
1871
- }, t("copyFailed")), list, empty, queue.nextCursor === void 0 || uiState !== "ready" ? null : (0, react.createElement)("button", {
2002
+ }, t("copyFailed")), notice, list, empty, queue.nextCursor === void 0 || uiState !== "ready" ? null : (0, react.createElement)("button", {
1872
2003
  type: "button",
1873
2004
  "data-role": "load-more",
1874
2005
  disabled: queue.status === "loading",
@@ -2253,8 +2384,19 @@ window.__ModuleLoader__.load({
2253
2384
  }
2254
2385
  };
2255
2386
  //#endregion
2387
+ //#region src/client/session.ts
2388
+ function currentSessionId(ctx) {
2389
+ const byId = ctx.sessions?.list?.getSnapshot?.().byId;
2390
+ if (byId === void 0) return void 0;
2391
+ return Object.values(byId).find((session) => (session.retainedBy?.mainView ?? 0) > 0)?.id;
2392
+ }
2393
+ //#endregion
2256
2394
  //#region src/client/index.ts
2257
- const inject = ["slots", "locale"];
2395
+ const inject = [
2396
+ "slots",
2397
+ "sessions",
2398
+ "locale"
2399
+ ];
2258
2400
  /** The view component receives the workbench's activeView marker alongside its face. */
2259
2401
  function MergeView(props) {
2260
2402
  return props.activeView === "merge" ? MergePanel(props) : null;
@@ -2264,6 +2406,10 @@ window.__ModuleLoader__.load({
2264
2406
  * @param ctx - the client context.
2265
2407
  */
2266
2408
  function apply(ctx) {
2409
+ const seat = createSettingsSeat(ctx);
2410
+ ctx.effect(() => () => {
2411
+ seat.dispose?.();
2412
+ }, "evolution-merge-ui: settings seat");
2267
2413
  registerWorkbenchView(ctx, {
2268
2414
  id: PANEL_ID,
2269
2415
  order: 45,
@@ -2284,7 +2430,6 @@ window.__ModuleLoader__.load({
2284
2430
  body,
2285
2431
  history
2286
2432
  };
2287
- const seat = createSettingsSeat(ctx);
2288
2433
  const workspaceId = panelWorkspaceId(ctx);
2289
2434
  return {
2290
2435
  store: new MergeStore(source),
@@ -2294,6 +2439,20 @@ window.__ModuleLoader__.load({
2294
2439
  ...seat.get("evolution-merge") === void 0 ? {} : { merge: seat.get("evolution-merge") },
2295
2440
  ...seat.get("evolution-approval") === void 0 ? {} : { platform: seat.get("evolution-approval") }
2296
2441
  },
2442
+ decide: async (rowId, verb) => {
2443
+ const sessionId = currentSessionId(ctx);
2444
+ if (sessionId === void 0) return {
2445
+ ok: false,
2446
+ message: "没有当前会话;请打开会话后重试"
2447
+ };
2448
+ const result = await decide(rowId, verb, sessionId);
2449
+ if (result.ok) return result.payload;
2450
+ const failure = result.failure;
2451
+ return {
2452
+ ok: false,
2453
+ message: "message" in failure && typeof failure.message === "string" ? failure.message : "审批请求失败"
2454
+ };
2455
+ },
2297
2456
  ...workspaceId === void 0 ? {} : { workspaceId }
2298
2457
  };
2299
2458
  }
@@ -9,6 +9,10 @@ export interface MergeFace {
9
9
  doctorCommand?: string;
10
10
  settings?: SettingsFace;
11
11
  workspaceId?: string;
12
+ decide?: (rowId: string, verb: 'approve' | 'reject') => Promise<{
13
+ ok: boolean;
14
+ message: string;
15
+ }>;
12
16
  }
13
17
  export declare function MergePanel(face: MergeFace): ReactNode;
14
18
  //# sourceMappingURL=Panel.d.ts.map
@@ -65,7 +65,8 @@ export interface MergeViewPayload {
65
65
  };
66
66
  notes: string[];
67
67
  degraded: boolean;
68
- latestRun?: MergeRunView;
68
+ activeRun?: MergeRunView;
69
+ lastRun?: MergeRunView;
69
70
  approval: MergeApproval;
70
71
  }
71
72
  export interface RowActionView {
@@ -177,6 +178,13 @@ export declare const isBlocked: (failure: MergeFailure) => failure is MergeBlock
177
178
  export declare function view(): Promise<MergeOutcome<MergeViewPayload>>;
178
179
  export declare function rows(queue: MergeQueue, cursor?: string, expiredOnly?: boolean): Promise<MergeOutcome<QueuePage>>;
179
180
  export declare function body(name: string, side: 'live' | 'archive', rowId?: string): Promise<MergeOutcome<MergeBody>>;
181
+ export interface MergeDecisionOutcome {
182
+ readonly ok: boolean;
183
+ readonly message: string;
184
+ readonly code?: string;
185
+ readonly runId?: string;
186
+ }
187
+ export declare function decide(rowId: string, verb: 'approve' | 'reject', sessionId: string): Promise<MergeOutcome<MergeDecisionOutcome>>;
180
188
  export declare function history(): Promise<MergeOutcome<{
181
189
  items: MergeHistoryItem[];
182
190
  }>>;
@@ -0,0 +1,2 @@
1
+ export declare function currentSessionId(ctx: unknown): string | undefined;
2
+ //# sourceMappingURL=session.d.ts.map
@@ -7,12 +7,12 @@ export interface StorageLike {
7
7
  export type RowOrderEdge = 'top' | 'bottom';
8
8
  /** Workspace scope for one tab. A missing panel workspace is the local scope. */
9
9
  export declare function resolveWorkspaceId(workspaceId: string | null | undefined): string;
10
- /** Stable localStorage key for one workspace and queue. */
11
- export declare function rowOrderKey(workspaceId: string | null | undefined, queue: MergeQueue): string;
10
+ /** Stable localStorage key for one workspace, queue and expired filter. */
11
+ export declare function rowOrderKey(workspaceId: string | null | undefined, queue: MergeQueue, expiredOnly?: boolean): string;
12
12
  /** One tab's persisted order; failures degrade to an empty (wire) order. */
13
13
  export declare function createRowOrderStore(storage?: StorageLike | undefined): {
14
- read(workspaceId: string | null | undefined, queue: MergeQueue): readonly string[];
15
- write(workspaceId: string | null | undefined, queue: MergeQueue, ids: readonly string[]): void;
14
+ read(workspaceId: string | null | undefined, queue: MergeQueue, expiredOnly?: boolean): readonly string[];
15
+ write(workspaceId: string | null | undefined, queue: MergeQueue, ids: readonly string[], expiredOnly?: boolean): void;
16
16
  };
17
17
  /** Apply persisted ids first, then append unknown rows in their wire order. */
18
18
  export declare function orderRows<T extends {
package/package.json CHANGED
@@ -11,7 +11,7 @@
11
11
  ]
12
12
  }
13
13
  },
14
- "version": "0.33.0",
14
+ "version": "0.34.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.33.0"
54
+ "@lmzhen/dsh-evolution-client-kit": "^0.34.0"
55
55
  },
56
56
  "engines": {
57
57
  "dsh": ">=0.2.1-rc.2 <0.2.2-0"