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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/client.js CHANGED
@@ -272,6 +272,25 @@ window.__ModuleLoader__.load({
272
272
  } catch {}
273
273
  }
274
274
  //#endregion
275
+ //#region ../evolution-client-kit/src/format.ts
276
+ /**
277
+ * Format an ISO string, epoch milliseconds, or null as local wall time.
278
+ * Invalid values return null; callers decide how to say "unknown".
279
+ * @param value - ISO string, epoch ms, or null.
280
+ * @param options - seconds on/off; defaults to on.
281
+ * @returns the formatted local time, or null when the value is absent/invalid.
282
+ */
283
+ function formatLocalDateTime(value, options = {}) {
284
+ if (value === null || value === void 0) return null;
285
+ const ms = typeof value === "number" ? value : Date.parse(value);
286
+ if (!Number.isFinite(ms)) return null;
287
+ const date = new Date(ms);
288
+ if (Number.isNaN(date.getTime())) return null;
289
+ const pad = (part) => String(part).padStart(2, "0");
290
+ const head = String(date.getFullYear()) + "-" + pad(date.getMonth() + 1) + "-" + pad(date.getDate()) + " " + pad(date.getHours()) + ":" + pad(date.getMinutes());
291
+ return options.seconds === false ? head : head + ":" + pad(date.getSeconds());
292
+ }
293
+ //#endregion
275
294
  //#region src/client/api.ts
276
295
  /** Read-only merge wire client. Classifies every failure and never writes. */
277
296
  const HOST_ROUTES = ROUTES.merge;
@@ -628,7 +647,7 @@ window.__ModuleLoader__.load({
628
647
  reason: item.reason,
629
648
  ...item.reasonCode === void 0 ? {} : { reasonCode: item.reasonCode },
630
649
  lastVersion: item.lastVersion,
631
- archivedAt: new Date(item.archivedAt).toISOString(),
650
+ archivedAt: item.archivedAt === null ? null : new Date(item.archivedAt).toISOString(),
632
651
  ...item.sourceRunId === void 0 ? {} : { sourceRunId: item.sourceRunId }
633
652
  })) }
634
653
  } : answer;
@@ -647,7 +666,9 @@ window.__ModuleLoader__.load({
647
666
  "degraded": "读模型降级:",
648
667
  "copied": "已复制:",
649
668
  "copyFailed": "复制失败:浏览器没有接受剪贴板写入。",
669
+ "copyRetry": "重试复制",
650
670
  "row.noSummary": "无摘要",
671
+ "row.summaryBoth": "双方摘要",
651
672
  "overlay.body": "正文",
652
673
  "hint": "手动触发;后台逐对判定。这里只读,不写技能。",
653
674
  "history": "历史",
@@ -691,6 +712,7 @@ window.__ModuleLoader__.load({
691
712
  "row.failure": "失败",
692
713
  "row.noRetry": "不可重试",
693
714
  "row.copy": "复制命令",
715
+ "row.more": "更多命令",
694
716
  "tab.expired": "已过期",
695
717
  "row.targetCreated": "新建",
696
718
  "row.verify": "判定状态",
@@ -730,6 +752,7 @@ window.__ModuleLoader__.load({
730
752
  "disabled.unavailable": "暂不可用",
731
753
  "history.empty": "没有历史。",
732
754
  "history.archivedAt": "归档于 {date}",
755
+ "history.archivedAtUnknown": "归档时间未知",
733
756
  "failureServer": "读取失败",
734
757
  "failureUnauthorized": "未登录",
735
758
  "failureForbidden": "无权访问",
@@ -808,6 +831,9 @@ window.__ModuleLoader__.load({
808
831
  "settings.capabilityDisabled": "当前部署不可用",
809
832
  "settings.readonly": "只读显示;修改请到平台设置页。",
810
833
  "run.title": "运行",
834
+ "run.progressUnknown": "进度未知",
835
+ "run.current": "当前运行",
836
+ "run.last": "上次完成",
811
837
  "run.id": "运行编号",
812
838
  "run.state": "状态",
813
839
  "run.stage": "阶段",
@@ -849,7 +875,9 @@ window.__ModuleLoader__.load({
849
875
  "degraded": "Degraded read model:",
850
876
  "copied": "Copied:",
851
877
  "copyFailed": "Copy failed: the browser did not accept the clipboard write.",
878
+ "copyRetry": "Retry copy",
852
879
  "row.noSummary": "No summary",
880
+ "row.summaryBoth": "Shared summary",
853
881
  "overlay.body": "Body",
854
882
  "hint": "Manual only; pairs are judged in the background. Read-only here.",
855
883
  "history": "History",
@@ -893,6 +921,7 @@ window.__ModuleLoader__.load({
893
921
  "row.failure": "Failure",
894
922
  "row.noRetry": "not retryable",
895
923
  "row.copy": "Copy command",
924
+ "row.more": "More commands",
896
925
  "tab.expired": "Expired",
897
926
  "row.targetCreated": "created",
898
927
  "row.verify": "Verify state",
@@ -932,6 +961,7 @@ window.__ModuleLoader__.load({
932
961
  "disabled.unavailable": "unavailable",
933
962
  "history.empty": "No history.",
934
963
  "history.archivedAt": "Archived {date}",
964
+ "history.archivedAtUnknown": "Archive time unknown",
935
965
  "failureServer": "Read failed",
936
966
  "failureUnauthorized": "Not signed in",
937
967
  "failureForbidden": "Forbidden",
@@ -1010,6 +1040,9 @@ window.__ModuleLoader__.load({
1010
1040
  "settings.capabilityDisabled": "Not available in this deployment",
1011
1041
  "settings.readonly": "Read-only here; edit in platform settings.",
1012
1042
  "run.title": "Run",
1043
+ "run.progressUnknown": "progress unknown",
1044
+ "run.current": "Current run",
1045
+ "run.last": "Last completed",
1013
1046
  "run.id": "Run ID",
1014
1047
  "run.state": "State",
1015
1048
  "run.stage": "Stage",
@@ -1091,7 +1124,8 @@ window.__ModuleLoader__.load({
1091
1124
  ".evolution-merge-panel [data-role=\"title\"] { margin:0; font-size:var(--evo-type-lg); font-weight:600; }",
1092
1125
  ".evolution-merge-panel [data-role=\"start\"] { display:flex; gap:var(--evo-space-6); flex-wrap:wrap; }",
1093
1126
  ".evolution-merge-panel [data-role=\"degraded\"] { margin:0; color:var(--dsw-alias-state-warn-primary); font-size:var(--evo-type-xs); }",
1094
- ".evolution-merge-panel [data-role=\"expired-toggle\"][data-active=\"true\"] { background:var(--dsw-alias-brand-primary); color:var(--dsw-alias-bg-base); }",
1127
+ ".evolution-merge-panel [data-role=\"expired-toggle\"] { display:inline-flex; align-items:center; gap:var(--evo-space-4); border:var(--evo-hairline-width) solid var(--dsw-alias-border-secondary); border-radius:var(--evo-radius-control); background:transparent; color:var(--dsw-alias-label-secondary); font:inherit; font-size:var(--evo-type-xs); padding:var(--evo-space-4) var(--evo-space-8); cursor:pointer; }",
1128
+ ".evolution-merge-panel [data-role=\"expired-toggle\"][data-active=\"true\"] { color:var(--dsw-alias-state-warn-primary); border-color:var(--dsw-alias-state-warn-primary); background:transparent; }",
1095
1129
  ".evolution-merge-panel [data-role=\"failure\"] { display:flex; flex-direction:column; gap:var(--evo-space-6); }",
1096
1130
  ".evolution-merge-panel [data-role=\"failure\"] [data-tone=\"fail\"] { color:var(--dsw-alias-state-error-primary); }",
1097
1131
  ".evolution-merge-panel [data-role=\"fallback\"] { display:flex; gap:var(--evo-space-6); flex-wrap:wrap; }",
@@ -1120,7 +1154,9 @@ window.__ModuleLoader__.load({
1120
1154
  ".evolution-merge-panel [data-role=\"settings-card\"] { display:flex; flex-direction:column; gap:var(--evo-space-6); padding:var(--evo-space-10); border:var(--evo-hairline-width) solid var(--dsw-alias-border-l2); border-radius:var(--evo-radius-control); background:var(--dsw-alias-bg-layer-1); }",
1121
1155
  ".evolution-merge-panel [data-role=\"settings-title\"] { margin:0; font-size:var(--evo-type-sm); }",
1122
1156
  ".evolution-merge-panel [data-role=\"settings-hint\"], .evolution-merge-panel [data-role=\"settings-readonly\"] { margin:0; color:var(--dsw-alias-label-tertiary); font-size:var(--evo-type-xs); }",
1123
- ".evolution-merge-panel [data-role=\"settings-section\"] { display:grid; grid-template-columns:minmax(0,1fr) auto auto; align-items:center; column-gap:var(--evo-space-8); }",
1157
+ ".evolution-merge-panel [data-role=\"settings-chips\"] { display:flex; flex-wrap:wrap; gap:var(--evo-space-6); }",
1158
+ ".evolution-merge-panel [data-role=\"settings-section\"] { display:inline-flex; align-items:center; gap:var(--evo-space-6); padding:var(--evo-space-2) var(--evo-space-8); border:var(--evo-hairline-width) solid var(--dsw-alias-border-l2); border-radius:var(--evo-radius-control); font-size:var(--evo-type-xs); }",
1159
+ ".evolution-merge-panel [data-role=\"settings-label\"] { color:var(--dsw-alias-label-secondary); }",
1124
1160
  ".evolution-merge-panel [data-role=\"settings-section\"][data-disabled=\"true\"] { color:var(--dsw-alias-label-tertiary); }",
1125
1161
  ".evolution-merge-panel [data-role=\"settings-toggle\"] { margin:0; }",
1126
1162
  ".evolution-merge-panel [data-role=\"settings-value\"] { font-weight:600; min-width:2.5em; text-align:right; }",
@@ -1305,121 +1341,14 @@ window.__ModuleLoader__.load({
1305
1341
  return edge === "top" ? [id, ...rest] : [...rest, id];
1306
1342
  }
1307
1343
  //#endregion
1308
- //#region src/client/fallbacks.ts
1309
- /** Static command-face fallback for a Facts read failure; the API fallback is unavailable then. */
1310
- const FALLBACK_COMMANDS = [
1311
- "/evolution merge scan",
1312
- "/evolution merge pair <a> <b>",
1313
- "/evolution merge pending",
1314
- "/evolution merge pending --expired",
1315
- "/evolution merge skipped",
1316
- "/evolution merge candidates",
1317
- "/evolution merge status",
1318
- "/evolution merge report",
1319
- "/evolution doctor"
1320
- ];
1321
- //#endregion
1322
- //#region src/client/Panel.ts
1323
- /** Batch 4 merge panel: keyed read store, four tabs, local overlays, localized errors. */
1324
- const QUEUES = [
1325
- "verify",
1326
- "pending",
1327
- "fuse",
1328
- "skipped"
1329
- ];
1330
- const DOCTOR_COMMAND = "/evolution doctor";
1331
- function problemKey(problem) {
1332
- switch (problem.code) {
1333
- case "unauthenticated": return "failureUnauthorized";
1334
- case "forbidden": return "failureForbidden";
1335
- case "unmounted": return "failureUnmounted";
1336
- case "capability": return "failureCapability";
1337
- case "body-ambiguous": return "failureBodyAmbiguous";
1338
- case "wire-version": return "failureWireVersion";
1339
- case "body-missing": return "failureBodyMissing";
1340
- case "bad-query": return "failureBadQuery";
1341
- case "refused": return "failureRefused";
1342
- case "server": return "failureServer";
1343
- }
1344
- }
1345
- function reasonText(t, reasonCode, fallback) {
1346
- if (reasonCode === void 0) return fallback;
1347
- const key = "history.reason." + reasonCode;
1348
- const text = t(key);
1349
- return text === key ? fallback : text;
1350
- }
1351
- function blockedReasonKey(failure) {
1352
- const code = failure.readiness.code ?? failure.code;
1353
- if (code.endsWith("needs-repair")) return "blocked.reason.needs-repair";
1354
- if (code.endsWith("forward-only")) return "blocked.reason.forward-only";
1355
- if (code.endsWith("migration-blocked")) return "blocked.reason.migration-blocked";
1356
- return "blocked.reason.unknown";
1357
- }
1358
- function failureNode(t, failure, fallback, copy) {
1359
- if (isBlocked(failure)) {
1360
- const raw = failure.readiness.message;
1361
- return (0, react.createElement)("div", {
1362
- "data-role": "blocked",
1363
- role: "alert",
1364
- "data-block-code": failure.code,
1365
- "data-readiness-code": failure.readiness.code ?? ""
1366
- }, (0, react.createElement)("h3", { "data-role": "blocked-title" }, t("blocked.title")), (0, react.createElement)("p", {
1367
- "data-role": "blocked-reason",
1368
- title: raw ?? t(blockedReasonKey(failure))
1369
- }, t(blockedReasonKey(failure))), (0, react.createElement)("p", { "data-role": "blocked-hint" }, t("blocked.hint")), raw === void 0 ? null : (0, react.createElement)("details", { "data-role": "blocked-detail" }, (0, react.createElement)("summary", null, t("blocked.detail")), (0, react.createElement)("p", { "data-role": "blocked-raw" }, raw)), (0, react.createElement)("div", { "data-role": "blocked-actions" }, (0, react.createElement)("button", {
1370
- type: "button",
1371
- "data-role": "doctor",
1372
- onClick: () => {
1373
- copy(DOCTOR_COMMAND);
1374
- }
1375
- }, t("blocked.doctor")), (0, react.createElement)("button", {
1376
- type: "button",
1377
- "data-role": "refresh",
1378
- onClick: () => {
1379
- window.dispatchEvent(new Event("evolution-merge:refresh"));
1380
- }
1381
- }, t("refresh"))));
1382
- }
1383
- const capability = failure.code === "capability";
1384
- return (0, react.createElement)("div", {
1385
- "data-role": capability ? "capability-failure" : "failure",
1386
- "data-error-code": failure.code,
1387
- "data-status": String(failure.status)
1388
- }, (0, react.createElement)("p", {
1389
- "data-role": "failure-title",
1390
- title: failure.message
1391
- }, t(problemKey(failure))), capability ? (0, react.createElement)("p", { "data-role": "capability-hint" }, t("failureCapabilityHint")) : null, failure.message === void 0 ? null : (0, react.createElement)("details", { "data-role": "failure-detail" }, (0, react.createElement)("summary", null, t("failureDetails")), (0, react.createElement)("p", { "data-role": "failure-raw" }, failure.message)), fallback.length === 0 ? null : (0, react.createElement)("div", { "data-role": "fallback" }, fallback.map((command) => (0, react.createElement)("button", {
1392
- key: command,
1393
- type: "button",
1394
- "data-role": "fallback-command",
1395
- onClick: () => {
1396
- copy(command);
1397
- }
1398
- }, command))));
1399
- }
1400
- function summaryNode(row, t) {
1401
- if (row.state !== "pending" || row.summary === void 0) return null;
1402
- return (0, react.createElement)("div", { "data-role": "pending-summary" }, [
1403
- ["row.summaryA", row.summary.a],
1404
- ["row.summaryB", row.summary.b],
1405
- ["row.rationale", row.summary.rationale]
1406
- ].map(([label, value]) => (0, react.createElement)("div", {
1407
- key: label,
1408
- "data-role": "pending-summary-slot"
1409
- }, (0, react.createElement)("span", { "data-role": "pending-summary-label" }, t(label)), (0, react.createElement)("span", { "data-role": "pending-summary-value" }, value === null || value === "" ? t("row.noSummary") : value))));
1410
- }
1411
- function stageNode(row, t) {
1412
- const stage = row.stageView;
1413
- if (stage === void 0) return null;
1414
- const tone = stage.state === "lost" ? "fail" : stage.state === "expired" || stage.state === "reserved" ? "warn" : "neutral";
1415
- const expiry = stage.expiresAt === void 0 ? "" : " · " + t("row.expires") + " " + new Date(stage.expiresAt).toLocaleString();
1416
- return (0, react.createElement)("span", {
1417
- "data-role": "stage-badge",
1418
- "data-stage": stage.state,
1419
- "data-tone": tone
1420
- }, t("stage.state." + stage.state) + expiry);
1421
- }
1422
- function runNode(view, t, copy) {
1344
+ //#region src/client/MergeRunStrip.ts
1345
+ /** The merge run strips: active/last identity, state, progress, report and details. */
1346
+ /**
1347
+ * Render active and last runs as two independently labelled strips.
1348
+ * @param props - view, translator and command copier.
1349
+ * @returns the strips, or null when neither run exists.
1350
+ */
1351
+ function MergeRunStrips({ view, t, copy }) {
1423
1352
  const active = view.activeRun;
1424
1353
  const last = view.lastRun;
1425
1354
  if (active === void 0 && last === void 0) return null;
@@ -1441,7 +1370,7 @@ window.__ModuleLoader__.load({
1441
1370
  run.endedAt === void 0 ? null : (0, react.createElement)("span", {
1442
1371
  key: "ended",
1443
1372
  "data-role": "run-ended"
1444
- }, t("run.endedAt") + " " + new Date(run.endedAt).toLocaleString()),
1373
+ }, t("run.endedAt") + " " + (formatLocalDateTime(run.endedAt) ?? "")),
1445
1374
  run.failure === void 0 ? null : (0, react.createElement)("span", {
1446
1375
  key: "failure",
1447
1376
  "data-role": "run-failure",
@@ -1465,7 +1394,10 @@ window.__ModuleLoader__.load({
1465
1394
  "data-role": "run-strip",
1466
1395
  "data-run-role": role,
1467
1396
  "data-run-state": run.state
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", {
1397
+ }, (0, react.createElement)("span", { "data-role": "run-identity" }, t(role === "active" ? "run.current" : "run.last")), (0, react.createElement)("span", { "data-role": "run-state" }, t("run.state") + " " + t("run." + run.state)), run.progress === null || run.progress === void 0 ? run.state === "succeeded" ? (0, react.createElement)("span", {
1398
+ "data-role": "run-progress",
1399
+ "data-progress": "unknown"
1400
+ }, t("run.progressUnknown")) : null : (0, react.createElement)("span", { "data-role": "run-progress" }, t("run.progress") + " " + String(run.progress.done) + "/" + String(run.progress.total)), (0, react.createElement)("button", {
1469
1401
  type: "button",
1470
1402
  "data-role": "run-report",
1471
1403
  "data-action": "report",
@@ -1486,45 +1418,68 @@ window.__ModuleLoader__.load({
1486
1418
  }
1487
1419
  }, t("action." + action.id))), (0, react.createElement)("details", { "data-role": "run-details" }, (0, react.createElement)("summary", null, t("run.details")), ...internals));
1488
1420
  }
1489
- function ReadOnlySetting({ section, label, value, disabled, missing, note, link, t }) {
1490
- const text = missing ? t("settings.capabilityMissing") : disabled ? t("settings.capabilityDisabled") : value === true ? t("settings.on") : t("settings.off");
1491
- return (0, react.createElement)("div", {
1492
- "data-role": "settings-section",
1493
- "data-section": section,
1494
- "data-disabled": missing || disabled ? "true" : "false"
1495
- }, (0, react.createElement)("span", { "data-role": "settings-label" }, label), (0, react.createElement)("span", {
1496
- "data-role": "settings-value",
1497
- "data-value": missing ? "missing" : disabled ? "unavailable" : value === true ? "on" : "off"
1498
- }, 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);
1421
+ //#endregion
1422
+ //#region src/client/merge-presentation.ts
1423
+ /**
1424
+ * Compare the two source summaries after trimming. Equal non-empty text is one fact;
1425
+ * missing text stays missing rather than being invented.
1426
+ * @param a - source A summary, or null.
1427
+ * @param b - source B summary, or null.
1428
+ * @returns the presentation form.
1429
+ */
1430
+ function summarizePair(a, b) {
1431
+ const left = a?.trim() ?? "";
1432
+ const right = b?.trim() ?? "";
1433
+ if (left === "" && right === "") return { kind: "missing" };
1434
+ if (left !== "" && left === right) return {
1435
+ kind: "same",
1436
+ text: left
1437
+ };
1438
+ return {
1439
+ kind: "different",
1440
+ a: left === "" ? null : left,
1441
+ b: right === "" ? null : right
1442
+ };
1499
1443
  }
1500
- function SettingsCard({ view, t }) {
1501
- const platform = view.approval.platformWrite;
1502
- const platformCapability = platform?.capability === true;
1503
- const platformFallback = platform?.enabled ?? false;
1504
- const mergeFallback = view.approval.mergeApproval;
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, {
1506
- section: "merge-approval",
1507
- label: t("settings.mergeApproval"),
1508
- value: mergeFallback,
1509
- disabled: false,
1510
- missing: false,
1511
- t
1512
- }), (0, react.createElement)(ReadOnlySetting, {
1513
- section: "platform-write",
1514
- label: t("settings.platformWrite"),
1515
- value: platformFallback,
1516
- disabled: platformCapability === false,
1517
- missing: platform === void 0,
1518
- note: t("settings.platformWriteLinkHint"),
1519
- t
1520
- }), (0, react.createElement)("p", { "data-role": "settings-readonly" }, t("settings.readonly")));
1444
+ //#endregion
1445
+ //#region src/client/MergeRow.ts
1446
+ /** One merge row: summary, stage, facts, primary decisions and the copy drawer. */
1447
+ function reasonText$1(t, reasonCode, fallback) {
1448
+ return reasonCode === void 0 ? fallback : t("history.reason." + reasonCode);
1521
1449
  }
1522
- function rowNode(row, t, copy, sort, openBody, onDecide, busy) {
1450
+ function summaryNode(row, t) {
1451
+ if (row.state !== "pending" || row.summary === void 0) return null;
1452
+ const pair = summarizePair(row.summary.a, row.summary.b);
1453
+ const slots = pair.kind === "same" ? [[t("row.summaryBoth"), pair.text]] : pair.kind === "missing" ? [[t("row.summaryBoth"), t("row.noSummary")]] : [[t("row.summaryA"), pair.a], [t("row.summaryB"), pair.b]];
1454
+ slots.push([t("row.rationale"), row.summary.rationale]);
1455
+ return (0, react.createElement)("div", { "data-role": "pending-summary" }, slots.map(([label, value]) => (0, react.createElement)("div", {
1456
+ key: label,
1457
+ "data-role": "pending-summary-slot"
1458
+ }, (0, react.createElement)("span", { "data-role": "pending-summary-label" }, label), (0, react.createElement)("span", { "data-role": "pending-summary-value" }, value === null || value === "" ? t("row.noSummary") : value))));
1459
+ }
1460
+ function stageNode(row, t) {
1461
+ const stage = row.stageView;
1462
+ if (stage === void 0) return null;
1463
+ const tone = stage.state === "lost" ? "fail" : stage.state === "expired" || stage.state === "reserved" ? "warn" : "neutral";
1464
+ const expiry = stage.expiresAt === void 0 ? "" : " · " + t("row.expires") + " " + new Date(stage.expiresAt).toLocaleString();
1465
+ return (0, react.createElement)("span", {
1466
+ "data-role": "stage-badge",
1467
+ "data-stage": stage.state,
1468
+ "data-tone": tone
1469
+ }, t("stage.state." + stage.state) + expiry);
1470
+ }
1471
+ /**
1472
+ * Render one row. Primary decisions stay visible; copy commands live in one details drawer.
1473
+ * @param props - row, translator, copier, sort hooks, body opener and decision callback.
1474
+ * @returns the row element.
1475
+ */
1476
+ function MergeRow(props) {
1477
+ const { row, t, copy, sort, openBody, onDecide, busy } = props;
1523
1478
  const first = [
1524
1479
  row.target === void 0 ? void 0 : t("row.target") + " " + row.target,
1525
1480
  row.failure === void 0 ? void 0 : t("row.failure") + " " + row.failure.code + (row.failure.retryable ? "" : " · " + t("row.noRetry")),
1526
1481
  row.expired === void 0 ? void 0 : t("row.expired") + " " + t("expired." + row.expired),
1527
- row.reasonCode === void 0 && row.reason === void 0 ? void 0 : t("row.reason") + " " + reasonText(t, row.reasonCode, row.reason ?? "")
1482
+ row.reasonCode === void 0 && row.reason === void 0 ? void 0 : t("row.reason") + " " + reasonText$1(t, row.reasonCode, row.reason ?? "")
1528
1483
  ].filter((value) => value !== void 0).join(" · ");
1529
1484
  const details = [
1530
1485
  t("row.id") + " " + row.id,
@@ -1534,9 +1489,9 @@ window.__ModuleLoader__.load({
1534
1489
  row.attempts === void 0 ? void 0 : t("row.attempts") + " " + String(row.attempts),
1535
1490
  row.confidence === void 0 ? void 0 : t("row.confidence") + " " + row.confidence.toFixed(2)
1536
1491
  ].filter((value) => value !== void 0).join(" · ");
1537
- const actions = row.actions.flatMap((action) => {
1492
+ const actions = row.actions.filter((action) => action.id !== "copy" && action.id !== "discard").map((action) => {
1538
1493
  const decision = (action.id === "approve" || action.id === "reject") && onDecide !== void 0;
1539
- const primary = (0, react.createElement)("button", {
1494
+ return (0, react.createElement)("button", {
1540
1495
  key: action.id,
1541
1496
  type: "button",
1542
1497
  "data-role": decision ? "row-decision" : "row-action",
@@ -1550,18 +1505,6 @@ window.__ModuleLoader__.load({
1550
1505
  else copy(action.command);
1551
1506
  }
1552
1507
  }, 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
1508
  });
1566
1509
  return (0, react.createElement)("div", {
1567
1510
  key: row.id,
@@ -1633,7 +1576,151 @@ window.__ModuleLoader__.load({
1633
1576
  openBody(bodyB.name, bodyB.side, bodyB.rowId);
1634
1577
  }
1635
1578
  }, "B · " + bodyB.name)];
1636
- })(), actions)));
1579
+ })(), actions), (0, react.createElement)("details", { "data-role": "row-more" }, (0, react.createElement)("summary", null, t("row.more")), ...row.actions.map((action) => (0, react.createElement)("button", {
1580
+ key: "copy-" + action.id,
1581
+ type: "button",
1582
+ "data-role": "row-copy",
1583
+ "data-action": action.id + "-copy",
1584
+ title: action.command,
1585
+ disabled: busy,
1586
+ onClick: () => {
1587
+ if (!busy) copy(action.command);
1588
+ }
1589
+ }, t("row.copy") + " " + t("action." + action.id))))));
1590
+ }
1591
+ //#endregion
1592
+ //#region src/client/MergeSettings.ts
1593
+ /** The merge settings summary: two read-only state chips and one settings path. */
1594
+ function SettingChip({ section, label, value, disabled, missing, t }) {
1595
+ const text = missing ? t("settings.capabilityMissing") : disabled ? t("settings.capabilityDisabled") : value === true ? t("settings.on") : t("settings.off");
1596
+ const state = missing ? "missing" : disabled ? "unavailable" : value === true ? "on" : "off";
1597
+ return (0, react.createElement)("span", {
1598
+ "data-role": "settings-section",
1599
+ "data-section": section,
1600
+ "data-disabled": missing || disabled ? "true" : "false",
1601
+ "data-state": state
1602
+ }, (0, react.createElement)("span", { "data-role": "settings-label" }, label), (0, react.createElement)("span", {
1603
+ "data-role": "settings-value",
1604
+ "data-value": state
1605
+ }, text));
1606
+ }
1607
+ /**
1608
+ * Render the read-only approval state without the old large settings card.
1609
+ * @param props - the merge view and translator.
1610
+ * @returns two chips and the one current settings path.
1611
+ */
1612
+ function MergeSettings({ view, t }) {
1613
+ const platform = view.approval.platformWrite;
1614
+ const platformCapability = platform?.capability === true;
1615
+ const platformFallback = platform?.enabled ?? false;
1616
+ 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-path" }, t("settings.openPlatformPath")), (0, react.createElement)("div", { "data-role": "settings-chips" }, (0, react.createElement)(SettingChip, {
1617
+ section: "merge-approval",
1618
+ label: t("settings.mergeApproval"),
1619
+ value: view.approval.mergeApproval,
1620
+ disabled: false,
1621
+ missing: false,
1622
+ t
1623
+ }), (0, react.createElement)(SettingChip, {
1624
+ section: "platform-write",
1625
+ label: t("settings.platformWrite"),
1626
+ value: platformFallback,
1627
+ disabled: platformCapability === false,
1628
+ missing: platform === void 0,
1629
+ t
1630
+ })));
1631
+ }
1632
+ //#endregion
1633
+ //#region src/client/fallbacks.ts
1634
+ /** Static command-face fallback for a Facts read failure; the API fallback is unavailable then. */
1635
+ const FALLBACK_COMMANDS = [
1636
+ "/evolution merge scan",
1637
+ "/evolution merge pair <a> <b>",
1638
+ "/evolution merge pending",
1639
+ "/evolution merge pending --expired",
1640
+ "/evolution merge skipped",
1641
+ "/evolution merge candidates",
1642
+ "/evolution merge status",
1643
+ "/evolution merge report",
1644
+ "/evolution doctor"
1645
+ ];
1646
+ //#endregion
1647
+ //#region src/client/Panel.ts
1648
+ /** Batch 4 merge panel: keyed read store, four tabs, local overlays, localized errors. */
1649
+ const QUEUES = [
1650
+ "verify",
1651
+ "pending",
1652
+ "fuse",
1653
+ "skipped"
1654
+ ];
1655
+ const DOCTOR_COMMAND = "/evolution doctor";
1656
+ function problemKey(problem) {
1657
+ switch (problem.code) {
1658
+ case "unauthenticated": return "failureUnauthorized";
1659
+ case "forbidden": return "failureForbidden";
1660
+ case "unmounted": return "failureUnmounted";
1661
+ case "capability": return "failureCapability";
1662
+ case "body-ambiguous": return "failureBodyAmbiguous";
1663
+ case "wire-version": return "failureWireVersion";
1664
+ case "body-missing": return "failureBodyMissing";
1665
+ case "bad-query": return "failureBadQuery";
1666
+ case "refused": return "failureRefused";
1667
+ case "server": return "failureServer";
1668
+ }
1669
+ }
1670
+ function reasonText(t, reasonCode, fallback) {
1671
+ if (reasonCode === void 0) return fallback;
1672
+ const key = "history.reason." + reasonCode;
1673
+ const text = t(key);
1674
+ return text === key ? fallback : text;
1675
+ }
1676
+ function blockedReasonKey(failure) {
1677
+ const code = failure.readiness.code ?? failure.code;
1678
+ if (code.endsWith("needs-repair")) return "blocked.reason.needs-repair";
1679
+ if (code.endsWith("forward-only")) return "blocked.reason.forward-only";
1680
+ if (code.endsWith("migration-blocked")) return "blocked.reason.migration-blocked";
1681
+ return "blocked.reason.unknown";
1682
+ }
1683
+ function failureNode(t, failure, fallback, copy) {
1684
+ if (isBlocked(failure)) {
1685
+ const raw = failure.readiness.message;
1686
+ return (0, react.createElement)("div", {
1687
+ "data-role": "blocked",
1688
+ role: "alert",
1689
+ "data-block-code": failure.code,
1690
+ "data-readiness-code": failure.readiness.code ?? ""
1691
+ }, (0, react.createElement)("h3", { "data-role": "blocked-title" }, t("blocked.title")), (0, react.createElement)("p", {
1692
+ "data-role": "blocked-reason",
1693
+ title: raw ?? t(blockedReasonKey(failure))
1694
+ }, t(blockedReasonKey(failure))), (0, react.createElement)("p", { "data-role": "blocked-hint" }, t("blocked.hint")), raw === void 0 ? null : (0, react.createElement)("details", { "data-role": "blocked-detail" }, (0, react.createElement)("summary", null, t("blocked.detail")), (0, react.createElement)("p", { "data-role": "blocked-raw" }, raw)), (0, react.createElement)("div", { "data-role": "blocked-actions" }, (0, react.createElement)("button", {
1695
+ type: "button",
1696
+ "data-role": "doctor",
1697
+ onClick: () => {
1698
+ copy(DOCTOR_COMMAND);
1699
+ }
1700
+ }, t("blocked.doctor")), (0, react.createElement)("button", {
1701
+ type: "button",
1702
+ "data-role": "refresh",
1703
+ onClick: () => {
1704
+ window.dispatchEvent(new Event("evolution-merge:refresh"));
1705
+ }
1706
+ }, t("refresh"))));
1707
+ }
1708
+ const capability = failure.code === "capability";
1709
+ return (0, react.createElement)("div", {
1710
+ "data-role": capability ? "capability-failure" : "failure",
1711
+ "data-error-code": failure.code,
1712
+ "data-status": String(failure.status)
1713
+ }, (0, react.createElement)("p", {
1714
+ "data-role": "failure-title",
1715
+ title: failure.message
1716
+ }, t(problemKey(failure))), capability ? (0, react.createElement)("p", { "data-role": "capability-hint" }, t("failureCapabilityHint")) : null, failure.message === void 0 ? null : (0, react.createElement)("details", { "data-role": "failure-detail" }, (0, react.createElement)("summary", null, t("failureDetails")), (0, react.createElement)("p", { "data-role": "failure-raw" }, failure.message)), fallback.length === 0 ? null : (0, react.createElement)("div", { "data-role": "fallback" }, fallback.map((command) => (0, react.createElement)("button", {
1717
+ key: command,
1718
+ type: "button",
1719
+ "data-role": "fallback-command",
1720
+ onClick: () => {
1721
+ copy(command);
1722
+ }
1723
+ }, command))));
1637
1724
  }
1638
1725
  const overlayKey = (event, onClose) => {
1639
1726
  if (event.key === "Escape") {
@@ -1712,7 +1799,7 @@ window.__ModuleLoader__.load({
1712
1799
  const kind = t("history.kind." + item.kind);
1713
1800
  const reason = reasonText(t, item.reasonCode, item.reason);
1714
1801
  return kind === reason ? kind : kind + " · " + reason;
1715
- })() + " · v" + String(item.lastVersion) + " · " + t("history.archivedAt", { date: new Date(item.archivedAt).toLocaleString() })))) : null);
1802
+ })() + " · v" + String(item.lastVersion) + " · " + (item.archivedAt === null ? t("history.archivedAtUnknown") : t("history.archivedAt", { date: formatLocalDateTime(item.archivedAt) ?? t("history.archivedAtUnknown") }))))) : null);
1716
1803
  return Modal === void 0 ? inner : (0, react.createElement)(Modal, {
1717
1804
  open: true,
1718
1805
  onClose,
@@ -1909,7 +1996,15 @@ window.__ModuleLoader__.load({
1909
1996
  "data-role": "rows",
1910
1997
  id: "merge-rows",
1911
1998
  role: "tabpanel"
1912
- }, visibleRows.map((row) => rowNode(row, t, copy, sortHandlers, openBody, face.decide === void 0 ? void 0 : decideRow, deciding === row.id))) : null;
1999
+ }, visibleRows.map((row) => (0, react.createElement)(MergeRow, {
2000
+ row,
2001
+ t,
2002
+ copy,
2003
+ sort: sortHandlers,
2004
+ openBody,
2005
+ onDecide: face.decide === void 0 ? void 0 : decideRow,
2006
+ busy: deciding === row.id
2007
+ }))) : null;
1913
2008
  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;
1914
2009
  const staleViewFailure = snapshot.viewState === "ready" ? snapshot.viewFailure : void 0;
1915
2010
  const activeFailure = viewFailure !== void 0 ? viewFailure : staleViewFailure !== void 0 ? staleViewFailure : queue.status === "error" ? queue.failure : void 0;
@@ -1987,19 +2082,29 @@ window.__ModuleLoader__.load({
1987
2082
  onClick: () => {
1988
2083
  copy(view.startCommands.pair);
1989
2084
  }
1990
- }, t("start.pair"))), view.degraded ? (0, react.createElement)("p", { "data-role": "degraded" }, t("degraded") + " " + view.notes.join(",")) : null, (0, react.createElement)(SettingsCard, {
2085
+ }, t("start.pair"))), view.degraded ? (0, react.createElement)("p", { "data-role": "degraded" }, t("degraded") + " " + view.notes.join(",")) : null, (0, react.createElement)(MergeSettings, {
1991
2086
  view,
1992
2087
  t
1993
- }), 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", {
2088
+ }), (0, react.createElement)(MergeRunStrips, {
2089
+ view,
2090
+ t,
2091
+ copy
2092
+ }), tabs, expiredToggle, snapshot.viewState === "loading" || queue.status === "loading" ? (0, react.createElement)("p", { "data-role": "note" }, t("loading")) : queue.refreshing === true ? (0, react.createElement)("p", {
1994
2093
  "data-role": "note",
1995
2094
  role: "status"
1996
2095
  }, t("refreshing")) : null, activeFailure === void 0 ? null : failureNode(t, activeFailure, fallback, copy), copyState.kind === "idle" ? null : copyState.kind === "copied" ? (0, react.createElement)("p", {
1997
2096
  "data-role": "copied",
1998
2097
  role: "status"
1999
- }, t("copied") + " " + (copyState.text ?? "")) : (0, react.createElement)("p", {
2098
+ }, t("copied") + " " + (copyState.text ?? "")) : (0, react.createElement)("div", {
2000
2099
  "data-role": "copy-failed",
2001
2100
  role: "alert"
2002
- }, t("copyFailed")), notice, list, empty, queue.nextCursor === void 0 || uiState !== "ready" ? null : (0, react.createElement)("button", {
2101
+ }, (0, react.createElement)("p", null, t("copyFailed")), copyState.text === void 0 ? null : (0, react.createElement)("code", { "data-role": "copy-failed-command" }, copyState.text), (0, react.createElement)("button", {
2102
+ type: "button",
2103
+ "data-role": "copy-retry",
2104
+ onClick: () => {
2105
+ if (copyState.text !== void 0) copy(copyState.text);
2106
+ }
2107
+ }, t("copyRetry"))), notice, list, empty, queue.nextCursor === void 0 || uiState !== "ready" ? null : (0, react.createElement)("button", {
2003
2108
  type: "button",
2004
2109
  "data-role": "load-more",
2005
2110
  disabled: queue.status === "loading",
@@ -0,0 +1,27 @@
1
+ /** One merge row: summary, stage, facts, primary decisions and the copy drawer. */
2
+ import { type DragEvent, type ReactNode } from 'react';
3
+ import type { MergeRowView } from './api.ts';
4
+ type Translate = (key: string, values?: Readonly<Record<string, string | number>>) => string;
5
+ export interface RowSortHandlers {
6
+ start(id: string): void;
7
+ over(event: DragEvent<HTMLDivElement>): void;
8
+ drop(id: string): void;
9
+ end(): void;
10
+ menu(id: string, x: number, y: number): void;
11
+ }
12
+ /**
13
+ * Render one row. Primary decisions stay visible; copy commands live in one details drawer.
14
+ * @param props - row, translator, copier, sort hooks, body opener and decision callback.
15
+ * @returns the row element.
16
+ */
17
+ export declare function MergeRow(props: {
18
+ row: MergeRowView;
19
+ t: Translate;
20
+ copy: (command: string) => void;
21
+ sort: RowSortHandlers;
22
+ openBody: (name: string, side: 'live' | 'archive', rowId: string | undefined) => void;
23
+ onDecide: ((rowId: string, verb: 'approve' | 'reject') => void) | undefined;
24
+ busy: boolean;
25
+ }): ReactNode;
26
+ export {};
27
+ //# sourceMappingURL=MergeRow.d.ts.map
@@ -0,0 +1,16 @@
1
+ /** The merge run strips: active/last identity, state, progress, report and details. */
2
+ import { type ReactNode } from 'react';
3
+ import type { MergeViewPayload } from './api.ts';
4
+ type Translate = (key: string, values?: Readonly<Record<string, string | number>>) => string;
5
+ /**
6
+ * Render active and last runs as two independently labelled strips.
7
+ * @param props - view, translator and command copier.
8
+ * @returns the strips, or null when neither run exists.
9
+ */
10
+ export declare function MergeRunStrips({ view, t, copy }: {
11
+ view: MergeViewPayload;
12
+ t: Translate;
13
+ copy: (command: string) => void;
14
+ }): ReactNode;
15
+ export {};
16
+ //# sourceMappingURL=MergeRunStrip.d.ts.map
@@ -0,0 +1,15 @@
1
+ /** The merge settings summary: two read-only state chips and one settings path. */
2
+ import { type ReactNode } from 'react';
3
+ import type { MergeViewPayload } from './api.ts';
4
+ type Translate = (key: string, values?: Readonly<Record<string, string | number>>) => string;
5
+ /**
6
+ * Render the read-only approval state without the old large settings card.
7
+ * @param props - the merge view and translator.
8
+ * @returns two chips and the one current settings path.
9
+ */
10
+ export declare function MergeSettings({ view, t }: {
11
+ view: MergeViewPayload;
12
+ t: Translate;
13
+ }): ReactNode;
14
+ export {};
15
+ //# sourceMappingURL=MergeSettings.d.ts.map
@@ -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 { 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;
@@ -144,7 +144,7 @@ export interface MergeHistoryItem {
144
144
  reason: string;
145
145
  reasonCode?: string;
146
146
  lastVersion: number;
147
- archivedAt: string;
147
+ archivedAt: string | null;
148
148
  sourceRunId?: string;
149
149
  }
150
150
  export interface MergeStateReadiness {
@@ -0,0 +1,20 @@
1
+ /** Pure presentation decisions for one merge row; no network or store state. */
2
+ export type PairSummary = {
3
+ readonly kind: 'same';
4
+ readonly text: string;
5
+ } | {
6
+ readonly kind: 'different';
7
+ readonly a: string | null;
8
+ readonly b: string | null;
9
+ } | {
10
+ readonly kind: 'missing';
11
+ };
12
+ /**
13
+ * Compare the two source summaries after trimming. Equal non-empty text is one fact;
14
+ * missing text stays missing rather than being invented.
15
+ * @param a - source A summary, or null.
16
+ * @param b - source B summary, or null.
17
+ * @returns the presentation form.
18
+ */
19
+ export declare function summarizePair(a: string | null | undefined, b: string | null | undefined): PairSummary;
20
+ //# sourceMappingURL=merge-presentation.d.ts.map
package/package.json CHANGED
@@ -11,7 +11,7 @@
11
11
  ]
12
12
  }
13
13
  },
14
- "version": "0.34.0",
14
+ "version": "0.35.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.34.0"
54
+ "@lmzhen/dsh-evolution-client-kit": "^0.35.0"
55
55
  },
56
56
  "engines": {
57
57
  "dsh": ">=0.2.1-rc.2 <0.2.2-0"