@lmzhen/dsh-evolution-merge-ui 0.0.0 → 0.29.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,3 +1,11 @@
1
1
  # @lmzhen/dsh-evolution-merge-ui
2
2
 
3
- Placeholder package. The real release is published from the family CI workflow.
3
+ Browser-only sidebar panel for the merge queues. It reads the four host routes
4
+ and never writes skills: row actions are copyable `/evolution merge ...`
5
+ commands.
6
+
7
+ - Queues: pending / retry / skipped.
8
+ - Row facts: target, confidence, failure, attempts, origin, staging expiry.
9
+ - Run strip: latest run id/state/stage/progress.
10
+ - Four states: loading / ready / empty / failed; view schemaVersion guard.
11
+ - Mounted by the `all` bundle only.
package/lib/client.js ADDED
@@ -0,0 +1,538 @@
1
+ window.__ModuleLoader__.load({
2
+ id: "@lmzhen/dsh-evolution-merge-ui",
3
+ factory: (require) => {
4
+ var module = { exports: {} };
5
+ var exports = module.exports;
6
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
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 = {
16
+ approval: {
17
+ pending: "/api/dsh-evolution/approval/pending",
18
+ preview: "/api/dsh-evolution/approval/preview",
19
+ approve: "/api/dsh-evolution/approval/approve",
20
+ reject: "/api/dsh-evolution/approval/reject"
21
+ },
22
+ "skill-history": {
23
+ skills: "/api/dsh-evolution/skill-history/skills",
24
+ versions: "/api/dsh-evolution/skill-history/versions",
25
+ diff: "/api/dsh-evolution/skill-history/versions/diff",
26
+ body: "/api/dsh-evolution/skill-history/versions/body",
27
+ undo: "/api/dsh-evolution/skill-history/undo",
28
+ health: "/api/dsh-evolution/skill-history/health"
29
+ },
30
+ "skill-health": { rows: "/api/dsh-evolution/skill-health/rows" },
31
+ merge: {
32
+ view: "/api/dsh-evolution/merge/view",
33
+ rows: "/api/dsh-evolution/merge/rows",
34
+ run: "/api/dsh-evolution/merge/run",
35
+ runs: "/api/dsh-evolution/merge/runs"
36
+ },
37
+ 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
50
+ };
51
+ if (status === 404) return {
52
+ kind: "unmounted",
53
+ status: 404
54
+ };
55
+ return {
56
+ kind: "server",
57
+ status
58
+ };
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) {
68
+ let response;
69
+ try {
70
+ response = await doFetch(path, init);
71
+ } catch (error) {
72
+ return {
73
+ ok: false,
74
+ failure: {
75
+ kind: "server",
76
+ status: 0,
77
+ detail: error instanceof Error ? error.message : String(error)
78
+ }
79
+ };
80
+ }
81
+ if (!response.ok) return {
82
+ ok: false,
83
+ failure: classify(response.status)
84
+ };
85
+ let body;
86
+ try {
87
+ body = await response.json();
88
+ } catch {
89
+ return {
90
+ ok: false,
91
+ failure: {
92
+ kind: "server",
93
+ status: response.status
94
+ }
95
+ };
96
+ }
97
+ if (typeof body === "object" && body !== null) {
98
+ const record = body;
99
+ if (record.ok === true) return {
100
+ ok: true,
101
+ data: record.data
102
+ };
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 {
108
+ ok: false,
109
+ failure: {
110
+ kind: "refused",
111
+ code,
112
+ message
113
+ }
114
+ };
115
+ }
116
+ return {
117
+ ok: false,
118
+ failure: {
119
+ kind: "server",
120
+ status: response.status
121
+ }
122
+ };
123
+ }
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;
131
+ async function view() {
132
+ const answer = await requestJson(fetch, HOST_ROUTES.view, { headers: { accept: "application/json" } });
133
+ return answer.ok ? {
134
+ ok: true,
135
+ payload: answer.data
136
+ } : {
137
+ ok: false,
138
+ failure: answer.failure
139
+ };
140
+ }
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
+ } : {
148
+ ok: false,
149
+ failure: answer.failure
150
+ };
151
+ }
152
+ //#endregion
153
+ //#region src/client/seam.ts
154
+ const PANEL_ID = "merge";
155
+ //#endregion
156
+ //#region src/client/messages.ts
157
+ const NS = "evolution-merge";
158
+ const zh = {
159
+ "entry.label": "技能合并",
160
+ "hint": "手动触发;后台逐对判定。这里只读,不写技能。",
161
+ "tab.pending": "待批准",
162
+ "tab.retry": "待重跑",
163
+ "tab.skipped": "无需合并",
164
+ "empty.pending": "没有待批准的合并。",
165
+ "empty.retry": "没有待重跑的对。",
166
+ "empty.skipped": "没有标记为无需合并的对。",
167
+ "loading": "正在读取…",
168
+ "refresh": "刷新",
169
+ "loadMore": "加载更多",
170
+ "approval.on": "审批开:合并前需人工批准。",
171
+ "approval.off": "审批关:合并自动执行。",
172
+ "failed": "读取失败,保留上次结果。",
173
+ "stage.decide": "判定",
174
+ "stage.author": "合并",
175
+ "stage.validate": "检查",
176
+ "stage.commit": "提交",
177
+ "row.target": "目标",
178
+ "row.confidence": "置信度",
179
+ "row.attempts": "尝试",
180
+ "row.origin": "来源",
181
+ "row.expires": "暂存到期",
182
+ "row.failure": "失败",
183
+ "row.noRetry": "不可重试",
184
+ "row.copy": "复制命令",
185
+ "origin.agent": "自动",
186
+ "origin.human": "人工",
187
+ "state.verify": "待判定",
188
+ "state.pending": "待批准",
189
+ "state.fuse": "待融合",
190
+ "state.skipped": "已跳过"
191
+ };
192
+ const en = {
193
+ "entry.label": "Skill merge",
194
+ "hint": "Manual only; pairs are judged in the background. Read-only here.",
195
+ "tab.pending": "Pending",
196
+ "tab.retry": "Retry",
197
+ "tab.skipped": "Skipped",
198
+ "empty.pending": "No pending merges.",
199
+ "empty.retry": "No pairs waiting to retry.",
200
+ "empty.skipped": "No pairs marked as not worth merging.",
201
+ "loading": "Loading…",
202
+ "refresh": "Refresh",
203
+ "loadMore": "Load more",
204
+ "approval.on": "Approval on: merges wait for a human.",
205
+ "approval.off": "Approval off: merges run automatically.",
206
+ "failed": "Read failed; keeping the last result.",
207
+ "stage.decide": "Decide",
208
+ "stage.author": "Author",
209
+ "stage.validate": "Validate",
210
+ "stage.commit": "Commit",
211
+ "row.target": "Target",
212
+ "row.confidence": "Confidence",
213
+ "row.attempts": "Attempts",
214
+ "row.origin": "Origin",
215
+ "row.expires": "Staging expires",
216
+ "row.failure": "Failure",
217
+ "row.noRetry": "not retryable",
218
+ "row.copy": "Copy command",
219
+ "origin.agent": "auto",
220
+ "origin.human": "human",
221
+ "state.verify": "Unverified",
222
+ "state.pending": "Pending approval",
223
+ "state.fuse": "Ready to fuse",
224
+ "state.skipped": "Skipped"
225
+ };
226
+ //#endregion
227
+ //#region src/client/styles.ts
228
+ const CSS_TAG_ID = "evolution-merge-panel";
229
+ const CSS = [
230
+ ":where(.evolution-merge-panel) { --evo-type-lg:calc(1em + 2px);--evo-type-body:1em;--evo-type-sm:calc(1em - 1px);--evo-type-xs:calc(1em - 2px);--evo-space-2:2px;--evo-space-4:4px;--evo-space-6:6px;--evo-space-8:8px;--evo-space-10:10px;--evo-space-12:12px;--evo-space-14:14px;--evo-space-16:16px;--evo-space-20:20px;--evo-radius-chip:4px;--evo-radius-control:8px;--evo-radius-surface:12px;--evo-radius-pill:999px;--evo-hairline-width:.5px;--evo-hairline-marker:2px;--evo-measure-read:80ch;--evo-leading-base:1.5;--evo-tone-quiet:var(--dsw-alias-label-secondary);--evo-tone-primary:var(--dsw-alias-brand-primary);--evo-tone-danger:var(--dsw-alias-state-error-primary);--evo-tone-success:var(--dsw-alias-state-success-primary);--evo-focus-ring:2px solid var(--dsw-alias-label-primary);--evo-focus-offset:-2px;--evo-cap-aside:280px;--evo-cap-block:260px }",
231
+ ".evolution-merge-panel { display:flex; flex-direction:column; gap:var(--evo-space-12); padding:var(--evo-space-16); color:var(--dsw-alias-label-primary); font-size:var(--evo-type-sm); line-height:var(--evo-leading-base); }",
232
+ ".evolution-merge-panel [data-role=\"hint\"] { margin:0; color:var(--dsw-alias-label-tertiary); font-size:var(--evo-type-xs); }",
233
+ ".evolution-merge-panel [data-role=\"stats\"] { display:flex; gap:var(--evo-space-8); flex-wrap:wrap; }",
234
+ ".evolution-merge-panel [data-role=\"stat\"] { border:var(--evo-hairline-width) solid var(--dsw-alias-border-l2); border-radius:var(--evo-radius-control); padding:var(--evo-space-6) var(--evo-space-10); min-width:calc(var(--evo-space-16) * 4); }",
235
+ ".evolution-merge-panel [data-role=\"stat-value\"] { font-size:var(--evo-type-lg); font-weight:600; }",
236
+ ".evolution-merge-panel [data-role=\"stat-label\"] { font-size:var(--evo-type-xs); color:var(--dsw-alias-label-tertiary); }",
237
+ ".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); }",
238
+ ".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; }",
239
+ ".evolution-merge-panel [data-role=\"tab\"][data-active=\"true\"] { border-bottom-color:var(--dsw-alias-brand-primary); }",
240
+ ".evolution-merge-panel [data-role=\"rows\"] { display:flex; flex-direction:column; gap:var(--evo-space-6); }",
241
+ ".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); }",
242
+ ".evolution-merge-panel [data-role=\"row-label\"] { font-weight:600; }",
243
+ ".evolution-merge-panel [data-role=\"row-meta\"] { color:var(--dsw-alias-label-tertiary); font-size:var(--evo-type-xs); }",
244
+ ".evolution-merge-panel [data-role=\"note\"] { color:var(--dsw-alias-label-tertiary); font-size:var(--evo-type-xs); }",
245
+ ".evolution-merge-panel [data-role=\"refresh\"] { align-self:flex-start; }"
246
+ ].join("\n");
247
+ //#endregion
248
+ //#region src/client/states.generated.ts
249
+ /** A neutral/ warn/ fail block; a fail block is role=alert so assistive tech announces it. */
250
+ function Block(props) {
251
+ const tone = props.tone ?? "neutral";
252
+ return (0, react.createElement)("div", {
253
+ "data-tone": tone,
254
+ ...tone === "fail" ? { role: "alert" } : {}
255
+ }, props.title === void 0 ? null : (0, react.createElement)("div", { "data-tone-title": tone }, props.title), props.children);
256
+ }
257
+ /** One classified failure as a reader-facing sentence. */
258
+ function failureText(failure, t) {
259
+ switch (failure.kind) {
260
+ case "unauthenticated": return t("failureUnauthorized");
261
+ case "forbidden": return t("failureForbidden");
262
+ case "unmounted": return t("failureUnmounted");
263
+ case "server": return failure.status === 0 ? t("failureServer") : t("failureServer") + " (HTTP " + String(failure.status) + ")";
264
+ case "refused": return failure.message;
265
+ }
266
+ }
267
+ /** The failed-state block: sentence + (when the surface exists but is unreachable) the command fallback. */
268
+ function FailureBlock(props) {
269
+ const hint = props.fallback === void 0 ? null : (0, react.createElement)("div", { "data-tone-hint": "true" }, props.t("fallbackHint", { command: props.fallback.command }));
270
+ return Block({
271
+ tone: "fail",
272
+ title: failureText(props.failure, props.t),
273
+ children: hint
274
+ });
275
+ }
276
+ //#endregion
277
+ //#region src/client/fallbacks.generated.ts
278
+ /**
279
+ * The command-face fallback for every Host surface, generated from evolution-commands'
280
+ * COMMAND_FALLBACKS (the ONE home). When the platform's connection carrier is absent (E-325) the
281
+ * panels render this hint instead of a bare HTTP failure — same use case, command face.
282
+ */
283
+ const FALLBACKS = {
284
+ approval: { command: "/evolution pending … /evolution approve <id> … /evolution reject <id> … /evolution release <id>" },
285
+ "skill-history": { command: "/evolution restore <name> [v](查看:/evolution skills history <name>)" },
286
+ "skill-health": { command: "/evolution skills refresh(健康行:/evolution doctor)" },
287
+ merge: { command: "/evolution merge status / pending / approve <id> / report" },
288
+ diagnostics: {
289
+ command: "/evolution doctor",
290
+ platformCli: "dsh --json /evolution doctor"
291
+ }
292
+ };
293
+ //#endregion
294
+ //#region src/client/Panel.ts
295
+ /**
296
+ * The merge panel body: three queues, counts, and a read-only row list. Actions are
297
+ * intentionally copied commands, not buttons that write from the browser.
298
+ */
299
+ const QUEUES = [
300
+ "pending",
301
+ "retry",
302
+ "skipped"
303
+ ];
304
+ function failure(t, failure) {
305
+ return FailureBlock({
306
+ failure,
307
+ t,
308
+ fallback: FALLBACKS["merge"]
309
+ });
310
+ }
311
+ function MergePanel(face) {
312
+ const t = face.t;
313
+ const [view, setView] = (0, react.useState)(face.view);
314
+ const [tab, setTab] = (0, react.useState)("pending");
315
+ const [rows, setRows] = (0, react.useState)(face.rows);
316
+ const [loading, setLoading] = (0, react.useState)(face.loadRows !== void 0 || face.loadView !== void 0);
317
+ const [failed, setFailed] = (0, react.useState)(void 0);
318
+ const [nextCursor, setNextCursor] = (0, react.useState)(void 0);
319
+ const request = (0, react.useRef)(0);
320
+ const loadRows = (queue, cursor = 0) => {
321
+ const load = face.loadRows;
322
+ if (load === void 0) return;
323
+ const token = request.current + 1;
324
+ request.current = token;
325
+ setLoading(true);
326
+ load(queue, cursor).then((outcome) => {
327
+ if (token !== request.current) return;
328
+ if (outcome.ok) {
329
+ setRows((current) => [...cursor === 0 ? [] : current, ...outcome.payload.items].sort((a, b) => b.updatedAt.localeCompare(a.updatedAt)));
330
+ setNextCursor(outcome.payload.nextCursor);
331
+ setFailed(void 0);
332
+ } else setFailed(outcome.failure);
333
+ }).catch(() => {
334
+ setFailed({
335
+ kind: "server",
336
+ status: 0
337
+ });
338
+ }).finally(() => {
339
+ setLoading(false);
340
+ });
341
+ };
342
+ const loadView = () => {
343
+ const load = face.loadView;
344
+ if (load === void 0) return;
345
+ load().then((outcome) => {
346
+ if (!outcome.ok) {
347
+ setFailed(outcome.failure);
348
+ return;
349
+ }
350
+ if (outcome.payload.schemaVersion !== void 0 && outcome.payload.schemaVersion !== 1) {
351
+ setFailed({
352
+ kind: "server",
353
+ status: 0
354
+ });
355
+ return;
356
+ }
357
+ setView(outcome.payload);
358
+ }).catch(() => {
359
+ setFailed({
360
+ kind: "server",
361
+ status: 0
362
+ });
363
+ });
364
+ };
365
+ (0, react.useEffect)(() => {
366
+ loadView();
367
+ loadRows("pending");
368
+ }, [face]);
369
+ (0, react.useEffect)(() => {
370
+ setNextCursor(void 0);
371
+ loadRows(tab, 0);
372
+ }, [tab]);
373
+ const counts = view.counts;
374
+ const stats = (0, react.createElement)("div", { "data-role": "stats" }, QUEUES.map((queue) => (0, react.createElement)("div", {
375
+ key: queue,
376
+ "data-role": "stat",
377
+ "data-queue": queue
378
+ }, (0, react.createElement)("div", { "data-role": "stat-value" }, String(counts[queue])), (0, react.createElement)("div", { "data-role": "stat-label" }, t("tab." + queue)))));
379
+ const latest = view.latestRun;
380
+ 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)));
381
+ const tabs = (0, react.createElement)("div", {
382
+ "data-role": "tabs",
383
+ role: "tablist"
384
+ }, QUEUES.map((queue) => (0, react.createElement)("button", {
385
+ key: queue,
386
+ type: "button",
387
+ role: "tab",
388
+ "aria-selected": tab === queue ? "true" : "false",
389
+ "data-role": "tab",
390
+ "data-active": tab === queue ? "true" : "false",
391
+ onClick: () => {
392
+ setTab(queue);
393
+ }
394
+ }, t("tab." + queue) + " " + String(counts[queue]))));
395
+ 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) => {
396
+ const facts = [
397
+ row.target === void 0 ? void 0 : t("row.target") + " " + row.target,
398
+ row.confidence === void 0 ? void 0 : t("row.confidence") + " " + row.confidence.toFixed(2),
399
+ row.attempts === void 0 ? void 0 : t("row.attempts") + " " + String(row.attempts),
400
+ row.origin === void 0 ? void 0 : t("row.origin") + " " + t("origin." + row.origin),
401
+ row.stagingExpiresAt === void 0 ? void 0 : t("row.expires") + " " + new Date(row.stagingExpiresAt).toLocaleString(),
402
+ row.failure === void 0 ? void 0 : t("row.failure") + " " + row.failure.message + (row.failure.retryable ? "" : " · " + t("row.noRetry"))
403
+ ].filter((value) => value !== void 0).join(" · ");
404
+ return (0, react.createElement)("div", {
405
+ key: row.id,
406
+ "data-role": "row"
407
+ }, (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))), 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" }, (row.actions ?? []).filter((action) => view.approval !== false || !action.includes("/evolution merge approve")).map((action) => (0, react.createElement)("button", {
408
+ key: action,
409
+ type: "button",
410
+ "data-role": "row-action",
411
+ title: action,
412
+ onClick: () => {
413
+ navigator.clipboard.writeText(action);
414
+ }
415
+ }, t("row.copy"))))));
416
+ }));
417
+ return (0, react.createElement)("section", {
418
+ className: CSS_TAG_ID,
419
+ "data-state": loading ? "loading" : failed !== void 0 ? "failed" : rows.length === 0 ? "empty" : "ready"
420
+ }, (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", {
421
+ type: "button",
422
+ "data-role": "load-more",
423
+ onClick: () => {
424
+ loadRows(tab, Number(nextCursor));
425
+ }
426
+ }, t("loadMore")), (0, react.createElement)("button", {
427
+ type: "button",
428
+ "data-role": "refresh",
429
+ onClick: () => {
430
+ loadView();
431
+ loadRows(tab, 0);
432
+ }
433
+ }, t("refresh")));
434
+ }
435
+ //#endregion
436
+ //#region src/client/index.ts
437
+ const inject = ["slots", "locale"];
438
+ function PanelIcon(props = {}) {
439
+ const size = typeof props.size === "number" && props.size > 0 ? props.size : 16;
440
+ return (0, react.createElement)("svg", {
441
+ "aria-hidden": "true",
442
+ width: String(size),
443
+ height: String(size),
444
+ viewBox: "0 0 16 16",
445
+ fill: "none",
446
+ stroke: "currentColor",
447
+ strokeWidth: "1.5",
448
+ strokeLinecap: "round",
449
+ strokeLinejoin: "round"
450
+ }, (0, react.createElement)("path", { d: "M3 4h10M3 8h6M3 12h8" }));
451
+ }
452
+ function seedView() {
453
+ return {
454
+ counts: {
455
+ pending: 0,
456
+ retry: 0,
457
+ skipped: 0
458
+ },
459
+ tabs: [
460
+ {
461
+ queue: "pending",
462
+ count: 0
463
+ },
464
+ {
465
+ queue: "retry",
466
+ count: 0
467
+ },
468
+ {
469
+ queue: "skipped",
470
+ count: 0
471
+ }
472
+ ],
473
+ commands: []
474
+ };
475
+ }
476
+ function apply(ctx) {
477
+ const seam = ctx;
478
+ let registered = false;
479
+ ctx.effect(() => {
480
+ try {
481
+ const dispose = seam.locale.register(NS, {
482
+ zh,
483
+ en
484
+ });
485
+ registered = true;
486
+ return dispose;
487
+ } catch (error) {
488
+ ctx.logger.warn("evolution-merge-ui: locale seat refused \"evolution-merge\" (" + String(error) + ")");
489
+ return () => {};
490
+ }
491
+ }, "evolution-merge-ui: locale");
492
+ ctx.effect(() => {
493
+ if (document.querySelector("style[data-plugin-css=\"evolution-merge-panel\"]") !== null) return () => {};
494
+ const tag = document.createElement("style");
495
+ tag.setAttribute("data-plugin-css", CSS_TAG_ID);
496
+ tag.textContent = CSS;
497
+ document.head.append(tag);
498
+ return () => {
499
+ tag.remove();
500
+ };
501
+ }, "evolution-merge-ui: styles");
502
+ const t = (key) => {
503
+ const fallback = zh[key] ?? key;
504
+ if (!registered) return fallback;
505
+ try {
506
+ return seam.locale.bind(NS)(key);
507
+ } catch {
508
+ return fallback;
509
+ }
510
+ };
511
+ const label = () => t("entry.label");
512
+ seam.slots.inject("sidebar.panellist", function* () {
513
+ yield seam.slots.register({
514
+ name: "sidebar.panellist",
515
+ id: PANEL_ID,
516
+ order: 45,
517
+ label
518
+ }, PanelIcon);
519
+ yield seam.slots.register({
520
+ name: "main",
521
+ key: PANEL_ID,
522
+ inject: () => ({
523
+ view: seedView(),
524
+ rows: [],
525
+ loadView: view,
526
+ loadRows: rows,
527
+ t
528
+ })
529
+ }, MergePanel);
530
+ });
531
+ }
532
+ //#endregion
533
+ exports.apply = apply;
534
+ exports.inject = inject;
535
+
536
+ return module.exports;
537
+ }
538
+ });
package/lib/index.js ADDED
@@ -0,0 +1,11 @@
1
+ //#region lib/types/index.js
2
+ /**
3
+ * Host half of the merge UI package: it registers nothing. The Loader discovers the
4
+ * `dsh.client` manifest and serves `./client` to the page; this entry exists so the row
5
+ * is a valid plugin.
6
+ */
7
+ const name = "evolution-merge-ui";
8
+ const inject = [];
9
+ function apply() {}
10
+ //#endregion
11
+ export { apply, inject, name };
@@ -0,0 +1,15 @@
1
+ /**
2
+ * The merge panel body: three queues, counts, and a read-only row list. Actions are
3
+ * intentionally copied commands, not buttons that write from the browser.
4
+ */
5
+ import { type ReactNode } from 'react';
6
+ import type { MergeOutcome, MergeQueue, MergeRowView, MergeViewPayload, QueuePage } from './api.ts';
7
+ export interface MergeFace {
8
+ view: MergeViewPayload;
9
+ rows: MergeRowView[];
10
+ loadView?: () => Promise<MergeOutcome<MergeViewPayload>>;
11
+ loadRows?: (queue: MergeQueue, cursor?: number) => Promise<MergeOutcome<QueuePage>>;
12
+ t: (key: string) => string;
13
+ }
14
+ export declare function MergePanel(face: MergeFace): ReactNode;
15
+ //# sourceMappingURL=Panel.d.ts.map
@@ -0,0 +1,74 @@
1
+ import { type RouteFailure } from './route-client.generated.ts';
2
+ export declare const HOST_ROUTES: {
3
+ readonly view: "/api/dsh-evolution/merge/view";
4
+ readonly rows: "/api/dsh-evolution/merge/rows";
5
+ readonly run: "/api/dsh-evolution/merge/run";
6
+ readonly runs: "/api/dsh-evolution/merge/runs";
7
+ };
8
+ export type MergeQueue = 'pending' | 'retry' | 'skipped';
9
+ export interface MergeCounts {
10
+ pending: number;
11
+ retry: number;
12
+ skipped: number;
13
+ }
14
+ export interface MergeRunView {
15
+ id: string;
16
+ state: string;
17
+ command?: string;
18
+ stage?: string;
19
+ progress?: {
20
+ done: number;
21
+ total: number;
22
+ };
23
+ startedAt: number;
24
+ }
25
+ export interface MergeViewPayload {
26
+ schemaVersion?: number;
27
+ counts: MergeCounts;
28
+ tabs: {
29
+ queue: MergeQueue;
30
+ count: number;
31
+ }[];
32
+ commands: string[];
33
+ latestRun?: MergeRunView;
34
+ approval?: boolean;
35
+ }
36
+ export interface MergeRowView {
37
+ id: string;
38
+ pairKey: string;
39
+ a: string;
40
+ b: string;
41
+ state: string;
42
+ queue: MergeQueue;
43
+ stage: string | null;
44
+ label: string;
45
+ updatedAt: string;
46
+ createdAt?: string;
47
+ target?: string;
48
+ confidence?: number;
49
+ failure?: {
50
+ stage: string;
51
+ code: string;
52
+ message: string;
53
+ retryable: boolean;
54
+ };
55
+ attempts?: number;
56
+ origin?: 'agent' | 'human';
57
+ stagingExpiresAt?: number;
58
+ actions?: string[];
59
+ }
60
+ export interface QueuePage {
61
+ queue: MergeQueue;
62
+ items: MergeRowView[];
63
+ nextCursor?: string;
64
+ }
65
+ export type MergeOutcome<T> = {
66
+ ok: true;
67
+ payload: T;
68
+ } | {
69
+ ok: false;
70
+ failure: RouteFailure;
71
+ };
72
+ export declare function view(): Promise<MergeOutcome<MergeViewPayload>>;
73
+ export declare function rows(queue: MergeQueue, cursor?: number): Promise<MergeOutcome<QueuePage>>;
74
+ //# sourceMappingURL=api.d.ts.map
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Literal mirror of the frozen Host diagnostics contract (W1 §2.8 / core diagnostics.ts). The browser
3
+ * build keeps every @deepseek-ai/* specifier external, so the client CANNOT import the host type — the
4
+ * shape is mirrored mechanically here instead. The key unions are rendered from core diagnostics.ts by
5
+ * the generator, so a key change regenerates all three copies (a stale copy fails --check).
6
+ */
7
+ export type DiagnosticsSchemaVersion = 2;
8
+ export type DiagnosticsFactKey = 'platform-line' | 'family-version' | 'install-form' | 'preset-ids' | 'readonly-contract' | 'upgrade-order' | 'fallback-map';
9
+ export type DiagnosticsRuntimeKey = 'subagent-channel' | 'restricted-preset' | 'host-surface' | 'session-fold' | 'platform-services' | 'runs' | 'platform-pairing' | 'install-form-detected';
10
+ export type DiagnosticStatus = 'ok' | 'warn' | 'fail' | 'unknown';
11
+ export interface DocFactRow {
12
+ readonly key: DiagnosticsFactKey;
13
+ readonly label: string;
14
+ readonly value: string;
15
+ readonly home: string;
16
+ }
17
+ export interface DiagnosticRow {
18
+ readonly key: DiagnosticsRuntimeKey;
19
+ readonly label: string;
20
+ readonly status: DiagnosticStatus;
21
+ readonly detail?: string;
22
+ readonly remedy?: string;
23
+ }
24
+ export interface DiagnosticsView {
25
+ readonly schemaVersion: DiagnosticsSchemaVersion;
26
+ readonly generatedAt: string;
27
+ readonly facts: readonly DocFactRow[];
28
+ readonly runtime: readonly DiagnosticRow[];
29
+ }
30
+ /** The installation form the doctor reports (W8 install-form-detected row). */
31
+ export type InstallFormId = 'M1' | 'M2' | 'M3' | 'M4';
32
+ /** One surface's command-face path(s) — the client `fallbacks.generated.ts` table's row type. */
33
+ export interface FallbackHint {
34
+ readonly command: string;
35
+ readonly platformCli?: string;
36
+ }
37
+ //# sourceMappingURL=contracts.generated.d.ts.map
@@ -0,0 +1,8 @@
1
+ import type { FallbackHint } from './contracts.generated.ts';
2
+ /**
3
+ * The command-face fallback for every Host surface, generated from evolution-commands'
4
+ * COMMAND_FALLBACKS (the ONE home). When the platform's connection carrier is absent (E-325) the
5
+ * panels render this hint instead of a bare HTTP failure — same use case, command face.
6
+ */
7
+ export declare const FALLBACKS: Readonly<Record<'approval' | 'skill-history' | 'skill-health' | 'merge' | 'diagnostics', FallbackHint>>;
8
+ //# sourceMappingURL=fallbacks.generated.d.ts.map
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Browser half: one sidebar row ("技能合并") and the read-only main body it opens.
3
+ * All facts arrive through the generated routes; the bundle holds no service and writes nothing.
4
+ */
5
+ import type { Context as ClientContext } from '@deepseek-ai/cordis';
6
+ export declare const inject: string[];
7
+ export declare function apply(ctx: ClientContext): void;
8
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,4 @@
1
+ export declare const NS = "evolution-merge";
2
+ export declare const zh: Record<string, string>;
3
+ export declare const en: Record<string, string>;
4
+ //# sourceMappingURL=messages.d.ts.map
@@ -0,0 +1,45 @@
1
+ /**
2
+ * The four-state transport classifier every family client shares (W7 §2). The platform owns
3
+ * admission (401/403/404 are its answers, not ours), so the client reads the STATUS only for those
4
+ * three and never tries to interpret a platform body. Everything the family route itself answers
5
+ * keeps its business envelope: 2xx + {ok:true,data} or 2xx + {ok:false,code,message}.
6
+ */
7
+ export type RouteFailure = {
8
+ readonly kind: 'unauthenticated';
9
+ readonly status: 401;
10
+ } | {
11
+ readonly kind: 'forbidden';
12
+ readonly status: 403;
13
+ } | {
14
+ readonly kind: 'unmounted';
15
+ readonly status: 404;
16
+ } | {
17
+ readonly kind: 'server';
18
+ readonly status: number;
19
+ readonly detail?: string;
20
+ } | {
21
+ readonly kind: 'refused';
22
+ readonly code: string;
23
+ readonly message: string;
24
+ };
25
+ /** One route answer: the data, or the classified failure. */
26
+ export type RouteAnswer<T> = {
27
+ readonly ok: true;
28
+ readonly data: T;
29
+ } | {
30
+ readonly ok: false;
31
+ readonly failure: RouteFailure;
32
+ };
33
+ /** Classify a non-2xx status: 401/403/404 keep their own kind, everything else is a server answer. */
34
+ export declare function classify(status: number): RouteFailure;
35
+ /**
36
+ * One request, no retries and no body parsing for the platform's admission answers.
37
+ * @param doFetch - the fetch in use (the browser's, or a spec's stub).
38
+ * @param path - the exact route path from the generated ROUTES table.
39
+ * @param init - standard RequestInit.
40
+ * @returns the data, or the classified failure (network/parse failures read as 'server').
41
+ */
42
+ export declare function requestJson<T>(doFetch: typeof fetch, path: string, init?: RequestInit): Promise<RouteAnswer<T>>;
43
+ /** The one-sentence rendering legacy callers attach to a classified failure. */
44
+ export declare function failureMessage(failure: RouteFailure): string;
45
+ //# sourceMappingURL=route-client.generated.d.ts.map
@@ -0,0 +1,37 @@
1
+ /** One host data-plane surface the browser half can call. */
2
+ export type SurfaceId = 'approval' | 'skill-history' | 'skill-health' | 'merge' | 'diagnostics';
3
+ /**
4
+ * Every family host path, generated from evolution-core's HOST_ROUTES (the ONE declaration).
5
+ * A browser half cannot import the host package at runtime (client bundles keep @deepseek-ai/*
6
+ * external), so the table is mirrored here mechanically; a rename in core regenerates all three
7
+ * client copies, and the generator's --check makes a stale copy red.
8
+ */
9
+ export declare const ROUTES: {
10
+ readonly approval: {
11
+ readonly pending: "/api/dsh-evolution/approval/pending";
12
+ readonly preview: "/api/dsh-evolution/approval/preview";
13
+ readonly approve: "/api/dsh-evolution/approval/approve";
14
+ readonly reject: "/api/dsh-evolution/approval/reject";
15
+ };
16
+ readonly 'skill-history': {
17
+ readonly skills: "/api/dsh-evolution/skill-history/skills";
18
+ readonly versions: "/api/dsh-evolution/skill-history/versions";
19
+ readonly diff: "/api/dsh-evolution/skill-history/versions/diff";
20
+ readonly body: "/api/dsh-evolution/skill-history/versions/body";
21
+ readonly undo: "/api/dsh-evolution/skill-history/undo";
22
+ readonly health: "/api/dsh-evolution/skill-history/health";
23
+ };
24
+ readonly 'skill-health': {
25
+ readonly rows: "/api/dsh-evolution/skill-health/rows";
26
+ };
27
+ readonly merge: {
28
+ readonly view: "/api/dsh-evolution/merge/view";
29
+ readonly rows: "/api/dsh-evolution/merge/rows";
30
+ readonly run: "/api/dsh-evolution/merge/run";
31
+ readonly runs: "/api/dsh-evolution/merge/runs";
32
+ };
33
+ readonly diagnostics: {
34
+ readonly view: "/api/dsh-evolution/diagnostics";
35
+ };
36
+ };
37
+ //# sourceMappingURL=routes.generated.d.ts.map
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Structural view of the client seams this bundle consumes. The package ships outside the
3
+ * platform repository, so it declares the seats instead of importing platform client sources.
4
+ */
5
+ import type { ReactNode } from 'react';
6
+ export declare const PANEL_ID = "merge";
7
+ export declare const PANEL_ORDER = 45;
8
+ export interface PanelRowOptions {
9
+ name: 'sidebar.panellist';
10
+ id: string;
11
+ order: number;
12
+ label: () => string;
13
+ }
14
+ export interface PanelBodyOptions {
15
+ name: 'main';
16
+ key: string;
17
+ inject: () => unknown;
18
+ }
19
+ export interface LocaleSeat {
20
+ register(namespace: string, dictionaries: {
21
+ zh: Record<string, string>;
22
+ en: Record<string, string>;
23
+ }): () => void;
24
+ bind(namespace: string): (key: string) => string;
25
+ }
26
+ export interface SlotsSeat {
27
+ inject(name: string, body: () => Generator<() => void, void, void>): void;
28
+ register(options: PanelRowOptions | PanelBodyOptions, component: (props: never) => ReactNode): () => void;
29
+ }
30
+ export interface ClientSeam {
31
+ slots: SlotsSeat;
32
+ locale: LocaleSeat;
33
+ }
34
+ //# sourceMappingURL=seam.d.ts.map
@@ -0,0 +1,43 @@
1
+ import { type ReactNode } from 'react';
2
+ import type { RouteAnswer, RouteFailure } from './route-client.generated.ts';
3
+ import type { FallbackHint } from './contracts.generated.ts';
4
+ /**
5
+ * The four-state base of every family card (W7 §2). Load states are read from the generated
6
+ * route-client's answer — a card owns no HTTP vocabulary and no second state machine.
7
+ */
8
+ export type LoadState<T> = {
9
+ readonly phase: 'loading';
10
+ } | {
11
+ readonly phase: 'ready';
12
+ readonly data: T;
13
+ } | {
14
+ readonly phase: 'empty';
15
+ } | {
16
+ readonly phase: 'failed';
17
+ readonly failure: RouteFailure;
18
+ };
19
+ /** Map one classified answer onto the card's four states. */
20
+ export declare function loadStateOf<T>(answer: RouteAnswer<T>, isEmpty: (data: T) => boolean): LoadState<T>;
21
+ /** The locale lookup every block calls (the platform's own `t`). */
22
+ export interface Translate {
23
+ (key: string, values?: Readonly<Record<string, string | number>>): string;
24
+ }
25
+ export type BlockTone = 'neutral' | 'warn' | 'fail';
26
+ export interface BlockProps {
27
+ readonly tone?: BlockTone;
28
+ readonly title?: string;
29
+ readonly children?: ReactNode;
30
+ }
31
+ /** A neutral/ warn/ fail block; a fail block is role=alert so assistive tech announces it. */
32
+ export declare function Block(props: BlockProps): ReactNode;
33
+ export interface FailureBlockProps {
34
+ readonly failure: RouteFailure;
35
+ readonly t: Translate;
36
+ /** The command-face fallback for this surface (rendered on unmounted/failed answers). */
37
+ readonly fallback?: FallbackHint | undefined;
38
+ }
39
+ /** One classified failure as a reader-facing sentence. */
40
+ export declare function failureText(failure: RouteFailure, t: Translate): string;
41
+ /** The failed-state block: sentence + (when the surface exists but is unreachable) the command fallback. */
42
+ export declare function FailureBlock(props: FailureBlockProps): ReactNode;
43
+ //# sourceMappingURL=states.generated.d.ts.map
@@ -0,0 +1,3 @@
1
+ export declare const CSS_TAG_ID = "evolution-merge-panel";
2
+ export declare const CSS: string;
3
+ //# sourceMappingURL=styles.d.ts.map
@@ -0,0 +1,25 @@
1
+ /**
2
+ * The family client scale - GENERATED from packages/scripts/client-tokens.json. Do not edit.
3
+ *
4
+ * Both browser halves of the family carry this file with identical bytes, because the scale is one
5
+ * fact: `verify-client-tokens.mjs` (a gate step) fails when either copy drifts from the source. The
6
+ * scale cannot be shared at runtime - a browser half may not import evolution-core, since every
7
+ * `@deepseek-ai/*` specifier stays external in the client build and the platform module table has no
8
+ * row for it - so the values are generated into each package instead.
9
+ *
10
+ * Usage: put `tokenVars(...)` for your root class in the package stylesheet, then read
11
+ * `var(--evo-group-name)` in its rules.
12
+ * Regenerate with: node packages/scripts/gen-client-tokens.mjs packages
13
+ * @module @deepseek-ai/dsh-evolution-client-scale
14
+ */
15
+ /** Every custom property of the scale, in the source order. */
16
+ export declare const TOKENS: Readonly<Record<string, string>>;
17
+ /** The declarations, without a selector. */
18
+ export declare const TOKEN_CSS: string;
19
+ /**
20
+ * The scale, declared on one element so it never leaks into the host shell.
21
+ * @param scope - the selector the custom properties are declared on.
22
+ * @returns the declaration block to put in the stylesheet.
23
+ */
24
+ export declare function tokenVars(scope: string): string;
25
+ //# sourceMappingURL=tokens.d.ts.map
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Host half of the merge UI package: it registers nothing. The Loader discovers the
3
+ * `dsh.client` manifest and serves `./client` to the page; this entry exists so the row
4
+ * is a valid plugin.
5
+ */
6
+ export declare const name = "evolution-merge-ui";
7
+ export declare const inject: string[];
8
+ export declare function apply(): void;
9
+ //# sourceMappingURL=index.d.ts.map
package/package.json CHANGED
@@ -1,11 +1,56 @@
1
1
  {
2
2
  "name": "@lmzhen/dsh-evolution-merge-ui",
3
- "version": "0.0.0",
4
- "description": "Placeholder for the skill merge UI package; the release is published from CI.",
5
- "license": "MIT",
3
+ "description": "Web surface for skill merge: pending, retry and skipped queues (community build)",
4
+ "dsh": {
5
+ "client": {
6
+ "platform": "web",
7
+ "inject": [
8
+ "@deepseek-ai/dsh-client-ui-sidebar",
9
+ "@deepseek-ai/dsh-client-ui-layout",
10
+ "@deepseek-ai/dsh-client-locale"
11
+ ]
12
+ }
13
+ },
14
+ "version": "0.29.0",
15
+ "publishConfig": {
16
+ "access": "public"
17
+ },
6
18
  "repository": {
7
19
  "type": "git",
8
20
  "url": "git+https://github.com/lmzhen/dsh-evolution.git",
9
21
  "directory": "packages/evolution-merge-ui"
22
+ },
23
+ "type": "module",
24
+ "main": "lib/index.js",
25
+ "types": "lib/types/index.d.ts",
26
+ "exports": {
27
+ ".": {
28
+ "types": "./lib/types/index.d.ts",
29
+ "default": "./lib/index.js"
30
+ },
31
+ "./client": {
32
+ "types": "./lib/types/client/index.d.ts",
33
+ "default": "./lib/client.js"
34
+ },
35
+ "./package.json": "./package.json"
36
+ },
37
+ "files": [
38
+ "lib/*.js",
39
+ "lib/types/**/*.d.ts"
40
+ ],
41
+ "license": "MIT",
42
+ "peerDependencies": {
43
+ "@deepseek-ai/cordis": "^4.0.5",
44
+ "react": "^18.2.0"
45
+ },
46
+ "devDependencies": {
47
+ "@deepseek-ai/cordis": "^4.0.5",
48
+ "@types/react": "~18.3.1",
49
+ "@types/react-dom": "^18.3.1",
50
+ "react": "^18.2.0",
51
+ "react-dom": "^18.2.0"
52
+ },
53
+ "engines": {
54
+ "dsh": ">=0.2.1-rc.1 <0.2.2-0"
10
55
  }
11
56
  }