@lmzhen/dsh-evolution-merge-ui 0.30.17 → 0.30.19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/client.js CHANGED
@@ -5,14 +5,10 @@ window.__ModuleLoader__.load({
5
5
  var exports = module.exports;
6
6
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
7
7
  let react = require("react");
8
- //#region src/client/routes.generated.ts
9
- /**
10
- * Every family host path, generated from evolution-core's HOST_ROUTES (the ONE declaration).
11
- * A browser half cannot import the host package at runtime (client bundles keep @deepseek-ai/*
12
- * external), so the table is mirrored here mechanically; a rename in core regenerates all three
13
- * client copies, and the generator's --check makes a stale copy red.
14
- */
15
- const ROUTES = {
8
+ //#endregion
9
+ //#region src/client/api.ts
10
+ /** Read-only merge wire client. Classifies every failure and never writes. */
11
+ const HOST_ROUTES = {
16
12
  approval: {
17
13
  pending: "/api/dsh-evolution/approval/pending",
18
14
  preview: "/api/dsh-evolution/approval/preview",
@@ -31,56 +27,62 @@ window.__ModuleLoader__.load({
31
27
  merge: {
32
28
  view: "/api/dsh-evolution/merge/view",
33
29
  rows: "/api/dsh-evolution/merge/rows",
30
+ run: "/api/dsh-evolution/merge/run",
31
+ runs: "/api/dsh-evolution/merge/runs",
34
32
  body: "/api/dsh-evolution/merge/body",
35
33
  history: "/api/dsh-evolution/merge/history"
36
34
  },
37
35
  diagnostics: { view: "/api/dsh-evolution/diagnostics" }
38
- };
39
- //#endregion
40
- //#region src/client/route-client.generated.ts
41
- /** Classify a non-2xx status: 401/403/404 keep their own kind, everything else is a server answer. */
42
- function classify(status) {
43
- if (status === 401) return {
44
- kind: "unauthenticated",
45
- status: 401
46
- };
47
- if (status === 403) return {
48
- kind: "forbidden",
49
- status: 403
36
+ }.merge;
37
+ const isBlocked = (failure) => failure.kind === "blocked";
38
+ const asReadiness = (value) => {
39
+ const raw = typeof value === "object" && value !== null ? value : {};
40
+ const steps = Array.isArray(raw.steps) ? raw.steps.filter((step) => typeof step === "string") : void 0;
41
+ return {
42
+ ...typeof raw.ok === "boolean" ? { ok: raw.ok } : {},
43
+ ...typeof raw.code === "string" ? { code: raw.code } : {},
44
+ ...typeof raw.message === "string" ? { message: raw.message } : {},
45
+ ...steps === void 0 ? {} : { steps }
50
46
  };
51
- if (status === 404) return {
52
- kind: "unmounted",
53
- status: 404
47
+ };
48
+ const asPlatformWrite = (value) => {
49
+ if (typeof value === "boolean") return {
50
+ enabled: value,
51
+ capability: true
54
52
  };
55
- return {
56
- kind: "server",
57
- status
53
+ if (typeof value !== "object" || value === null) return void 0;
54
+ const raw = value;
55
+ const enabled = typeof raw.enabled === "boolean" ? raw.enabled : typeof raw.value === "boolean" ? raw.value : void 0;
56
+ const capability = typeof raw.capability === "boolean" ? raw.capability : typeof raw.available === "boolean" ? raw.available : true;
57
+ return enabled === void 0 ? void 0 : {
58
+ enabled,
59
+ capability
58
60
  };
59
- }
60
- /**
61
- * One request, no retries and no body parsing for the platform's admission answers.
62
- * @param doFetch - the fetch in use (the browser's, or a spec's stub).
63
- * @param path - the exact route path from the generated ROUTES table.
64
- * @param init - standard RequestInit.
65
- * @returns the data, or the classified failure (network/parse failures read as 'server').
66
- */
67
- async function requestJson(doFetch, path, init) {
61
+ };
62
+ const problem = (code, status, wireCode, message) => ({
63
+ kind: "problem",
64
+ code,
65
+ status,
66
+ ...wireCode === void 0 ? {} : { wireCode },
67
+ ...message === void 0 ? {} : { message }
68
+ });
69
+ async function requestWire(path) {
68
70
  let response;
69
71
  try {
70
- response = await doFetch(path, init);
71
- } catch (error) {
72
+ response = await fetch(path, { headers: { accept: "application/json" } });
73
+ } catch {
72
74
  return {
73
75
  ok: false,
74
- failure: {
75
- kind: "server",
76
- status: 0,
77
- detail: error instanceof Error ? error.message : String(error)
78
- }
76
+ failure: problem("server", 0)
79
77
  };
80
78
  }
81
- if (!response.ok) return {
79
+ if (response.status === 401) return {
80
+ ok: false,
81
+ failure: problem("unauthenticated", 401)
82
+ };
83
+ if (response.status === 403) return {
82
84
  ok: false,
83
- failure: classify(response.status)
85
+ failure: problem("forbidden", 403)
84
86
  };
85
87
  let body;
86
88
  try {
@@ -88,87 +90,250 @@ window.__ModuleLoader__.load({
88
90
  } catch {
89
91
  return {
90
92
  ok: false,
91
- failure: {
92
- kind: "server",
93
- status: response.status
94
- }
93
+ failure: problem("server", response.status)
95
94
  };
96
95
  }
97
- if (typeof body === "object" && body !== null) {
98
- const record = body;
99
- if (record.ok === true) return {
100
- ok: true,
101
- data: record.data
96
+ const record = body;
97
+ const code = typeof record.code === "string" ? record.code : void 0;
98
+ const message = typeof record.message === "string" ? record.message : void 0;
99
+ if (code === "E-323" || message?.startsWith("E-323") === true) return {
100
+ ok: false,
101
+ failure: problem("capability", response.status, code, message)
102
+ };
103
+ if (response.status === 503 && code === "merge-state-blocked" && typeof record.readiness === "object" && record.readiness !== null) return {
104
+ ok: false,
105
+ failure: {
106
+ kind: "blocked",
107
+ code,
108
+ readiness: asReadiness(record.readiness)
109
+ }
110
+ };
111
+ if (response.status === 404) {
112
+ if (code === "E-325") return {
113
+ ok: false,
114
+ failure: problem("unmounted", 404, code, message)
102
115
  };
103
- const nested = record.error;
104
- const errorRecord = typeof nested === "object" && nested !== null ? nested : void 0;
105
- const code = typeof record.code === "string" && record.code !== "" ? record.code : typeof errorRecord?.code === "string" ? errorRecord.code : void 0;
106
- const message = typeof record.message === "string" && record.message !== "" ? record.message : typeof errorRecord?.message === "string" ? errorRecord.message : void 0;
107
- if (record.ok === false && code !== void 0 && message !== void 0 && message !== "") return {
116
+ if (code === "merge-body-missing" || code === "merge-body-not-found") return {
108
117
  ok: false,
109
- failure: {
110
- kind: "refused",
111
- code,
112
- message
113
- }
118
+ failure: problem("body-missing", 404, code, message)
119
+ };
120
+ return {
121
+ ok: false,
122
+ failure: problem("unmounted", 404, code, message)
114
123
  };
115
124
  }
116
- return {
125
+ if (response.status === 400 && code === "merge-bad-query") return {
117
126
  ok: false,
118
- failure: {
119
- kind: "server",
120
- status: response.status
121
- }
127
+ failure: problem("bad-query", 400, code, message)
128
+ };
129
+ if (!response.ok) return {
130
+ ok: false,
131
+ failure: problem("server", response.status, code, message)
132
+ };
133
+ if (record.wireVersion !== 1) return {
134
+ ok: false,
135
+ failure: problem("wire-version", response.status, code, message)
136
+ };
137
+ if (code === "merge-body-missing" || code === "merge-body-not-found") return {
138
+ ok: false,
139
+ failure: problem("body-missing", response.status, code, message)
140
+ };
141
+ if (code !== void 0) return {
142
+ ok: false,
143
+ failure: problem("refused", response.status, code, message)
144
+ };
145
+ return {
146
+ ok: true,
147
+ payload: body
122
148
  };
123
149
  }
124
- //#endregion
125
- //#region src/client/api.ts
126
- /**
127
- * The two read routes this panel uses, through the generated client route table and the
128
- * shared route client. A refusal keeps the host's own sentence.
129
- */
130
- const HOST_ROUTES = ROUTES.merge;
150
+ const toPlatformWrite = (facts) => asPlatformWrite(facts.approval.platformWrite ?? facts.platformWrite);
151
+ const toRun = (run) => ({
152
+ id: run.id,
153
+ state: run.state,
154
+ ...run.command === void 0 ? {} : { command: run.command },
155
+ ...run.stage === void 0 ? {} : { stage: run.stage },
156
+ ...run.progress === null || run.progress === void 0 ? {} : { progress: {
157
+ done: run.progress.done,
158
+ total: run.progress.total
159
+ } },
160
+ startedAt: run.startedAt,
161
+ ...run.endedAt === void 0 ? {} : { endedAt: run.endedAt },
162
+ ...run.failure === void 0 ? {} : { failure: {
163
+ stage: run.failure.stage,
164
+ code: run.failure.code,
165
+ retryable: run.failure.retryable,
166
+ ...run.failure.detail === void 0 ? {} : { detail: run.failure.detail }
167
+ } },
168
+ ...run.parentRunId === void 0 ? {} : { parentRunId: run.parentRunId },
169
+ ...run.currentPairKey === void 0 ? {} : { currentPairKey: run.currentPairKey },
170
+ ...run.resultRef === void 0 ? {} : { resultRef: run.resultRef },
171
+ actions: (run.actions ?? []).map((action) => ({
172
+ id: action.id,
173
+ command: action.command,
174
+ confirm: action.confirm === true,
175
+ enabled: action.enabled !== false,
176
+ ...action.disabledReason === void 0 ? {} : { disabledReason: action.disabledReason }
177
+ }))
178
+ });
179
+ const stageOf = (row) => {
180
+ const normalize = (raw) => {
181
+ if (typeof raw === "string") return raw === "reserved" || raw === "present" || raw === "expired" || raw === "lost" || raw === "absent" ? { state: raw } : void 0;
182
+ if (raw === void 0) return void 0;
183
+ if (raw.state === "reserved" || raw.state === "present" || raw.state === "expired" || raw.state === "lost" || raw.state === "absent") return {
184
+ state: raw.state,
185
+ ...raw.expiresAt === void 0 ? {} : { expiresAt: raw.expiresAt }
186
+ };
187
+ if (raw.state === "kept") return {
188
+ state: "present",
189
+ ...raw.expiresAt === void 0 ? {} : { expiresAt: raw.expiresAt }
190
+ };
191
+ };
192
+ const stage = normalize(row.stage);
193
+ const staging = normalize(row.staging);
194
+ if (stage === void 0) return staging;
195
+ const expiresAt = stage.expiresAt ?? (staging?.state === stage.state ? staging.expiresAt : void 0);
196
+ return expiresAt === void 0 ? stage : {
197
+ ...stage,
198
+ expiresAt
199
+ };
200
+ };
201
+ const toRow = (row) => ({
202
+ id: row.id,
203
+ pairKey: row.id,
204
+ a: row.a,
205
+ b: row.b,
206
+ state: row.state,
207
+ queue: row.queue,
208
+ stage: null,
209
+ label: row.a + " + " + row.b,
210
+ updatedAt: new Date(row.updatedAt).toISOString(),
211
+ createdAt: new Date(row.createdAt).toISOString(),
212
+ ...row.target === void 0 ? {} : {
213
+ target: row.target.requested,
214
+ ...row.target.actual === void 0 ? {} : { targetActual: row.target.actual },
215
+ ...row.target.created === void 0 ? {} : { targetCreated: row.target.created }
216
+ },
217
+ ...row.confidence === void 0 ? {} : { confidence: row.confidence },
218
+ ...row.summary === void 0 ? {} : { summary: {
219
+ a: row.summary.a,
220
+ b: row.summary.b,
221
+ rationale: row.summary.rationale
222
+ } },
223
+ ...row.reason === void 0 ? {} : { reason: row.reason },
224
+ ...row.reasonCode === void 0 ? {} : { reasonCode: row.reasonCode },
225
+ ...row.origin === void 0 ? {} : { origin: row.origin },
226
+ ...row.attempts === void 0 ? {} : { attempts: row.attempts },
227
+ ...row.expiresAt === void 0 ? {} : { stagingExpiresAt: row.expiresAt },
228
+ ...row.expired === void 0 ? {} : { expired: row.expired.kind },
229
+ ...stageOf(row) === void 0 ? {} : { stageView: stageOf(row) },
230
+ ...row.failure === void 0 ? {} : { failure: {
231
+ stage: row.failure.stage,
232
+ code: row.failure.code,
233
+ message: row.failure.detail ?? row.failure.code,
234
+ retryable: row.failure.retryable
235
+ } },
236
+ ...row.verifySubState === void 0 ? {} : { verifySubState: row.verifySubState },
237
+ ...row.skipKind === void 0 ? {} : { skipKind: row.skipKind },
238
+ ...row.bodies === void 0 ? {} : { bodies: row.bodies.map((body) => ({
239
+ name: body.name,
240
+ side: body.side
241
+ })) },
242
+ actions: row.actions.map((action) => ({
243
+ id: action.id,
244
+ command: action.command,
245
+ confirm: action.confirm,
246
+ enabled: action.enabled,
247
+ ...action.disabledReason === void 0 ? {} : { disabledReason: action.disabledReason }
248
+ }))
249
+ });
131
250
  async function view() {
132
- const answer = await requestJson(fetch, HOST_ROUTES.view, { headers: { accept: "application/json" } });
133
- return answer.ok ? {
251
+ const answer = await requestWire(HOST_ROUTES.view);
252
+ if (!answer.ok) return answer;
253
+ const facts = answer.payload;
254
+ const platformWrite = toPlatformWrite(facts);
255
+ return {
134
256
  ok: true,
135
- payload: answer.data
136
- } : {
137
- ok: false,
138
- failure: answer.failure
257
+ payload: {
258
+ counts: facts.counts.active,
259
+ expired: facts.counts.expired,
260
+ tabs: [
261
+ "verify",
262
+ "pending",
263
+ "fuse",
264
+ "skipped"
265
+ ].map((queue) => ({
266
+ queue,
267
+ count: facts.counts.active[queue]
268
+ })),
269
+ commands: facts.fallback.map((item) => item.command),
270
+ startCommands: facts.startCommands,
271
+ notes: [...facts.notes],
272
+ degraded: facts.degraded,
273
+ ...facts.last === null && facts.active === null ? {} : { latestRun: toRun(facts.last ?? facts.active) },
274
+ approval: {
275
+ mergeApproval: facts.approval.mergeApproval,
276
+ ...platformWrite === void 0 ? {} : { platformWrite }
277
+ }
278
+ }
139
279
  };
140
280
  }
141
- async function rows(queue, cursor = 0) {
142
- const url = HOST_ROUTES.rows + "?queue=" + encodeURIComponent(queue) + "&cursor=" + String(cursor);
143
- const answer = await requestJson(fetch, url, { headers: { accept: "application/json" } });
144
- return answer.ok ? {
145
- ok: true,
146
- payload: answer.data
147
- } : {
281
+ async function rows(queue, cursor, expiredOnly = false) {
282
+ const answer = await requestWire(HOST_ROUTES.rows + "?queue=" + encodeURIComponent(queue) + (cursor === void 0 ? "" : "&cursor=" + encodeURIComponent(cursor)) + (expiredOnly ? "&expired=1" : ""));
283
+ if (!answer.ok) return answer;
284
+ if (answer.payload.queue !== queue || answer.payload.expiredOnly !== expiredOnly) return {
148
285
  ok: false,
149
- failure: answer.failure
286
+ failure: problem("server", 200)
287
+ };
288
+ return {
289
+ ok: true,
290
+ payload: {
291
+ queue: answer.payload.queue,
292
+ expiredOnly: answer.payload.expiredOnly,
293
+ items: answer.payload.rows.map(toRow),
294
+ counts: answer.payload.counts,
295
+ ...answer.payload.nextCursor === void 0 ? {} : { nextCursor: answer.payload.nextCursor }
296
+ }
150
297
  };
151
298
  }
152
- async function body(id, side) {
153
- const url = HOST_ROUTES.body + "?id=" + encodeURIComponent(id) + "&side=" + side;
154
- const answer = await requestJson(fetch, url, { headers: { accept: "application/json" } });
155
- return answer.ok ? {
299
+ async function body(name, side) {
300
+ const answer = await requestWire(HOST_ROUTES.body + "?name=" + encodeURIComponent(name) + "&side=" + side);
301
+ if (!answer.ok) {
302
+ if (answer.failure.kind === "problem" && answer.failure.code === "bad-query" && (answer.failure.message ?? "").includes("multiple merge rows")) return {
303
+ ok: false,
304
+ failure: {
305
+ ...answer.failure,
306
+ code: "body-ambiguous"
307
+ }
308
+ };
309
+ return answer;
310
+ }
311
+ return {
156
312
  ok: true,
157
- payload: answer.data
158
- } : {
159
- ok: false,
160
- failure: answer.failure
313
+ payload: {
314
+ name: answer.payload.name,
315
+ side: answer.payload.side,
316
+ text: answer.payload.text,
317
+ markdown: answer.payload.markdown,
318
+ truncated: answer.payload.truncated,
319
+ totalBytes: answer.payload.totalBytes
320
+ }
161
321
  };
162
322
  }
163
323
  async function history() {
164
- const answer = await requestJson(fetch, HOST_ROUTES.history, { headers: { accept: "application/json" } });
324
+ const answer = await requestWire(HOST_ROUTES.history);
165
325
  return answer.ok ? {
166
326
  ok: true,
167
- payload: answer.data
168
- } : {
169
- ok: false,
170
- failure: answer.failure
171
- };
327
+ payload: { items: answer.payload.items.map((item) => ({
328
+ name: item.name,
329
+ kind: item.kind,
330
+ reason: item.reason,
331
+ ...item.reasonCode === void 0 ? {} : { reasonCode: item.reasonCode },
332
+ lastVersion: item.lastVersion,
333
+ archivedAt: new Date(item.archivedAt).toISOString(),
334
+ ...item.sourceRunId === void 0 ? {} : { sourceRunId: item.sourceRunId }
335
+ })) }
336
+ } : answer;
172
337
  }
173
338
  //#endregion
174
339
  //#region src/client/seam.ts
@@ -178,6 +343,13 @@ window.__ModuleLoader__.load({
178
343
  const NS = "evolution-merge";
179
344
  const zh = {
180
345
  "entry.label": "技能合并",
346
+ "title": "技能合并",
347
+ "start.scan": "复制扫描命令",
348
+ "start.pair": "复制配对命令",
349
+ "degraded": "读模型降级:",
350
+ "copied": "已复制:",
351
+ "row.noSummary": "无摘要",
352
+ "overlay.body": "正文",
181
353
  "hint": "手动触发;后台逐对判定。这里只读,不写技能。",
182
354
  "history": "历史",
183
355
  "close": "关闭",
@@ -186,6 +358,7 @@ window.__ModuleLoader__.load({
186
358
  "tab.fuse": "待合并",
187
359
  "tab.skipped": "无需合并",
188
360
  "empty.verify": "没有待判断的对。",
361
+ "empty.scope": "这里只做两轮合成;机械追加属于 skill 管理,不进入合并队列。",
189
362
  "empty.pending": "没有待批准的合并。",
190
363
  "empty.fuse": "没有待合并的对。",
191
364
  "empty.skipped": "没有标记为无需合并的对。",
@@ -195,11 +368,20 @@ window.__ModuleLoader__.load({
195
368
  "approval.on": "审批开:合并前需人工批准。",
196
369
  "approval.off": "审批关:合并自动执行。",
197
370
  "failed": "读取失败,保留上次结果。",
371
+ "blocked.title": "合并状态未就绪",
372
+ "blocked.hint": "迁移/备份修复后刷新;面板不会自动降级。",
373
+ "blocked.doctor": "复制诊断命令",
374
+ "blocked.detail": "查看原始信息",
375
+ "blocked.reason.migration-blocked": "合并状态需要迁移,面板已暂停读取。",
376
+ "blocked.reason.needs-repair": "合并状态需要修复,面板已暂停读取。",
377
+ "blocked.reason.forward-only": "合并状态版本过新,当前面板暂不支持。",
378
+ "blocked.reason.unknown": "合并状态未就绪,面板已暂停读取。",
198
379
  "stage.decide": "判定",
199
380
  "stage.author": "合并",
200
381
  "stage.validate": "检查",
201
382
  "stage.commit": "提交",
202
383
  "row.target": "目标",
384
+ "row.actualTarget": "实际目标",
203
385
  "row.confidence": "置信度",
204
386
  "row.attempts": "尝试",
205
387
  "row.origin": "来源",
@@ -209,6 +391,59 @@ window.__ModuleLoader__.load({
209
391
  "row.failure": "失败",
210
392
  "row.noRetry": "不可重试",
211
393
  "row.copy": "复制命令",
394
+ "tab.expired": "已过期",
395
+ "row.targetCreated": "新建",
396
+ "row.verify": "判定状态",
397
+ "row.staging": "暂存",
398
+ "row.skipKind": "跳过类型",
399
+ "row.bodies": "正文",
400
+ "verify.queued": "排队",
401
+ "verify.running": "进行中",
402
+ "verify.failed": "失败",
403
+ "verify.cancelled": "已取消",
404
+ "staging.none": "无",
405
+ "staging.kept": "保留",
406
+ "staging.cleared-source-ok": "已清理(源完好)",
407
+ "staging.cleared-source-gone": "已清理(源已缺失)",
408
+ "skipKind.agent-keep": "自动保留",
409
+ "skipKind.human-reject": "人工拒绝",
410
+ "skipKind.over-limit": "超限",
411
+ "skipKind.forced": "强制",
412
+ "action.approve": "复制批准",
413
+ "action.reject": "复制拒绝",
414
+ "action.retry-candidates": "重试候选",
415
+ "action.retry-resume": "重试续跑",
416
+ "action.discard": "复制丢弃",
417
+ "action.restore": "恢复",
418
+ "action.pair": "复制配对",
419
+ "action.status": "状态",
420
+ "action.report": "报告",
421
+ "action.cancel": "取消",
422
+ "action.confirm": "确认复制",
423
+ "disabled.expired": "已过期,不可用",
424
+ "disabled.invalid": "状态不允许",
425
+ "disabled.not-retryable": "不可重试",
426
+ "disabled.source-gone": "源已缺失",
427
+ "disabled.owned": "已有运行",
428
+ "disabled.unavailable": "暂不可用",
429
+ "history.empty": "没有历史。",
430
+ "failureServer": "读取失败",
431
+ "failureUnauthorized": "未登录",
432
+ "failureForbidden": "无权访问",
433
+ "failureUnmounted": "宿主未挂载",
434
+ "failureCapability": "子代理通道不可用",
435
+ "failureCapabilityHint": "先运行全局诊断,或复制下方命令继续处理。preset/host-only 形态没有面板时仍可用这些命令兜底。",
436
+ "failureBodyAmbiguous": "同名技能匹配到多行,无法确定读取哪一个;请用唯一名称或从具体行打开。",
437
+ "fallbackHint": "可复制命令见下方",
438
+ "body.missing": "正文不存在或不可读。",
439
+ "expired.target-taken": "目标已被占用",
440
+ "expired.source-changed": "源已变更",
441
+ "expired.source-gone": "源已缺失",
442
+ "expired.skipped-ttl": "跳过已到期",
443
+ "history.kind.merged": "已合并",
444
+ "history.kind.deleted": "已删除",
445
+ "history.kind.split": "已拆分",
446
+ "history.kind.other": "其他",
212
447
  "origin.agent": "自动",
213
448
  "origin.human": "人工",
214
449
  "state.verify": "待判定",
@@ -217,8 +452,98 @@ window.__ModuleLoader__.load({
217
452
  "state.fuse": "待合并",
218
453
  "state.skipped": "已跳过"
219
454
  };
455
+ Object.assign(zh, {
456
+ "stage.screening": "筛选",
457
+ "stage.settle": "结算",
458
+ "stage.state.reserved": "已预留",
459
+ "stage.state.present": "已暂存",
460
+ "stage.state.expired": "已过期",
461
+ "stage.state.lost": "暂存丢失",
462
+ "stage.state.absent": "无暂存",
463
+ "origin.system": "系统",
464
+ "row.id": "编号",
465
+ "row.requestedTarget": "请求目标",
466
+ "row.stage": "合并暂存",
467
+ "row.summaryA": "摘要 A",
468
+ "row.summaryB": "摘要 B",
469
+ "row.rationale": "理由",
470
+ "row.notProvided": "未提供",
471
+ "row.fullId": "完整编号",
472
+ "row.actions": "操作",
473
+ "action.unavailable": "不可用",
474
+ "history.reason.target-taken": "目标已被占用",
475
+ "history.reason.source-changed": "源已变更",
476
+ "history.reason.skipped-ttl": "跳过已到期",
477
+ "history.reason.validation-failed": "检查失败",
478
+ "history.reason.merge-over-limit": "超过限制",
479
+ "history.reason.source-gone": "源已缺失",
480
+ "history.reason.merged": "已合并",
481
+ "history.reason.archived": "已归档",
482
+ "history.reason.unknown": "原因未提供",
483
+ "history.lastVersion": "最后版本",
484
+ "history.archivedAt": "归档时间",
485
+ "body.truncated": "正文过长,已截断。",
486
+ "body.empty": "正文为空。",
487
+ "body.totalBytes": "总字节",
488
+ "body.live": "当前版本",
489
+ "body.archive": "归档版本",
490
+ "failureWireVersion": "宿主返回了不兼容的数据版本,请升级页面。",
491
+ "failureBadQuery": "请求已失效,请刷新后重试。",
492
+ "failureBodyMissing": "正文不存在或不可读。",
493
+ "failureRefused": "宿主拒绝了这次读取。",
494
+ "failureDetails": "查看详情",
495
+ "failureFallback": "可复制命令见下方",
496
+ "settings.title": "审批设置",
497
+ "settings.hint": "两段审批分开显示;值来自宿主能力,不从另一项推导。",
498
+ "settings.mergeApproval": "合并审批",
499
+ "settings.platformWrite": "平台写入审批",
500
+ "settings.on": "开",
501
+ "settings.off": "关",
502
+ "settings.capabilityMissing": "宿主未提供该能力",
503
+ "settings.capabilityDisabled": "当前部署不可用",
504
+ "settings.readonly": "只读显示;修改请到平台设置页。",
505
+ "run.title": "运行",
506
+ "run.id": "运行编号",
507
+ "run.state": "状态",
508
+ "run.stage": "阶段",
509
+ "run.progress": "进度",
510
+ "run.endedAt": "结束时间",
511
+ "run.failure": "失败",
512
+ "run.currentPair": "当前对",
513
+ "run.parent": "父运行",
514
+ "run.report": "报告",
515
+ "run.noReport": "暂无报告",
516
+ "run.resultRef": "报告位置",
517
+ "run.actions": "运行操作",
518
+ "run.running": "运行中",
519
+ "run.ended": "已结束",
520
+ "run.cancelled": "已取消",
521
+ "run.failed": "失败",
522
+ "run.succeeded": "成功",
523
+ "run.awaiting": "等待审批",
524
+ "run.awaiting-approval": "等待审批",
525
+ "run.undone": "已撤销",
526
+ "run.superseded": "已失效",
527
+ "run.interrupted": "已中断",
528
+ "run.orphan": "孤立",
529
+ "error.title": "读取失败",
530
+ "error.retry": "重试",
531
+ "error.raw": "原始信息",
532
+ "doctor.open": "打开全局诊断",
533
+ "sort.updatedAt": "按更新时间",
534
+ "sort.score": "按分数",
535
+ "pin": "置顶",
536
+ "unpin": "取消置顶"
537
+ });
220
538
  const en = {
221
539
  "entry.label": "Skill merge",
540
+ "title": "Skill merge",
541
+ "start.scan": "Copy scan command",
542
+ "start.pair": "Copy pair command",
543
+ "degraded": "Degraded read model:",
544
+ "copied": "Copied:",
545
+ "row.noSummary": "No summary",
546
+ "overlay.body": "Body",
222
547
  "hint": "Manual only; pairs are judged in the background. Read-only here.",
223
548
  "history": "History",
224
549
  "close": "Close",
@@ -227,6 +552,7 @@ window.__ModuleLoader__.load({
227
552
  "tab.fuse": "Ready to merge",
228
553
  "tab.skipped": "Skipped",
229
554
  "empty.verify": "No pairs to judge.",
555
+ "empty.scope": "This panel only performs two-round synthesis; mechanical append belongs to skill management and does not enter the merge queue.",
230
556
  "empty.pending": "No pending merges.",
231
557
  "empty.fuse": "No pairs ready to merge.",
232
558
  "empty.skipped": "No pairs marked as not worth merging.",
@@ -236,11 +562,20 @@ window.__ModuleLoader__.load({
236
562
  "approval.on": "Approval on: merges wait for a human.",
237
563
  "approval.off": "Approval off: merges run automatically.",
238
564
  "failed": "Read failed; keeping the last result.",
565
+ "blocked.title": "Merge state blocked",
566
+ "blocked.hint": "Fix the migration/backup, then refresh. The panel does not fall back automatically.",
567
+ "blocked.doctor": "Copy diagnostics command",
568
+ "blocked.detail": "Show host message",
569
+ "blocked.reason.migration-blocked": "Merge state needs migration; the panel is paused.",
570
+ "blocked.reason.needs-repair": "Merge state needs repair; the panel is paused.",
571
+ "blocked.reason.forward-only": "Merge state was written by a newer version; this panel cannot read it.",
572
+ "blocked.reason.unknown": "Merge state is not ready; the panel is paused.",
239
573
  "stage.decide": "Decide",
240
574
  "stage.author": "Author",
241
575
  "stage.validate": "Validate",
242
576
  "stage.commit": "Commit",
243
577
  "row.target": "Target",
578
+ "row.actualTarget": "Actual target",
244
579
  "row.confidence": "Confidence",
245
580
  "row.attempts": "Attempts",
246
581
  "row.origin": "Origin",
@@ -250,6 +585,59 @@ window.__ModuleLoader__.load({
250
585
  "row.failure": "Failure",
251
586
  "row.noRetry": "not retryable",
252
587
  "row.copy": "Copy command",
588
+ "tab.expired": "Expired",
589
+ "row.targetCreated": "created",
590
+ "row.verify": "Verify state",
591
+ "row.staging": "Staging",
592
+ "row.skipKind": "Skip kind",
593
+ "row.bodies": "Bodies",
594
+ "verify.queued": "queued",
595
+ "verify.running": "running",
596
+ "verify.failed": "failed",
597
+ "verify.cancelled": "cancelled",
598
+ "staging.none": "none",
599
+ "staging.kept": "kept",
600
+ "staging.cleared-source-ok": "cleared (source ok)",
601
+ "staging.cleared-source-gone": "cleared (source gone)",
602
+ "skipKind.agent-keep": "agent keep",
603
+ "skipKind.human-reject": "human reject",
604
+ "skipKind.over-limit": "over limit",
605
+ "skipKind.forced": "forced",
606
+ "action.approve": "Copy approve",
607
+ "action.reject": "Copy reject",
608
+ "action.retry-candidates": "Retry candidates",
609
+ "action.retry-resume": "Retry resume",
610
+ "action.discard": "Copy discard",
611
+ "action.restore": "Restore",
612
+ "action.pair": "Copy pair",
613
+ "action.status": "Status",
614
+ "action.report": "Report",
615
+ "action.cancel": "Cancel",
616
+ "action.confirm": "Copy confirmation",
617
+ "disabled.expired": "expired",
618
+ "disabled.invalid": "invalid state",
619
+ "disabled.not-retryable": "not retryable",
620
+ "disabled.source-gone": "source gone",
621
+ "disabled.owned": "already running",
622
+ "disabled.unavailable": "unavailable",
623
+ "history.empty": "No history.",
624
+ "failureServer": "Read failed",
625
+ "failureUnauthorized": "Not signed in",
626
+ "failureForbidden": "Forbidden",
627
+ "failureUnmounted": "Host not mounted",
628
+ "failureCapability": "Subagent channel unavailable",
629
+ "failureCapabilityHint": "Run global diagnostics first, or copy a command below to continue. The command fallback also covers preset/host-only shapes without a panel.",
630
+ "failureBodyAmbiguous": "The skill name matches multiple rows, so the body is ambiguous. Use a unique name or open it from a specific row.",
631
+ "fallbackHint": "Copy the command below",
632
+ "body.missing": "Body missing or unreadable.",
633
+ "expired.target-taken": "target taken",
634
+ "expired.source-changed": "source changed",
635
+ "expired.source-gone": "source gone",
636
+ "expired.skipped-ttl": "skip expired",
637
+ "history.kind.merged": "Merged",
638
+ "history.kind.deleted": "Deleted",
639
+ "history.kind.split": "Split",
640
+ "history.kind.other": "Other",
253
641
  "origin.agent": "auto",
254
642
  "origin.human": "human",
255
643
  "state.verify": "Unverified",
@@ -258,6 +646,117 @@ window.__ModuleLoader__.load({
258
646
  "state.fuse": "Ready to merge",
259
647
  "state.skipped": "Skipped"
260
648
  };
649
+ Object.assign(en, {
650
+ "stage.screening": "Screen",
651
+ "stage.settle": "Settle",
652
+ "stage.state.reserved": "Reserved",
653
+ "stage.state.present": "Staged",
654
+ "stage.state.expired": "Expired",
655
+ "stage.state.lost": "Staging lost",
656
+ "stage.state.absent": "Not staged",
657
+ "origin.system": "System",
658
+ "row.id": "ID",
659
+ "row.requestedTarget": "Requested target",
660
+ "row.stage": "Merge staging",
661
+ "row.summaryA": "Summary A",
662
+ "row.summaryB": "Summary B",
663
+ "row.rationale": "Rationale",
664
+ "row.notProvided": "not provided",
665
+ "row.fullId": "Full ID",
666
+ "row.actions": "Actions",
667
+ "action.unavailable": "Unavailable",
668
+ "history.reason.target-taken": "target taken",
669
+ "history.reason.source-changed": "source changed",
670
+ "history.reason.skipped-ttl": "skip expired",
671
+ "history.reason.validation-failed": "validation failed",
672
+ "history.reason.merge-over-limit": "over limit",
673
+ "history.reason.source-gone": "source gone",
674
+ "history.reason.merged": "merged",
675
+ "history.reason.archived": "archived",
676
+ "history.reason.unknown": "reason not provided",
677
+ "history.lastVersion": "Last version",
678
+ "history.archivedAt": "Archived at",
679
+ "body.truncated": "Body is truncated.",
680
+ "body.empty": "Body is empty.",
681
+ "body.totalBytes": "Total bytes",
682
+ "body.live": "Live",
683
+ "body.archive": "Archive",
684
+ "failureWireVersion": "The host returned an incompatible data version; reload the page.",
685
+ "failureBadQuery": "This request is stale; refresh and retry.",
686
+ "failureBodyMissing": "Body missing or unreadable.",
687
+ "failureRefused": "The host refused this read.",
688
+ "failureDetails": "Show details",
689
+ "failureFallback": "Copy a command below",
690
+ "settings.title": "Approvals",
691
+ "settings.hint": "Two independent approval values; each comes from host capability, never derived from the other.",
692
+ "settings.mergeApproval": "Merge approval",
693
+ "settings.platformWrite": "Platform write approval",
694
+ "settings.on": "On",
695
+ "settings.off": "Off",
696
+ "settings.capabilityMissing": "Host did not provide this capability",
697
+ "settings.capabilityDisabled": "Not available in this deployment",
698
+ "settings.readonly": "Read-only here; edit in platform settings.",
699
+ "run.title": "Run",
700
+ "run.id": "Run ID",
701
+ "run.state": "State",
702
+ "run.stage": "Stage",
703
+ "run.progress": "Progress",
704
+ "run.endedAt": "Ended at",
705
+ "run.failure": "Failure",
706
+ "run.currentPair": "Current pair",
707
+ "run.parent": "Parent run",
708
+ "run.report": "Report",
709
+ "run.noReport": "No report yet",
710
+ "run.resultRef": "Report location",
711
+ "run.actions": "Run actions",
712
+ "run.running": "Running",
713
+ "run.ended": "Ended",
714
+ "run.cancelled": "Cancelled",
715
+ "run.failed": "Failed",
716
+ "run.succeeded": "Succeeded",
717
+ "run.awaiting": "Awaiting approval",
718
+ "run.awaiting-approval": "Awaiting approval",
719
+ "run.undone": "Undone",
720
+ "run.superseded": "Superseded",
721
+ "run.interrupted": "Interrupted",
722
+ "run.orphan": "Orphan",
723
+ "error.title": "Read failed",
724
+ "error.retry": "Retry",
725
+ "error.raw": "Raw message",
726
+ "doctor.open": "Open global diagnostics",
727
+ "sort.updatedAt": "Last updated",
728
+ "sort.score": "Score",
729
+ "pin": "Pin",
730
+ "unpin": "Unpin"
731
+ });
732
+ Object.assign(zh, {
733
+ "settings.saved": "已保存",
734
+ "settings.saveFailed": "保存失败",
735
+ "settings.openPlatform": "打开平台设置",
736
+ "settings.live": "可直接在此保存;platformWrite 能力来自宿主。"
737
+ });
738
+ Object.assign(en, {
739
+ "settings.saved": "Saved",
740
+ "settings.saveFailed": "Save failed",
741
+ "settings.openPlatform": "Open platform settings",
742
+ "settings.live": "Save directly here; platformWrite capability comes from the host."
743
+ });
744
+ Object.assign(zh, { "settings.platformWriteLinkHint": "此开关属于平台设置,这里只读;请到平台设置修改。" });
745
+ Object.assign(en, { "settings.platformWriteLinkHint": "This switch belongs to platform settings; this card is read-only. Change it in platform settings." });
746
+ Object.assign(zh, {
747
+ "sort.drag": "拖动排序",
748
+ "sort.menu": "行排序",
749
+ "sort.pinTop": "置顶",
750
+ "sort.pinBottom": "置底",
751
+ "sort.cancel": "取消"
752
+ });
753
+ Object.assign(en, {
754
+ "sort.drag": "Drag to reorder",
755
+ "sort.menu": "Row order",
756
+ "sort.pinTop": "Pin to top",
757
+ "sort.pinBottom": "Pin to bottom",
758
+ "sort.cancel": "Cancel"
759
+ });
261
760
  //#endregion
262
761
  //#region src/client/styles.ts
263
762
  const CSS_TAG_ID = "evolution-merge-panel";
@@ -272,266 +771,1051 @@ window.__ModuleLoader__.load({
272
771
  ".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); }",
273
772
  ".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; }",
274
773
  ".evolution-merge-panel [data-role=\"tab\"][data-active=\"true\"] { border-bottom-color:var(--dsw-alias-brand-primary); }",
774
+ ".evolution-merge-panel button:focus-visible { outline:2px solid currentColor !important; outline-offset:2px; box-shadow:0 0 0 2px currentColor !important; }",
775
+ ".evolution-merge-panel [data-role=\"title\"] { margin:0; font-size:var(--evo-type-lg); font-weight:600; }",
776
+ ".evolution-merge-panel [data-role=\"start\"] { display:flex; gap:var(--evo-space-6); flex-wrap:wrap; }",
777
+ ".evolution-merge-panel [data-role=\"degraded\"] { margin:0; color:var(--dsw-alias-state-warning-primary); font-size:var(--evo-type-xs); }",
778
+ ".evolution-merge-panel [data-role=\"expired-toggle\"][data-active=\"true\"] { background:var(--dsw-alias-brand-primary); color:var(--dsw-alias-bg-base); }",
779
+ ".evolution-merge-panel [data-role=\"failure\"] { display:flex; flex-direction:column; gap:var(--evo-space-6); }",
780
+ ".evolution-merge-panel [data-role=\"failure\"] [data-tone=\"fail\"] { color:var(--dsw-alias-state-error-primary); }",
781
+ ".evolution-merge-panel [data-role=\"fallback\"] { display:flex; gap:var(--evo-space-6); flex-wrap:wrap; }",
782
+ ".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); }",
783
+ ".evolution-merge-panel [data-role=\"overlay-head\"] { display:flex; align-items:center; justify-content:space-between; gap:var(--evo-space-8); }",
784
+ ".evolution-merge-panel [data-role=\"overlay-title\"] { margin:0; font-size:var(--evo-type-lg); }",
275
785
  ".evolution-merge-panel [data-role=\"rows\"] { display:flex; flex-direction:column; gap:var(--evo-space-6); }",
276
- ".evolution-merge-panel [data-role=\"row\"] { display:grid; grid-template-columns:1fr auto; gap:var(--evo-space-8); padding:var(--evo-space-8); border:var(--evo-hairline-width) solid var(--dsw-alias-border-l2); border-radius:var(--evo-radius-control); }",
786
+ ".evolution-merge-panel [data-role=\"row\"] { display:grid; grid-template-columns:1fr auto; gap:var(--evo-space-8); padding:var(--evo-space-8); border:var(--evo-hairline-width) solid var(--dsw-alias-border-l2); border-radius:var(--evo-radius-control); cursor:grab; }",
787
+ ".evolution-merge-panel [data-role=\"row\"]:active { cursor:grabbing; }",
277
788
  ".evolution-merge-panel [data-role=\"row-label\"] { font-weight:600; }",
278
789
  ".evolution-merge-panel [data-role=\"row-meta\"] { color:var(--dsw-alias-label-tertiary); font-size:var(--evo-type-xs); }",
279
790
  ".evolution-merge-panel [data-role=\"note\"] { color:var(--dsw-alias-label-tertiary); font-size:var(--evo-type-xs); }",
791
+ ".evolution-merge-panel [data-role=\"blocked\"] { display:flex; flex-direction:column; gap:var(--evo-space-8); padding:var(--evo-space-12); border:var(--evo-hairline-width) solid var(--dsw-alias-state-warning-primary); border-left-width:var(--evo-hairline-width); border-radius:var(--evo-radius-control); background:var(--dsw-alias-bg-layer-1); }",
792
+ ".evolution-merge-panel [data-role=\"blocked-title\"] { margin:0; font-size:var(--evo-type-lg); font-weight:600; color:var(--dsw-alias-state-warning-primary); }",
793
+ ".evolution-merge-panel [data-role=\"blocked-reason\"] { margin:0; white-space:pre-wrap; overflow-wrap:anywhere; }",
794
+ ".evolution-merge-panel [data-role=\"blocked-hint\"] { margin:0; color:var(--dsw-alias-label-tertiary); font-size:var(--evo-type-xs); }",
795
+ ".evolution-merge-panel [data-role=\"blocked-actions\"] { display:flex; gap:var(--evo-space-6); flex-wrap:wrap; }",
796
+ ".evolution-merge-panel [data-role=\"content\"] { display:flex; flex-direction:column; gap:var(--evo-space-12); }",
797
+ ".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); }",
798
+ ".evolution-merge-panel [data-role=\"settings-title\"] { margin:0; font-size:var(--evo-type-sm); }",
799
+ ".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); }",
800
+ ".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); }",
801
+ ".evolution-merge-panel [data-role=\"settings-section\"][data-disabled=\"true\"] { color:var(--dsw-alias-label-tertiary); }",
802
+ ".evolution-merge-panel [data-role=\"settings-toggle\"] { margin:0; }",
803
+ ".evolution-merge-panel [data-role=\"settings-value\"] { font-weight:600; min-width:2.5em; text-align:right; }",
804
+ ".evolution-merge-panel [data-role=\"settings-note\"] { grid-column:1/-1; color:var(--dsw-alias-label-tertiary); font-size:var(--evo-type-xs); }",
805
+ ".evolution-merge-panel [data-role=\"settings-link\"] { grid-column:1/-1; color:var(--dsw-alias-brand-primary); font-size:var(--evo-type-xs); }",
806
+ ".evolution-merge-panel [data-role=\"run-strip\"] { display:flex; gap:var(--evo-space-8); flex-wrap:wrap; align-items:center; font-size:var(--evo-type-xs); }",
807
+ ".evolution-merge-panel [data-role=\"run-failure\"] { color:var(--dsw-alias-state-error-primary); }",
808
+ ".evolution-merge-panel [data-role=\"stage-badge\"] { display:inline-block; margin-top:var(--evo-space-4); padding:0 var(--evo-space-4); border:var(--evo-hairline-width) solid var(--dsw-alias-border-l2); border-radius:var(--evo-radius-control); font-size:var(--evo-type-xs); }",
809
+ ".evolution-merge-panel [data-role=\"stage-badge\"][data-tone=\"warn\"] { color:var(--dsw-alias-state-warning-primary); }",
810
+ ".evolution-merge-panel [data-role=\"stage-badge\"][data-tone=\"fail\"] { color:var(--dsw-alias-state-error-primary); border-color:var(--dsw-alias-state-error-primary); }",
811
+ ".evolution-merge-panel [data-role=\"row-id\"] { color:var(--dsw-alias-label-tertiary); font-size:var(--evo-type-xs); }",
812
+ ".evolution-merge-panel [data-role=\"pending-summary\"] { display:flex; flex-direction:column; gap:var(--evo-space-4); margin-top:var(--evo-space-4); }",
813
+ ".evolution-merge-panel [data-role=\"pending-summary-slot\"] { display:flex; gap:var(--evo-space-6); }",
814
+ ".evolution-merge-panel [data-role=\"pending-summary-label\"] { color:var(--dsw-alias-label-tertiary); min-width:calc(var(--evo-space-16) * 2); }",
815
+ ".evolution-merge-panel [data-role=\"body-text\"] { white-space:pre-wrap; overflow-wrap:anywhere; max-height:50vh; overflow:auto; }",
816
+ ".evolution-merge-panel [data-role=\"overlay\"] { 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); }",
817
+ ".evolution-merge-panel button:focus-visible, .evolution-merge-panel a:focus-visible { outline:2px solid var(--dsw-alias-brand-primary); outline-offset:2px; }",
818
+ ".evolution-merge-panel [data-role=\"sort-menu\"] { position:fixed; z-index:70; display:flex; flex-direction:column; min-width:var(--evo-measure-read); padding:var(--evo-space-4); 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-6) var(--dsw-alias-border-l2); }",
819
+ ".evolution-merge-panel [data-role=\"sort-menu\"] button { text-align:left; }",
280
820
  ".evolution-merge-panel [data-role=\"refresh\"] { align-self:flex-start; }"
281
821
  ].join("\n");
282
822
  //#endregion
283
- //#region src/client/states.generated.ts
284
- /** A neutral/ warn/ fail block; a fail block is role=alert so assistive tech announces it. */
285
- function Block(props) {
286
- const tone = props.tone ?? "neutral";
287
- return (0, react.createElement)("div", {
288
- "data-tone": tone,
289
- ...tone === "fail" ? { role: "alert" } : {}
290
- }, props.title === void 0 ? null : (0, react.createElement)("div", { "data-tone-title": tone }, props.title), props.children);
291
- }
292
- /** One classified failure as a reader-facing sentence. */
293
- function failureText(failure, t) {
294
- switch (failure.kind) {
295
- case "unauthenticated": return t("failureUnauthorized");
296
- case "forbidden": return t("failureForbidden");
297
- case "unmounted": return t("failureUnmounted");
298
- case "server": return failure.status === 0 ? t("failureServer") : t("failureServer") + " (HTTP " + String(failure.status) + ")";
299
- case "refused": return failure.message;
823
+ //#region src/client/sorting.ts
824
+ /** Workspace scope for one tab. A missing panel workspace is the local scope. */
825
+ function resolveWorkspaceId(workspaceId) {
826
+ const value = workspaceId?.trim() ?? "";
827
+ return value === "" ? "local" : value;
828
+ }
829
+ /** Stable localStorage key for one workspace and queue. */
830
+ function rowOrderKey(workspaceId, queue) {
831
+ return "dsh-evolution-merge.row-order.v1:" + encodeURIComponent(resolveWorkspaceId(workspaceId)) + ":" + queue;
832
+ }
833
+ function defaultStorage() {
834
+ try {
835
+ return globalThis.localStorage;
836
+ } catch {
837
+ return;
300
838
  }
301
839
  }
302
- /** The failed-state block: sentence + (when the surface exists but is unreachable) the command fallback. */
303
- function FailureBlock(props) {
304
- const hint = props.fallback === void 0 ? null : (0, react.createElement)("div", { "data-tone-hint": "true" }, props.t("fallbackHint", { command: props.fallback.command }));
305
- return Block({
306
- tone: "fail",
307
- title: failureText(props.failure, props.t),
308
- children: hint
309
- });
840
+ function normalize(raw) {
841
+ if (raw === null) return [];
842
+ try {
843
+ const value = JSON.parse(raw);
844
+ if (!Array.isArray(value)) return [];
845
+ const ids = [];
846
+ const seen = /* @__PURE__ */ new Set();
847
+ for (const item of value) {
848
+ if (typeof item !== "string" || item === "" || seen.has(item)) continue;
849
+ seen.add(item);
850
+ ids.push(item);
851
+ }
852
+ return ids;
853
+ } catch {
854
+ return [];
855
+ }
310
856
  }
311
- //#endregion
312
- //#region src/client/fallbacks.generated.ts
313
- /**
314
- * The command-face fallback for every Host surface, generated from evolution-commands'
315
- * COMMAND_FALLBACKS (the ONE home). When the platform's connection carrier is absent (E-325) the
316
- * panels render this hint instead of a bare HTTP failure — same use case, command face.
317
- */
318
- const FALLBACKS = {
319
- approval: { command: "/evolution pending … /evolution approve <id> … /evolution reject <id> … /evolution release <id>" },
320
- "skill-history": { command: "/evolution restore <name> [v](查看:/evolution skills history <name>)" },
321
- "skill-health": { command: "/evolution skills refresh(健康行:/evolution doctor)" },
322
- merge: { command: "/evolution merge status / pending / approve <id> / report" },
323
- diagnostics: {
324
- command: "/evolution doctor",
325
- platformCli: "dsh --json /evolution doctor"
857
+ /** One tab's persisted order; failures degrade to an empty (wire) order. */
858
+ function createRowOrderStore(storage = defaultStorage()) {
859
+ return {
860
+ read(workspaceId, queue) {
861
+ if (storage === void 0) return [];
862
+ try {
863
+ return normalize(storage.getItem(rowOrderKey(workspaceId, queue)));
864
+ } catch {
865
+ return [];
866
+ }
867
+ },
868
+ write(workspaceId, queue, ids) {
869
+ if (storage === void 0) return;
870
+ try {
871
+ storage.setItem(rowOrderKey(workspaceId, queue), JSON.stringify(ids));
872
+ } catch {}
873
+ }
874
+ };
875
+ }
876
+ /** Apply persisted ids first, then append unknown rows in their wire order. */
877
+ function orderRows(rows, order) {
878
+ if (order.length === 0) return rows;
879
+ const byId = new Map(rows.map((row) => [row.id, row]));
880
+ const out = [];
881
+ const seen = /* @__PURE__ */ new Set();
882
+ for (const id of order) {
883
+ const row = byId.get(id);
884
+ if (row === void 0 || seen.has(id)) continue;
885
+ seen.add(id);
886
+ out.push(row);
326
887
  }
327
- };
888
+ for (const row of rows) if (!seen.has(row.id)) out.push(row);
889
+ return out;
890
+ }
891
+ /** Move one visible row before another; unknown ids leave the order unchanged. */
892
+ function moveRow(order, fromId, beforeId) {
893
+ if (!order.includes(fromId)) return order;
894
+ const rest = order.filter((id) => id !== fromId);
895
+ if (beforeId === void 0) return [...rest, fromId];
896
+ const index = rest.indexOf(beforeId);
897
+ if (index < 0) return [...rest, fromId];
898
+ return [
899
+ ...rest.slice(0, index),
900
+ fromId,
901
+ ...rest.slice(index)
902
+ ];
903
+ }
904
+ /** Pin one visible row to the top or bottom of the full visible order. */
905
+ function pinRow(order, id, edge) {
906
+ if (!order.includes(id)) return order;
907
+ const rest = order.filter((item) => item !== id);
908
+ return edge === "top" ? [id, ...rest] : [...rest, id];
909
+ }
910
+ //#endregion
911
+ //#region src/client/fallbacks.ts
912
+ /** Static command-face fallback for a Facts read failure; the API fallback is unavailable then. */
913
+ const FALLBACK_COMMANDS = [
914
+ "/evolution merge scan",
915
+ "/evolution merge pair <a> <b>",
916
+ "/evolution merge pending",
917
+ "/evolution merge pending --expired",
918
+ "/evolution merge skipped",
919
+ "/evolution merge candidates",
920
+ "/evolution merge status",
921
+ "/evolution merge report",
922
+ "/evolution doctor"
923
+ ];
328
924
  //#endregion
329
925
  //#region src/client/Panel.ts
330
- /**
331
- * The merge panel body: three queues, counts, and a read-only row list. Actions are
332
- * intentionally copied commands, not buttons that write from the browser.
333
- */
926
+ /** Batch 4 merge panel: keyed read store, four tabs, local overlays, localized errors. */
927
+ async function saveSetting(form, field, value) {
928
+ try {
929
+ return await form.set(field, value);
930
+ } catch {
931
+ return false;
932
+ }
933
+ }
334
934
  const QUEUES = [
335
935
  "verify",
336
936
  "pending",
337
937
  "fuse",
338
938
  "skipped"
339
939
  ];
340
- function failure(t, failure) {
341
- return FailureBlock({
342
- failure,
343
- t,
344
- fallback: FALLBACKS["merge"]
345
- });
940
+ const DOCTOR_COMMAND = "/evolution doctor";
941
+ function problemKey(problem) {
942
+ switch (problem.code) {
943
+ case "unauthenticated": return "failureUnauthorized";
944
+ case "forbidden": return "failureForbidden";
945
+ case "unmounted": return "failureUnmounted";
946
+ case "capability": return "failureCapability";
947
+ case "body-ambiguous": return "failureBodyAmbiguous";
948
+ case "wire-version": return "failureWireVersion";
949
+ case "body-missing": return "failureBodyMissing";
950
+ case "bad-query": return "failureBadQuery";
951
+ case "refused": return "failureRefused";
952
+ case "server": return "failureServer";
953
+ }
346
954
  }
347
- function MergePanel(face) {
348
- const t = face.t;
349
- const [view, setView] = (0, react.useState)(face.view);
350
- const [tab, setTab] = (0, react.useState)("pending");
351
- const [rows, setRows] = (0, react.useState)(face.rows);
352
- const [loading, setLoading] = (0, react.useState)(face.loadRows !== void 0 || face.loadView !== void 0);
353
- const [failed, setFailed] = (0, react.useState)(void 0);
354
- const [nextCursor, setNextCursor] = (0, react.useState)(void 0);
355
- const [overlay, setOverlay] = (0, react.useState)(void 0);
356
- const request = (0, react.useRef)(0);
357
- const loadRows = (queue, cursor = 0) => {
358
- const load = face.loadRows;
359
- if (load === void 0) return;
360
- const token = request.current + 1;
361
- request.current = token;
362
- setLoading(true);
363
- load(queue, cursor).then((outcome) => {
364
- if (token !== request.current) return;
365
- if (outcome.ok) {
366
- setRows((current) => [...cursor === 0 ? [] : current, ...outcome.payload.items].sort((a, b) => b.updatedAt.localeCompare(a.updatedAt)));
367
- setNextCursor(outcome.payload.nextCursor);
368
- setFailed(void 0);
369
- } else setFailed(outcome.failure);
370
- }).catch(() => {
371
- setFailed({
372
- kind: "server",
373
- status: 0
374
- });
375
- }).finally(() => {
376
- setLoading(false);
377
- });
955
+ function reasonText(t, reasonCode, fallback) {
956
+ if (reasonCode === void 0) return fallback;
957
+ const key = "history.reason." + reasonCode;
958
+ const text = t(key);
959
+ return text === key ? fallback : text;
960
+ }
961
+ function blockedReasonKey(failure) {
962
+ const code = failure.readiness.code ?? failure.code;
963
+ if (code.endsWith("needs-repair")) return "blocked.reason.needs-repair";
964
+ if (code.endsWith("forward-only")) return "blocked.reason.forward-only";
965
+ if (code.endsWith("migration-blocked")) return "blocked.reason.migration-blocked";
966
+ return "blocked.reason.unknown";
967
+ }
968
+ function failureNode(t, failure, fallback, copy) {
969
+ if (isBlocked(failure)) {
970
+ const raw = failure.readiness.message;
971
+ return (0, react.createElement)("div", {
972
+ "data-role": "blocked",
973
+ role: "alert",
974
+ "data-block-code": failure.code,
975
+ "data-readiness-code": failure.readiness.code ?? ""
976
+ }, (0, react.createElement)("h3", { "data-role": "blocked-title" }, t("blocked.title")), (0, react.createElement)("p", {
977
+ "data-role": "blocked-reason",
978
+ title: raw ?? t(blockedReasonKey(failure))
979
+ }, 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", {
980
+ type: "button",
981
+ "data-role": "doctor",
982
+ onClick: () => {
983
+ copy(DOCTOR_COMMAND);
984
+ }
985
+ }, t("blocked.doctor")), (0, react.createElement)("button", {
986
+ type: "button",
987
+ "data-role": "refresh",
988
+ onClick: () => {
989
+ window.dispatchEvent(new Event("evolution-merge:refresh"));
990
+ }
991
+ }, t("refresh"))));
992
+ }
993
+ const capability = failure.code === "capability";
994
+ return (0, react.createElement)("div", {
995
+ "data-role": capability ? "capability-failure" : "failure",
996
+ "data-error-code": failure.code,
997
+ "data-status": String(failure.status)
998
+ }, (0, react.createElement)("p", {
999
+ "data-role": "failure-title",
1000
+ title: failure.message
1001
+ }, 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", {
1002
+ key: command,
1003
+ type: "button",
1004
+ "data-role": "fallback-command",
1005
+ onClick: () => {
1006
+ copy(command);
1007
+ }
1008
+ }, command))));
1009
+ }
1010
+ function summaryNode(row, t) {
1011
+ if (row.state !== "pending" || row.summary === void 0) return null;
1012
+ return (0, react.createElement)("div", { "data-role": "pending-summary" }, [
1013
+ ["row.summaryA", row.summary.a],
1014
+ ["row.summaryB", row.summary.b],
1015
+ ["row.rationale", row.summary.rationale]
1016
+ ].map(([label, value]) => (0, react.createElement)("div", {
1017
+ key: label,
1018
+ "data-role": "pending-summary-slot"
1019
+ }, (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))));
1020
+ }
1021
+ function stageNode(row, t) {
1022
+ const stage = row.stageView;
1023
+ if (stage === void 0) return null;
1024
+ const tone = stage.state === "lost" ? "fail" : stage.state === "expired" || stage.state === "reserved" ? "warn" : "neutral";
1025
+ const expiry = stage.expiresAt === void 0 ? "" : " · " + t("row.expires") + " " + new Date(stage.expiresAt).toLocaleString();
1026
+ return (0, react.createElement)("span", {
1027
+ "data-role": "stage-badge",
1028
+ "data-stage": stage.state,
1029
+ "data-tone": tone
1030
+ }, t("stage.state." + stage.state) + expiry);
1031
+ }
1032
+ function runNode(view, t, copy) {
1033
+ const run = view.latestRun;
1034
+ if (run === void 0) return null;
1035
+ const report = run.actions.find((action) => action.id === "report");
1036
+ const reportEnabled = report?.enabled === true;
1037
+ return (0, react.createElement)("div", {
1038
+ "data-role": "run-strip",
1039
+ "data-run-state": run.state
1040
+ }, (0, react.createElement)("span", {
1041
+ "data-role": "run-id",
1042
+ title: run.id
1043
+ }, 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", {
1044
+ "data-role": "run-failure",
1045
+ title: run.failure.detail
1046
+ }, 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", {
1047
+ "data-role": "run-result-ref",
1048
+ title: run.resultRef
1049
+ }, t("run.resultRef")), (0, react.createElement)("button", {
1050
+ type: "button",
1051
+ "data-role": "run-report",
1052
+ "data-action": "report",
1053
+ disabled: !reportEnabled,
1054
+ title: reportEnabled ? report?.command ?? t("run.report") : t("run.noReport"),
1055
+ onClick: () => {
1056
+ if (reportEnabled && report !== void 0) copy(report.command);
1057
+ }
1058
+ }, t("run.report")), ...run.actions.filter((action) => action.id !== "report").map((action) => (0, react.createElement)("button", {
1059
+ key: action.id,
1060
+ type: "button",
1061
+ "data-role": "run-action",
1062
+ "data-action": action.id,
1063
+ disabled: !action.enabled,
1064
+ title: action.disabledReason === void 0 ? action.command : t("disabled." + action.disabledReason),
1065
+ onClick: () => {
1066
+ if (action.enabled) copy(action.command);
1067
+ }
1068
+ }, t("action." + action.id))));
1069
+ }
1070
+ function ReadOnlySetting({ section, label, value, disabled, missing, note, link, t }) {
1071
+ const text = missing ? t("settings.capabilityMissing") : disabled ? t("settings.capabilityDisabled") : value === true ? t("settings.on") : t("settings.off");
1072
+ return (0, react.createElement)("div", {
1073
+ "data-role": "settings-section",
1074
+ "data-section": section,
1075
+ "data-disabled": missing || disabled ? "true" : "false"
1076
+ }, (0, react.createElement)("span", { "data-role": "settings-label" }, label), (0, react.createElement)("span", {
1077
+ "data-role": "settings-value",
1078
+ "data-value": missing ? "missing" : disabled ? "unavailable" : value === true ? "on" : "off"
1079
+ }, text), note === void 0 ? null : (0, react.createElement)("span", { "data-role": "settings-note" }, note), link === true ? (0, react.createElement)("a", {
1080
+ "data-role": "settings-link",
1081
+ href: "#/settings",
1082
+ onClick: () => {
1083
+ window.dispatchEvent(new KeyboardEvent("keydown", {
1084
+ key: ",",
1085
+ code: "Comma",
1086
+ ctrlKey: true,
1087
+ altKey: true,
1088
+ bubbles: true
1089
+ }));
1090
+ }
1091
+ }, t("settings.openPlatform")) : null);
1092
+ }
1093
+ function EditableSetting({ form, field, label, fallback, capability, t }) {
1094
+ const subscribe = (0, react.useCallback)((listener) => form.subscribe(listener), [form]);
1095
+ const readSnapshot = (0, react.useCallback)(() => form["getSnapshot"](), [form]);
1096
+ const snapshot = (0, react.useSyncExternalStore)(subscribe, readSnapshot, readSnapshot);
1097
+ const fieldPresent = snapshot.status !== "ready" || snapshot.value === void 0 || Object.prototype.hasOwnProperty.call(snapshot.value, field);
1098
+ const [busy, setBusy] = (0, react.useState)(false);
1099
+ const [saved, setSaved] = (0, react.useState)(void 0);
1100
+ const value = snapshot.status === "ready" && typeof snapshot.value?.[field] === "boolean" ? snapshot.value[field] : fallback;
1101
+ const disabled = !capability || busy || snapshot.status !== "ready" || !snapshot.writable;
1102
+ const toggle = async () => {
1103
+ if (disabled) return;
1104
+ setBusy(true);
1105
+ setSaved(void 0);
1106
+ const ok = await saveSetting(form, field, !value);
1107
+ setBusy(false);
1108
+ setSaved(ok ? "saved" : "failed");
378
1109
  };
379
- const loadView = () => {
380
- const load = face.loadView;
381
- if (load === void 0) return;
382
- load().then((outcome) => {
383
- if (!outcome.ok) {
384
- setFailed(outcome.failure);
385
- return;
1110
+ if (!fieldPresent) return (0, react.createElement)(ReadOnlySetting, {
1111
+ section: "platform-write",
1112
+ label,
1113
+ value: fallback,
1114
+ disabled: !capability,
1115
+ missing: false,
1116
+ note: t("settings.platformWriteLinkHint"),
1117
+ link: true,
1118
+ t
1119
+ });
1120
+ return (0, react.createElement)("div", {
1121
+ "data-role": "settings-section",
1122
+ "data-section": field === "mergeApproval" ? "merge-approval" : "platform-write",
1123
+ ...disabled ? { "data-disabled": "true" } : {}
1124
+ }, (0, react.createElement)("label", { "data-role": "settings-label" }, label), (0, react.createElement)("input", {
1125
+ type: "checkbox",
1126
+ role: "switch",
1127
+ "data-role": "settings-toggle",
1128
+ "data-field": field,
1129
+ "data-value": value ? "on" : "off",
1130
+ checked: value,
1131
+ disabled,
1132
+ onChange: () => {
1133
+ toggle();
1134
+ }
1135
+ }), (0, react.createElement)("span", { "data-role": "settings-value" }, value ? t("settings.on") : t("settings.off")), ...saved === void 0 ? [] : [(0, react.createElement)("span", {
1136
+ "data-role": "settings-save-state",
1137
+ "data-save": saved
1138
+ }, t(saved === "saved" ? "settings.saved" : "settings.saveFailed"))]);
1139
+ }
1140
+ function SettingsCard({ view, settings, t }) {
1141
+ const platform = view.approval.platformWrite;
1142
+ const platformCapability = platform?.capability ?? settings?.platform !== void 0;
1143
+ const platformFallback = platform?.enabled ?? false;
1144
+ const mergeCapability = settings?.merge !== void 0;
1145
+ const mergeFallback = view.approval.mergeApproval;
1146
+ 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, {
1147
+ section: "merge-approval",
1148
+ label: t("settings.mergeApproval"),
1149
+ value: mergeFallback,
1150
+ disabled: false,
1151
+ missing: false,
1152
+ t
1153
+ }) : (0, react.createElement)(EditableSetting, {
1154
+ form: settings.merge,
1155
+ field: "mergeApproval",
1156
+ label: t("settings.mergeApproval"),
1157
+ fallback: mergeFallback,
1158
+ capability: mergeCapability,
1159
+ t
1160
+ }), settings?.platform === void 0 ? (0, react.createElement)(ReadOnlySetting, {
1161
+ section: "platform-write",
1162
+ label: t("settings.platformWrite"),
1163
+ value: platformFallback,
1164
+ disabled: platformCapability === false,
1165
+ missing: platform === void 0,
1166
+ note: t("settings.platformWriteLinkHint"),
1167
+ link: true,
1168
+ t
1169
+ }) : (0, react.createElement)(EditableSetting, {
1170
+ form: settings.platform,
1171
+ field: "enabled",
1172
+ label: t("settings.platformWrite"),
1173
+ fallback: platformFallback,
1174
+ capability: platformCapability,
1175
+ t
1176
+ }), settings?.merge === void 0 || settings?.platform === void 0 ? (0, react.createElement)("a", {
1177
+ "data-role": "settings-link",
1178
+ href: "#/settings"
1179
+ }, t("settings.openPlatform")) : null, (0, react.createElement)("p", { "data-role": "settings-readonly" }, settings?.merge === void 0 || settings?.platform === void 0 ? t("settings.readonly") : t("settings.live")));
1180
+ }
1181
+ function rowNode(row, store, t, copy, sort) {
1182
+ const facts = [
1183
+ row.target === void 0 ? void 0 : t("row.target") + " " + row.target,
1184
+ row.targetActual === void 0 ? void 0 : t("row.actualTarget") + " " + row.targetActual,
1185
+ row.verifySubState === void 0 ? void 0 : t("row.verify") + " " + t("verify." + row.verifySubState),
1186
+ row.origin === void 0 ? void 0 : t("row.origin") + " " + t("origin." + row.origin),
1187
+ row.attempts === void 0 ? void 0 : t("row.attempts") + " " + String(row.attempts),
1188
+ row.confidence === void 0 ? void 0 : t("row.confidence") + " " + row.confidence.toFixed(2),
1189
+ row.failure === void 0 ? void 0 : t("row.failure") + " " + row.failure.code + (row.failure.retryable ? "" : " · " + t("row.noRetry")),
1190
+ row.expired === void 0 ? void 0 : t("row.expired") + " " + t("expired." + row.expired),
1191
+ row.reasonCode === void 0 && row.reason === void 0 ? void 0 : t("row.reason") + " " + reasonText(t, row.reasonCode, row.reason ?? "")
1192
+ ].filter((value) => value !== void 0).join(" · ");
1193
+ const actions = row.actions.map((action) => (0, react.createElement)("button", {
1194
+ key: action.id,
1195
+ type: "button",
1196
+ "data-role": "row-action",
1197
+ "data-action": action.id,
1198
+ ...action.disabledReason === void 0 ? {} : { "data-disabled-reason": action.disabledReason },
1199
+ title: action.disabledReason === void 0 ? action.command : t("disabled." + action.disabledReason),
1200
+ disabled: !action.enabled,
1201
+ onClick: () => {
1202
+ if (!action.enabled) return;
1203
+ if (action.confirm && globalThis.confirm !== void 0 && !globalThis.confirm(t("action.confirm") + " " + (t("action." + action.id) || action.id))) return;
1204
+ copy(action.command);
1205
+ }
1206
+ }, t("action." + action.id)));
1207
+ return (0, react.createElement)("div", {
1208
+ key: row.id,
1209
+ "data-role": "row",
1210
+ "data-row-id": row.id,
1211
+ "data-row-state": row.state,
1212
+ "data-row-queue": row.queue,
1213
+ draggable: true,
1214
+ title: t("sort.drag"),
1215
+ onDragStart: (event) => {
1216
+ if (event.dataTransfer != null) {
1217
+ event.dataTransfer.setData("text/plain", row.id);
1218
+ event.dataTransfer.effectAllowed = "move";
386
1219
  }
387
- if (outcome.payload.schemaVersion !== void 0 && outcome.payload.schemaVersion !== 2) {
388
- setFailed({
389
- kind: "server",
390
- status: 0
391
- });
392
- return;
1220
+ sort.start(row.id);
1221
+ },
1222
+ onDragOver: (event) => {
1223
+ event.preventDefault();
1224
+ if (event.dataTransfer != null) event.dataTransfer.dropEffect = "move";
1225
+ sort.over(event);
1226
+ },
1227
+ onDrop: (event) => {
1228
+ event.preventDefault();
1229
+ sort.drop(row.id);
1230
+ },
1231
+ onDragEnd: () => {
1232
+ sort.end();
1233
+ },
1234
+ onContextMenu: (event) => {
1235
+ event.preventDefault();
1236
+ sort.menu(row.id, event.clientX, event.clientY);
1237
+ }
1238
+ }, (0, react.createElement)("div", null, (0, react.createElement)("div", { "data-role": "row-label" }, row.label), (0, react.createElement)("div", {
1239
+ "data-role": "row-id",
1240
+ title: row.id
1241
+ }, 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" }, (() => {
1242
+ const bodyA = row.bodies?.find((body) => body.name === row.a);
1243
+ const bodyB = row.bodies?.find((body) => body.name === row.b);
1244
+ return [bodyA === void 0 ? null : (0, react.createElement)("button", {
1245
+ key: "a",
1246
+ type: "button",
1247
+ "data-role": "body-a",
1248
+ onClick: () => {
1249
+ store.openBody(bodyA.name, bodyA.side);
393
1250
  }
394
- setView(outcome.payload);
395
- }).catch(() => {
396
- setFailed({
397
- kind: "server",
398
- status: 0
399
- });
400
- });
1251
+ }, "A"), bodyB === void 0 ? null : (0, react.createElement)("button", {
1252
+ key: "b",
1253
+ type: "button",
1254
+ "data-role": "body-b",
1255
+ onClick: () => {
1256
+ store.openBody(bodyB.name, bodyB.side);
1257
+ }
1258
+ }, "B")];
1259
+ })(), actions)));
1260
+ }
1261
+ function bodyNode(overlay, t, onClose) {
1262
+ return (0, react.createElement)("section", {
1263
+ "data-role": "overlay",
1264
+ "data-overlay": "body",
1265
+ role: "dialog",
1266
+ "aria-modal": "true",
1267
+ "aria-label": t("overlay.body")
1268
+ }, (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", {
1269
+ type: "button",
1270
+ "data-role": "overlay-close",
1271
+ onClick: onClose
1272
+ }, 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);
1273
+ }
1274
+ function historyNode(overlay, t, onClose) {
1275
+ return (0, react.createElement)("section", {
1276
+ "data-role": "overlay",
1277
+ "data-overlay": "history",
1278
+ role: "dialog",
1279
+ "aria-modal": "true",
1280
+ "aria-label": t("history")
1281
+ }, (0, react.createElement)("div", { "data-role": "overlay-head" }, (0, react.createElement)("h3", { "data-role": "overlay-title" }, t("history")), (0, react.createElement)("button", {
1282
+ type: "button",
1283
+ "data-role": "overlay-close",
1284
+ onClick: onClose
1285
+ }, 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", {
1286
+ key: item.name + ":" + item.archivedAt,
1287
+ "data-role": "history-item"
1288
+ }, item.name + " · " + (() => {
1289
+ const kind = t("history.kind." + item.kind);
1290
+ const reason = reasonText(t, item.reasonCode, item.reason);
1291
+ return kind === reason ? kind : kind + " · " + reason;
1292
+ })() + " · v" + String(item.lastVersion)))) : null);
1293
+ }
1294
+ function MergePanel(face) {
1295
+ const { store, t } = face;
1296
+ const snapshot = (0, react.useSyncExternalStore)(store.subscribe, store.read, store.read);
1297
+ const [copied, setCopied] = (0, react.useState)(void 0);
1298
+ const [orders, setOrders] = (0, react.useState)({});
1299
+ const [dragId, setDragId] = (0, react.useState)(void 0);
1300
+ const [menu, setMenu] = (0, react.useState)(void 0);
1301
+ const workspaceId = resolveWorkspaceId(face.workspaceId);
1302
+ const orderStore = (0, react.useMemo)(() => createRowOrderStore(), []);
1303
+ const copy = (command) => {
1304
+ navigator.clipboard.writeText(command);
1305
+ setCopied(command);
401
1306
  };
402
- const openBody = (id, side) => {
403
- const load = face.loadBody;
404
- if (load === void 0) return;
405
- load(id, side).then((outcome) => {
406
- if (outcome.ok) setOverlay({
407
- kind: "body",
408
- html: outcome.payload.html,
409
- label: id + ":" + side
410
- });
411
- else setFailed(outcome.failure);
412
- }).catch(() => {
413
- setFailed({
414
- kind: "server",
415
- status: 0
416
- });
417
- });
1307
+ const doctorCommand = face.doctorCommand ?? DOCTOR_COMMAND;
1308
+ (0, react.useEffect)(() => {
1309
+ store.start();
1310
+ return () => {
1311
+ store.dispose();
1312
+ };
1313
+ }, [store]);
1314
+ (0, react.useEffect)(() => {
1315
+ const onFocus = () => {
1316
+ store.refresh();
1317
+ };
1318
+ const onVisibility = () => {
1319
+ if (document.visibilityState === "visible") store.refresh();
1320
+ };
1321
+ const onRefresh = () => {
1322
+ store.refresh();
1323
+ };
1324
+ window.addEventListener("focus", onFocus);
1325
+ document.addEventListener("visibilitychange", onVisibility);
1326
+ window.addEventListener("evolution-merge:refresh", onRefresh);
1327
+ return () => {
1328
+ window.removeEventListener("focus", onFocus);
1329
+ document.removeEventListener("visibilitychange", onVisibility);
1330
+ window.removeEventListener("evolution-merge:refresh", onRefresh);
1331
+ };
1332
+ }, [store]);
1333
+ const view = snapshot.view;
1334
+ const fallback = [...new Set([...view.commands, ...FALLBACK_COMMANDS])];
1335
+ const blocked = snapshot.viewState === "blocked" && snapshot.viewFailure !== void 0 ? snapshot.viewFailure : void 0;
1336
+ const viewFailure = snapshot.viewState === "error" ? snapshot.viewFailure : void 0;
1337
+ const queue = snapshot.current;
1338
+ 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";
1339
+ const orderKey = rowOrderKey(workspaceId, snapshot.queue);
1340
+ const visibleOrder = orders[orderKey] ?? orderStore.read(workspaceId, snapshot.queue);
1341
+ const visibleRows = orderRows(queue.rows, visibleOrder);
1342
+ const saveOrder = (ids) => {
1343
+ setOrders((previous) => ({
1344
+ ...previous,
1345
+ [orderKey]: ids
1346
+ }));
1347
+ orderStore.write(workspaceId, snapshot.queue, ids);
418
1348
  };
419
- const openHistory = () => {
420
- const load = face.loadHistory;
421
- if (load === void 0) return;
422
- load().then((outcome) => {
423
- if (outcome.ok) setOverlay({
424
- kind: "history",
425
- items: outcome.payload.items
426
- });
427
- else setFailed(outcome.failure);
428
- }).catch(() => {
429
- setFailed({
430
- kind: "server",
431
- status: 0
1349
+ const sortHandlers = {
1350
+ start: (id) => {
1351
+ setDragId(id);
1352
+ },
1353
+ over: () => {},
1354
+ end: () => {
1355
+ setDragId(void 0);
1356
+ },
1357
+ drop: (targetId) => {
1358
+ const fromId = dragId;
1359
+ setDragId(void 0);
1360
+ if (fromId === void 0 || fromId === targetId) return;
1361
+ saveOrder(moveRow(visibleRows.map((row) => row.id), fromId, targetId));
1362
+ },
1363
+ menu: (id, x, y) => {
1364
+ setMenu({
1365
+ id,
1366
+ x,
1367
+ y
432
1368
  });
433
- });
1369
+ }
1370
+ };
1371
+ const pinMenu = (edge) => {
1372
+ if (menu === void 0) return;
1373
+ saveOrder(pinRow(visibleRows.map((row) => row.id), menu.id, edge));
1374
+ setMenu(void 0);
434
1375
  };
435
- (0, react.useEffect)(() => {
436
- loadView();
437
- loadRows("pending");
438
- }, [face]);
439
- (0, react.useEffect)(() => {
440
- setNextCursor(void 0);
441
- loadRows(tab, 0);
442
- }, [tab]);
443
- const counts = view.counts;
444
- const stats = (0, react.createElement)("div", { "data-role": "stats" }, QUEUES.map((queue) => (0, react.createElement)("div", {
445
- key: queue,
446
- "data-role": "stat",
447
- "data-queue": queue
448
- }, (0, react.createElement)("div", { "data-role": "stat-value" }, String(counts[queue])), (0, react.createElement)("div", { "data-role": "stat-label" }, t("tab." + queue)))));
449
- const latest = view.latestRun;
450
- const runStrip = latest === void 0 ? null : (0, react.createElement)("div", { "data-role": "run-strip" }, (0, react.createElement)("span", { "data-role": "run-id" }, latest.id), (0, react.createElement)("span", { "data-role": "run-state" }, latest.state), latest.stage === void 0 ? null : (0, react.createElement)("span", { "data-role": "run-stage" }, t("stage." + latest.stage)), latest.progress === void 0 ? null : (0, react.createElement)("span", { "data-role": "run-progress" }, String(latest.progress.done) + "/" + String(latest.progress.total)));
451
1376
  const tabs = (0, react.createElement)("div", {
452
1377
  "data-role": "tabs",
453
1378
  role: "tablist"
454
- }, QUEUES.map((queue) => (0, react.createElement)("button", {
455
- key: queue,
1379
+ }, QUEUES.map((item) => (0, react.createElement)("button", {
1380
+ key: item,
456
1381
  type: "button",
457
1382
  role: "tab",
458
- "aria-selected": tab === queue ? "true" : "false",
1383
+ "aria-selected": snapshot.queue === item ? "true" : "false",
459
1384
  "data-role": "tab",
460
- "data-active": tab === queue ? "true" : "false",
1385
+ "data-active": snapshot.queue === item ? "true" : "false",
461
1386
  onClick: () => {
462
- setTab(queue);
463
- }
464
- }, t("tab." + queue) + " " + String(counts[queue]))));
465
- const list = rows.length === 0 ? (0, react.createElement)("p", { "data-role": "note" }, t("empty." + tab)) : (0, react.createElement)("div", { "data-role": "rows" }, rows.map((row) => {
466
- const facts = [
467
- row.target === void 0 ? void 0 : t("row.target") + " " + row.target,
468
- row.confidence === void 0 ? void 0 : t("row.confidence") + " " + row.confidence.toFixed(2),
469
- row.attempts === void 0 ? void 0 : t("row.attempts") + " " + String(row.attempts),
470
- row.origin === void 0 ? void 0 : t("row.origin") + " " + t("origin." + row.origin),
471
- row.stagingExpiresAt === void 0 ? void 0 : t("row.expires") + " " + new Date(row.stagingExpiresAt).toLocaleString(),
472
- row.expired === void 0 ? void 0 : t("row.expired") + " " + row.expired,
473
- row.reason === void 0 ? void 0 : t("row.reason") + " " + row.reason,
474
- row.summary === void 0 ? void 0 : row.summary.filter(Boolean).join(" · "),
475
- row.failure === void 0 ? void 0 : t("row.failure") + " " + row.failure.message + (row.failure.retryable ? "" : " · " + t("row.noRetry"))
476
- ].filter((value) => value !== void 0).join(" · ");
477
- return (0, react.createElement)("div", {
478
- key: row.id,
479
- "data-role": "row"
480
- }, (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)) + " · " + row.id), facts === "" ? null : (0, react.createElement)("div", { "data-role": "row-facts" }, facts)), (0, react.createElement)("div", null, (0, react.createElement)("div", { "data-role": "row-meta" }, row.updatedAt), (0, react.createElement)("div", { "data-role": "row-actions" }, face.loadBody === void 0 ? null : (0, react.createElement)("button", {
481
- type: "button",
482
- "data-role": "body-a",
483
- onClick: () => {
484
- openBody(row.id, "a");
485
- }
486
- }, "A"), face.loadBody === void 0 ? null : (0, react.createElement)("button", {
487
- type: "button",
488
- "data-role": "body-b",
489
- onClick: () => {
490
- openBody(row.id, "b");
491
- }
492
- }, "B"), (row.actions ?? []).filter((action) => view.approval !== false || !action.includes("/evolution merge approve")).map((action) => (0, react.createElement)("button", {
493
- key: action,
494
- type: "button",
495
- "data-role": "row-action",
496
- title: action,
497
- onClick: () => {
498
- navigator.clipboard.writeText(action);
499
- }
500
- }, t("row.copy"))))));
501
- }));
1387
+ setMenu(void 0);
1388
+ store.select(item);
1389
+ }
1390
+ }, t("tab." + item) + " " + String(view.counts[item]))));
1391
+ const expiredToggle = snapshot.queue === "pending" ? (0, react.createElement)("button", {
1392
+ type: "button",
1393
+ role: "switch",
1394
+ "aria-checked": snapshot.expiredOnly ? "true" : "false",
1395
+ "data-role": "expired-toggle",
1396
+ "data-active": snapshot.expiredOnly ? "true" : "false",
1397
+ onClick: () => {
1398
+ store.toggleExpired();
1399
+ }
1400
+ }, t("tab.expired") + " " + String(view.expired)) : null;
1401
+ const list = uiState === "ready" && queue.status === "ready" ? (0, react.createElement)("div", { "data-role": "rows" }, visibleRows.map((row) => rowNode(row, store, t, copy, sortHandlers))) : null;
1402
+ 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;
1403
+ const activeFailure = viewFailure !== void 0 ? viewFailure : queue.status === "error" ? queue.failure : void 0;
1404
+ const overlay = snapshot.overlay;
1405
+ const menuNode = menu === void 0 ? null : (0, react.createElement)("div", {
1406
+ "data-role": "sort-menu",
1407
+ role: "menu",
1408
+ "aria-label": t("sort.menu"),
1409
+ style: {
1410
+ left: String(Math.min(menu.x, Math.max(0, (globalThis.innerWidth || 1440) - 180))) + "px",
1411
+ top: String(Math.min(menu.y, Math.max(0, (globalThis.innerHeight || 800) - 110))) + "px"
1412
+ }
1413
+ }, (0, react.createElement)("button", {
1414
+ type: "button",
1415
+ role: "menuitem",
1416
+ "data-role": "sort-pin-top",
1417
+ onClick: () => {
1418
+ pinMenu("top");
1419
+ }
1420
+ }, t("sort.pinTop")), (0, react.createElement)("button", {
1421
+ type: "button",
1422
+ role: "menuitem",
1423
+ "data-role": "sort-pin-bottom",
1424
+ onClick: () => {
1425
+ pinMenu("bottom");
1426
+ }
1427
+ }, t("sort.pinBottom")), (0, react.createElement)("button", {
1428
+ type: "button",
1429
+ role: "menuitem",
1430
+ "data-role": "sort-cancel",
1431
+ onClick: () => {
1432
+ setMenu(void 0);
1433
+ }
1434
+ }, t("sort.cancel")));
502
1435
  return (0, react.createElement)("section", {
503
1436
  className: CSS_TAG_ID,
504
- "data-state": loading ? "loading" : failed !== void 0 ? "failed" : rows.length === 0 ? "empty" : "ready"
505
- }, (0, react.createElement)("p", { "data-role": "hint" }, t("hint")), view.approval === void 0 ? null : (0, react.createElement)("p", { "data-role": "approval" }, t(view.approval ? "approval.on" : "approval.off")), runStrip, stats, tabs, loading ? (0, react.createElement)("p", { "data-role": "note" }, t("loading")) : null, failed === void 0 ? null : (0, react.createElement)("div", { "data-role": "note" }, failure(t, failed)), list, nextCursor === void 0 ? null : (0, react.createElement)("button", {
1437
+ "data-state": uiState
1438
+ }, (0, react.createElement)("h2", { "data-role": "title" }, t("title")), (0, react.createElement)("p", { "data-role": "hint" }, t("hint")), blocked !== void 0 ? failureNode(t, blocked, fallback, copy) : (0, react.createElement)("div", { "data-role": "content" }, (0, react.createElement)("div", { "data-role": "start" }, (0, react.createElement)("button", {
1439
+ type: "button",
1440
+ "data-role": "start-scan",
1441
+ onClick: () => {
1442
+ copy(view.startCommands.scan);
1443
+ }
1444
+ }, t("start.scan")), (0, react.createElement)("button", {
1445
+ type: "button",
1446
+ "data-role": "start-pair",
1447
+ onClick: () => {
1448
+ copy(view.startCommands.pair);
1449
+ }
1450
+ }, t("start.pair"))), view.degraded ? (0, react.createElement)("p", { "data-role": "degraded" }, t("degraded") + " " + view.notes.join(",")) : null, (0, react.createElement)(SettingsCard, {
1451
+ view,
1452
+ settings: face.settings,
1453
+ t
1454
+ }), 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", {
1455
+ "data-role": "copied",
1456
+ role: "status"
1457
+ }, t("copied") + " " + copied), list, empty, queue.nextCursor === void 0 || uiState !== "ready" ? null : (0, react.createElement)("button", {
506
1458
  type: "button",
507
1459
  "data-role": "load-more",
1460
+ disabled: queue.status === "loading",
508
1461
  onClick: () => {
509
- loadRows(tab, Number(nextCursor));
1462
+ store.loadMore();
510
1463
  }
511
- }, t("loadMore")), face.loadHistory === void 0 ? null : (0, react.createElement)("button", {
1464
+ }, t("loadMore")), (0, react.createElement)("button", {
512
1465
  type: "button",
513
1466
  "data-role": "history",
514
- onClick: openHistory
515
- }, t("history")), overlay === void 0 ? null : (0, react.createElement)("section", { "data-role": "overlay" }, (0, react.createElement)("button", {
1467
+ onClick: () => {
1468
+ store.openHistory();
1469
+ }
1470
+ }, t("history")), (0, react.createElement)("button", {
516
1471
  type: "button",
517
- "data-role": "overlay-close",
1472
+ "data-role": "doctor",
518
1473
  onClick: () => {
519
- setOverlay(void 0);
1474
+ copy(doctorCommand);
520
1475
  }
521
- }, t("close")), overlay.kind === "body" ? (0, react.createElement)("div", {
522
- "data-role": "body",
523
- "data-label": overlay.label,
524
- dangerouslySetInnerHTML: { __html: overlay.html }
525
- }) : (0, react.createElement)("ul", { "data-role": "history-list" }, overlay.items.map((item) => (0, react.createElement)("li", { key: item.name }, item.name + " · " + item.kind + " · " + item.reason + " · v" + String(item.lastVersion))))), (0, react.createElement)("button", {
1476
+ }, t("doctor.open")), overlay === void 0 ? null : overlay.kind === "body" ? bodyNode(overlay, t, () => {
1477
+ store.closeOverlay();
1478
+ }) : historyNode(overlay, t, () => {
1479
+ store.closeOverlay();
1480
+ }), (0, react.createElement)("button", {
526
1481
  type: "button",
527
1482
  "data-role": "refresh",
528
1483
  onClick: () => {
529
- loadView();
530
- loadRows(tab, 0);
1484
+ store.refresh();
531
1485
  }
532
- }, t("refresh")));
1486
+ }, t("refresh")), menuNode));
533
1487
  }
534
1488
  //#endregion
1489
+ //#region src/client/MergeStore.ts
1490
+ const emptyView = () => ({
1491
+ counts: {
1492
+ verify: 0,
1493
+ pending: 0,
1494
+ fuse: 0,
1495
+ skipped: 0
1496
+ },
1497
+ expired: 0,
1498
+ tabs: [
1499
+ {
1500
+ queue: "verify",
1501
+ count: 0
1502
+ },
1503
+ {
1504
+ queue: "pending",
1505
+ count: 0
1506
+ },
1507
+ {
1508
+ queue: "fuse",
1509
+ count: 0
1510
+ },
1511
+ {
1512
+ queue: "skipped",
1513
+ count: 0
1514
+ }
1515
+ ],
1516
+ commands: [],
1517
+ startCommands: {
1518
+ scan: "/evolution merge scan",
1519
+ pair: "/evolution merge pair <a> <b>"
1520
+ },
1521
+ notes: [],
1522
+ degraded: false,
1523
+ approval: { mergeApproval: false }
1524
+ });
1525
+ const keyOf = (queue, expiredOnly) => queue + ":" + (expiredOnly ? "1" : "0");
1526
+ const loadingQueue = () => ({
1527
+ status: "loading",
1528
+ rows: []
1529
+ });
1530
+ var MergeStore = class {
1531
+ source;
1532
+ listeners = /* @__PURE__ */ new Set();
1533
+ cache = /* @__PURE__ */ new Map();
1534
+ tokens = /* @__PURE__ */ new Map();
1535
+ viewToken = 0;
1536
+ overlayToken = 0;
1537
+ snapshot;
1538
+ started = false;
1539
+ disposed = false;
1540
+ constructor(source, initial = {}) {
1541
+ this.source = source;
1542
+ const queue = initial.queue ?? "pending";
1543
+ const expiredOnly = queue === "pending" ? initial.expiredOnly ?? false : false;
1544
+ const initialRows = initial.rows;
1545
+ const current = initialRows === void 0 ? loadingQueue() : {
1546
+ status: initialRows.length === 0 ? "empty" : "ready",
1547
+ rows: initialRows
1548
+ };
1549
+ if (initialRows !== void 0) this.cache.set(keyOf(queue, expiredOnly), current);
1550
+ this.snapshot = {
1551
+ view: initial.view ?? emptyView(),
1552
+ viewState: initial.view === void 0 ? "loading" : "ready",
1553
+ queue,
1554
+ expiredOnly,
1555
+ current
1556
+ };
1557
+ }
1558
+ read = () => this.snapshot;
1559
+ subscribe = (listener) => {
1560
+ this.listeners.add(listener);
1561
+ return () => {
1562
+ this.listeners.delete(listener);
1563
+ };
1564
+ };
1565
+ emit() {
1566
+ if (this.disposed) return;
1567
+ for (const listener of this.listeners) listener();
1568
+ }
1569
+ set(next) {
1570
+ this.snapshot = next;
1571
+ this.emit();
1572
+ }
1573
+ isCurrent(queue, expiredOnly) {
1574
+ return this.snapshot.queue === queue && this.snapshot.expiredOnly === expiredOnly;
1575
+ }
1576
+ start() {
1577
+ if (this.started || this.disposed) return;
1578
+ this.started = true;
1579
+ this.loadView();
1580
+ this.loadQueue(this.snapshot.queue, this.snapshot.expiredOnly);
1581
+ }
1582
+ async loadView(showLoading = true) {
1583
+ const token = ++this.viewToken;
1584
+ if (showLoading) this.set({
1585
+ ...this.snapshot,
1586
+ viewState: "loading",
1587
+ viewFailure: void 0
1588
+ });
1589
+ const outcome = await this.source.view();
1590
+ if (this.disposed || token !== this.viewToken) return;
1591
+ if (!outcome.ok) {
1592
+ if (outcome.failure.kind === "blocked") {
1593
+ this.set({
1594
+ ...this.snapshot,
1595
+ viewState: "blocked",
1596
+ viewFailure: outcome.failure,
1597
+ current: {
1598
+ status: "idle",
1599
+ rows: []
1600
+ }
1601
+ });
1602
+ for (const key of this.cache.keys()) this.cache.set(key, {
1603
+ status: "idle",
1604
+ rows: []
1605
+ });
1606
+ return;
1607
+ }
1608
+ this.set({
1609
+ ...this.snapshot,
1610
+ viewState: "error",
1611
+ viewFailure: outcome.failure
1612
+ });
1613
+ return;
1614
+ }
1615
+ this.set({
1616
+ ...this.snapshot,
1617
+ view: outcome.payload,
1618
+ viewState: "ready",
1619
+ viewFailure: void 0
1620
+ });
1621
+ }
1622
+ async loadQueue(queue, expiredOnly, cursor) {
1623
+ const key = keyOf(queue, expiredOnly);
1624
+ const token = (this.tokens.get(key) ?? 0) + 1;
1625
+ this.tokens.set(key, token);
1626
+ const previous = this.cache.get(key);
1627
+ const next = cursor === void 0 ? {
1628
+ status: "loading",
1629
+ rows: []
1630
+ } : {
1631
+ status: "loading",
1632
+ rows: previous?.rows ?? [],
1633
+ ...previous?.nextCursor === void 0 ? {} : { nextCursor: previous.nextCursor }
1634
+ };
1635
+ this.cache.set(key, next);
1636
+ if (this.isCurrent(queue, expiredOnly)) this.set({
1637
+ ...this.snapshot,
1638
+ current: next
1639
+ });
1640
+ const outcome = await this.source.rows(queue, cursor, queue === "pending" && expiredOnly);
1641
+ if (this.disposed || token !== this.tokens.get(key)) return;
1642
+ if (!outcome.ok) {
1643
+ if (outcome.failure.kind === "blocked") {
1644
+ this.set({
1645
+ ...this.snapshot,
1646
+ viewState: "blocked",
1647
+ viewFailure: outcome.failure,
1648
+ current: {
1649
+ status: "idle",
1650
+ rows: []
1651
+ }
1652
+ });
1653
+ return;
1654
+ }
1655
+ const failed = {
1656
+ status: "error",
1657
+ rows: [],
1658
+ failure: outcome.failure
1659
+ };
1660
+ this.cache.set(key, failed);
1661
+ if (this.isCurrent(queue, expiredOnly)) this.set({
1662
+ ...this.snapshot,
1663
+ current: failed
1664
+ });
1665
+ return;
1666
+ }
1667
+ const items = cursor === void 0 ? outcome.payload.items : [...previous?.rows ?? [], ...outcome.payload.items];
1668
+ const ready = {
1669
+ status: items.length === 0 ? "empty" : "ready",
1670
+ rows: items,
1671
+ ...outcome.payload.nextCursor === void 0 ? {} : { nextCursor: outcome.payload.nextCursor }
1672
+ };
1673
+ this.cache.set(key, ready);
1674
+ if (this.isCurrent(queue, expiredOnly)) this.set({
1675
+ ...this.snapshot,
1676
+ current: ready,
1677
+ expiredOnly,
1678
+ queue
1679
+ });
1680
+ }
1681
+ select(queue) {
1682
+ const expiredOnly = queue === "pending" ? this.snapshot.expiredOnly : false;
1683
+ if (queue === this.snapshot.queue && expiredOnly === this.snapshot.expiredOnly) return;
1684
+ const key = keyOf(queue, expiredOnly);
1685
+ const cached = this.cache.get(key);
1686
+ const current = cached ?? loadingQueue();
1687
+ this.set({
1688
+ ...this.snapshot,
1689
+ queue,
1690
+ expiredOnly,
1691
+ current,
1692
+ overlay: void 0
1693
+ });
1694
+ if (cached === void 0) this.loadQueue(queue, expiredOnly);
1695
+ }
1696
+ toggleExpired() {
1697
+ if (this.snapshot.queue !== "pending") return;
1698
+ const expiredOnly = !this.snapshot.expiredOnly;
1699
+ const key = keyOf("pending", expiredOnly);
1700
+ const cached = this.cache.get(key);
1701
+ const current = cached ?? loadingQueue();
1702
+ this.set({
1703
+ ...this.snapshot,
1704
+ expiredOnly,
1705
+ current,
1706
+ overlay: void 0
1707
+ });
1708
+ if (cached === void 0) this.loadQueue("pending", expiredOnly);
1709
+ }
1710
+ refresh() {
1711
+ if (this.disposed) return;
1712
+ this.loadView(true);
1713
+ this.loadQueue(this.snapshot.queue, this.snapshot.expiredOnly);
1714
+ }
1715
+ loadMore() {
1716
+ const current = this.snapshot.current;
1717
+ if (current.nextCursor === void 0 || current.status === "loading" || this.snapshot.viewState === "blocked") return;
1718
+ this.loadQueue(this.snapshot.queue, this.snapshot.expiredOnly, current.nextCursor);
1719
+ }
1720
+ async openBody(name, side) {
1721
+ const token = ++this.overlayToken;
1722
+ this.set({
1723
+ ...this.snapshot,
1724
+ overlay: {
1725
+ kind: "body",
1726
+ name,
1727
+ side,
1728
+ status: "loading"
1729
+ }
1730
+ });
1731
+ const outcome = await this.source.body(name, side);
1732
+ if (this.disposed || token !== this.overlayToken) return;
1733
+ if (!outcome.ok) {
1734
+ if (outcome.failure.kind === "blocked") {
1735
+ this.set({
1736
+ ...this.snapshot,
1737
+ viewState: "blocked",
1738
+ viewFailure: outcome.failure,
1739
+ overlay: void 0
1740
+ });
1741
+ return;
1742
+ }
1743
+ this.set({
1744
+ ...this.snapshot,
1745
+ overlay: {
1746
+ kind: "body",
1747
+ name,
1748
+ side,
1749
+ status: "error",
1750
+ failure: outcome.failure
1751
+ }
1752
+ });
1753
+ return;
1754
+ }
1755
+ this.set({
1756
+ ...this.snapshot,
1757
+ overlay: {
1758
+ kind: "body",
1759
+ name,
1760
+ side,
1761
+ status: "ready",
1762
+ body: outcome.payload
1763
+ }
1764
+ });
1765
+ }
1766
+ async openHistory() {
1767
+ const token = ++this.overlayToken;
1768
+ this.set({
1769
+ ...this.snapshot,
1770
+ overlay: {
1771
+ kind: "history",
1772
+ status: "loading"
1773
+ }
1774
+ });
1775
+ const outcome = await this.source.history();
1776
+ if (this.disposed || token !== this.overlayToken) return;
1777
+ if (!outcome.ok) {
1778
+ if (outcome.failure.kind === "blocked") {
1779
+ this.set({
1780
+ ...this.snapshot,
1781
+ viewState: "blocked",
1782
+ viewFailure: outcome.failure,
1783
+ overlay: void 0
1784
+ });
1785
+ return;
1786
+ }
1787
+ this.set({
1788
+ ...this.snapshot,
1789
+ overlay: {
1790
+ kind: "history",
1791
+ status: "error",
1792
+ failure: outcome.failure
1793
+ }
1794
+ });
1795
+ return;
1796
+ }
1797
+ this.set({
1798
+ ...this.snapshot,
1799
+ overlay: {
1800
+ kind: "history",
1801
+ status: "ready",
1802
+ items: outcome.payload.items
1803
+ }
1804
+ });
1805
+ }
1806
+ closeOverlay() {
1807
+ this.overlayToken += 1;
1808
+ this.set({
1809
+ ...this.snapshot,
1810
+ overlay: void 0
1811
+ });
1812
+ }
1813
+ dispose() {
1814
+ this.disposed = true;
1815
+ this.listeners.clear();
1816
+ }
1817
+ };
1818
+ //#endregion
535
1819
  //#region src/client/index.ts
536
1820
  const inject = ["slots", "locale"];
537
1821
  function PanelIcon(props = {}) {
@@ -548,34 +1832,19 @@ window.__ModuleLoader__.load({
548
1832
  strokeLinejoin: "round"
549
1833
  }, (0, react.createElement)("path", { d: "M3 4h10M3 8h6M3 12h8" }));
550
1834
  }
551
- function seedView() {
552
- return {
553
- counts: {
554
- verify: 0,
555
- pending: 0,
556
- fuse: 0,
557
- skipped: 0
558
- },
559
- tabs: [
560
- {
561
- queue: "verify",
562
- count: 0
563
- },
564
- {
565
- queue: "pending",
566
- count: 0
567
- },
568
- {
569
- queue: "fuse",
570
- count: 0
571
- },
572
- {
573
- queue: "skipped",
574
- count: 0
575
- }
576
- ],
577
- commands: []
578
- };
1835
+ function interpolate(text, values) {
1836
+ if (values === void 0) return text;
1837
+ return text.replace(/\{(\w+)\}/g, (_match, key) => String(values[key] ?? ""));
1838
+ }
1839
+ /** Best-effort panel workspace; the merge panel itself is not workspace-keyed. */
1840
+ function panelWorkspaceId(ctx) {
1841
+ const get = ctx.get;
1842
+ if (typeof get !== "function") return void 0;
1843
+ for (const name of ["workspace", "uiWorkspace"]) try {
1844
+ const seat = get.call(ctx, name);
1845
+ const id = seat?.workspaceId ?? seat?.currentWorkspaceId;
1846
+ if (typeof id === "string" && id.trim() !== "") return id;
1847
+ } catch {}
579
1848
  }
580
1849
  function apply(ctx) {
581
1850
  const seam = ctx;
@@ -603,14 +1872,15 @@ window.__ModuleLoader__.load({
603
1872
  tag.remove();
604
1873
  };
605
1874
  }, "evolution-merge-ui: styles");
606
- const t = (key) => {
1875
+ const t = (key, values) => {
607
1876
  const fallback = zh[key] ?? key;
608
- if (!registered) return fallback;
609
- try {
610
- return seam.locale.bind(NS)(key);
1877
+ let text = fallback;
1878
+ if (registered) try {
1879
+ text = seam.locale.bind(NS)(key);
611
1880
  } catch {
612
- return fallback;
1881
+ text = fallback;
613
1882
  }
1883
+ return interpolate(text, values);
614
1884
  };
615
1885
  const label = () => t("entry.label");
616
1886
  seam.slots.inject("sidebar.panellist", function* () {
@@ -623,15 +1893,31 @@ window.__ModuleLoader__.load({
623
1893
  yield seam.slots.register({
624
1894
  name: "main",
625
1895
  key: PANEL_ID,
626
- inject: () => ({
627
- view: seedView(),
628
- rows: [],
629
- loadView: view,
630
- loadRows: rows,
631
- loadBody: body,
632
- loadHistory: history,
633
- t
634
- })
1896
+ inject: () => {
1897
+ const source = {
1898
+ view,
1899
+ rows,
1900
+ body,
1901
+ history
1902
+ };
1903
+ let seat;
1904
+ try {
1905
+ seat = ctx.get?.("configForms");
1906
+ } catch {
1907
+ seat = void 0;
1908
+ }
1909
+ const workspaceId = panelWorkspaceId(ctx);
1910
+ return {
1911
+ store: new MergeStore(source),
1912
+ t,
1913
+ doctorCommand: "/evolution doctor",
1914
+ settings: { ...seat === void 0 ? {} : {
1915
+ merge: seat.get("evolution-merge"),
1916
+ platform: seat.get("evolution-approval")
1917
+ } },
1918
+ ...workspaceId === void 0 ? {} : { workspaceId }
1919
+ };
1920
+ }
635
1921
  }, MergePanel);
636
1922
  });
637
1923
  }